zoukankan      html  css  js  c++  java
  • formData批量上传的多种实现

    前言

      最近项目需要批量上传附件,查了下资料,网上很多但看着一脸懵,只贴部分代码,介绍也不详细,这里记录一下自己的采坑与多种实现,以免以后忘记。

      这里先介绍下FormData对象,以下内容摘自:https://developer.mozilla.org/zh-CN/docs/Web/API/FormData

      XMLHttpRequest Level 2添加了一个新的接口FormData.利用FormData对象,我们可以通过JavaScript用一些键值对来模拟一系列表单控件,我们还可以使用XMLHttpRequest的send()方法来异步的提交这个"表单".比起普通的ajax,使用FormData的最大优点就是我们可以异步上传一个二进制文件.

      在我的自定义input文件上传样式里就已经实现里单文件上传,并且实现了自定义input样式;如果构造FormData对象是传入表单js对象,formData会自动注入表单里的值;如果是new一个空对象,然后手动append的表单类型为file时要注意:这里append进去的是File对象,而不是FileList对象

       

    效果

      先看一下大概效果:

     代码编写

      controller有两种方法:三种方式调的都是用一个接口

        /**
         * 批量上传
         */
        @PostMapping("upload")
        public ResultModel<List<AttachmentVo>> upload(HttpServletRequest request, @RequestParam("applyId") String applyId){
            List<MultipartFile> multipartFileList = ((MultipartHttpServletRequest) request).getFiles("attachment");
            System.out.println(multipartFileList.size());
            System.out.println(applyId);
            return null;
        }
    
        /**
         * 批量上传2 (推荐使用)
         */
        @PostMapping("upload2")
        public ResultModel<List<AttachmentVo>> upload2(MultipartFile[] attachment,@RequestParam("applyId") String applyId){
            System.out.println(attachment.length);
            System.out.println(applyId);
            return null;
        }

      自定义样式:(三种方式都是用这个样式),要引入bootstrap, 图标用的是font awesome

      

          .nav-bar {
              border-top: 1px solid #9E9E9E;
              margin: 10px 0 20px;
          }
    
          .nav-bar-title {
              margin: -13px 0 0 35px;
              background-color: white;
              padding: 0 10px;
              float: left;
              color: #199ED8;
          }
           
           .attachment-remove {
                font-size: 25px;
                color: red;
                margin-left: 5px;
                cursor: pointer;
            }
    
            .attachment-text-p {
                border: 1px solid #c2cad8;
                padding: 5px 5px;
                margin: 0;
                float: left;
                height: 30px;
                width: 90%;
            }
    
            .attachment-text-p + i {
                float: left;
                line-height: 30px !important;
            }
    
            .input-attachment {
                width: 90% !important;
                padding: 4px 12px !important;
            }

    方式1

      点击Add,追加一个input,点击Delete,删除一个input,点击叉号也可以删除对应的input,需要单独为每个input选择文件

      效果

      html

    <form id="attachments" enctype="multipart/form-data" class="form-horizontal nice-validator n-yellow" novalidate="novalidate">
            <div class='form-body'>
                <div class='form-group'>
                    <label class="control-label col-md-1">附件管理:</label>
                    <div class="col-md-4">
                        <button id="attachmentAddBtn" type="button" class="btn btn-default">Add Attachment</button>
                        <button id="attachmentDeleteBtn" type="button" class="btn btn-default">Delete Attachment</button>
                        <button id="attachmentUploadBtn" type="button" class="btn btn-default">Upload</button>
                    </div>
                </div>
                <div class='form-group'>
                    <label class="control-label col-md-1">附件上传:</label>
                    <div id="attachmentInputs" class="col-md-3">
    
                    </div>
                </div>
            </div>
        </form>

      js

        //attachment-remove
        $("#attachmentInputs").on("click", ".attachment-remove", function (even) {
            $(this).prev().remove();//删除上一个兄弟节点
            $(this).remove();//删除自己
        });
    
        //add but
        $("#attachmentAddBtn").click(function (even) {
            //name值一样就可以
            $("#attachmentInputs").append("<input name="attachment" type="file" class="form-control input-attachment"/><i class="fa fa-times attachment-remove"></i>");
        });
    
        //delete
        $("#attachmentDeleteBtn").click(function (even) {
            var files = $("#attachmentInputs input[type='file']");
            files.each(function (index, element) {
                //从最下面开始删除,至少保留一个
                if (!(index === 0) && index === (files.length - 1)) {
                    $(element).next().remove();
                    $(element).remove();
                }
            });
        });
    
        //upload
        $("#attachmentUploadBtn").click(function (even) {
            //1、通过HTML表单创建FormData对象 自动注入
            // var formData = new FormData($("#attachments")[0]);
    
            //2、从零开始创建FormData对象 手动注入
            var formData = new FormData();
            //注入 name=file
            var files = $("#attachmentInputs input[type='file']");
            for (var i = 0; i < files.length; i++) {
                //注意:这里append进去的是File对象,而不是FileList对象
                formData.append("attachment", files[i].files[0]);
            }
            //注入name=text
            formData.append("applyId", "123456");
    
            console.log(formData.getAll("attachment"));
            
            //执行上传
            $.ajax({
                url: ctx + "/attachment/upload2",
                type: "post",
                data: formData,
                processData: false,
                contentType: false,
                success: function (data) {
                },
                error: function (e) {
                }
            });
        });
    
        //add one input
        $("#attachmentAddBtn").click();

    方式2

      第二种方式只有一个input,用的是multiple="multiple"属性,可以再弹窗里选择多个文件提交,如果再加工一下,也做成第三种一样,展示出文件名,同时可以删除对应的文件

      效果

      html

    <form id="attachments2" enctype="multipart/form-data" class="form-horizontal" novalidate="novalidate">
            <div class='form-body'>
                <div class='form-group'>
                    <label class="control-label col-md-1">附件管理:</label>
                    <div class="col-md-4">
                        <button id="attachmentUploadBtn2" type="button" class="btn btn-default">Upload</button>
                    </div>
                </div>
                <div class='form-group'>
                    <label class="control-label col-md-1">附件上传:</label>
                    <div id="attachmentInputs2" class="col-md-3">
                        <input name="attachment" type="file" class="form-control input-attachment" multiple="multiple"/>
                    </div>
                </div>
            </div>
        </form>

      js

       //upload2
        $("#attachmentUploadBtn2").click(function (even) {
            //1、通过HTML表单创建FormData对象 自动注入
            // var formData = new FormData($("#attachments2")[0]);
    
            //2、从零开始创建FormData对象 手动注入
            var formData = new FormData();
            //注入 name=file
            var files = $("#attachmentInputs2 input[type='file']");
            for (var i = 0; i < files[0].files.length; i++) {
                formData.append("attachment", files[0].files[i]);
            }
            //注入name=text
            formData.append("applyId", "123456");
    
            console.log(formData.getAll("attachment"));
    
            //执行上传
            $.ajax({
                url: ctx + "/attachment/upload2",
                type: "post",
                data: formData,
                processData: false,
                contentType: false,
                success: function (data) {
                },
                error: function (e) {
                }
            });
        });

    方式3

       定义了一个隐藏的input,并将Select File按钮的click与input的click对等,点击按钮相当于点击input,弹出选择文件对话框,监听了input的change事件,将选择的file对象push到全局数组变量attachmentArray中,点击Upload时再遍历注入到formData中

      效果

      html

    <form id="attachments3" enctype="multipart/form-data" class="form-horizontal" novalidate="novalidate">
            <div class='form-body'>
                <div class='form-group'>
                    <label class="control-label col-md-1">附件管理:</label>
                    <div class="col-md-4">
                        <button id="selectFile" type="button" class="btn btn-default">Select File</button>
                        <button id="attachmentUploadBtn3" type="button" class="btn btn-default">Upload</button>
                    </div>
                </div>
                <div class='form-group'>
                    <label class="control-label col-md-1">附件上传:</label>
                    <input id="attachmentInputs3" type="file" style="display: none;"/>
                    <div id="attachmentText3" class="col-md-3">
                    </div>
                </div>
            </div>
        </form>

      js

        //存放file对象
        var attachmentArray = [];
        //attachment-remove
        $("#attachmentText3").on("click", ".attachment-remove", function (even) {
            //删除attachmentArray数据
            attachmentArray.splice($(this).data("index"), 1);
            //删除html对象
            $(this).prev().prev().remove();
            $(this).prev().remove();
            $(this).remove();
        });
    
        //Select File
        $("#selectFile").click(function (even) {
            // 获取input
            $("#attachmentInputs3").click();
        });
    
        //input change
        $("#attachmentInputs3").change(function (even) {
            // 获取input
            var fileName = $(this).val();
            var file = $(this)[0].files[0];
            //是否选择了文件
            if (fileName) {
                attachmentArray.push(file);
                $("#attachmentText3").append("<div><p class='attachment-text-p'>" + fileName + "</p><i data-index='" + (attachmentArray.length - 1) + "' class="fa fa-times attachment-remove"></i></div>")
            }
        });
    
        //upload3
        $("#attachmentUploadBtn3").click(function (even) {
            //这里只能手动注入
            var formData = new FormData();
            //遍历数据,手动注入formData
            for (var i = 0; i < attachmentArray.length; i++) {
                formData.append("attachment", attachmentArray[i]);
            }
            formData.append("applyId", "123456");
            console.log(formData.getAll("attachment"));
            //执行上传
            $.ajax({
                url: ctx + "/attachment/upload",
                type: "post",
                data: formData,
                processData: false,
                contentType: false,
                success: function (data) {
                },
                error: function (e) {
                }
            });
        });

       2019-12-31更新:感谢 Spring2Sun 指出错误之处,发现了方式三的一个bug;

        bug描述:进行删除后,数组长度已经减1,但标签的data-index的值没有更新,导致后面再进行删除时下标对应不上,删除后标签与数据对应错乱

        bug修复:

          1、在移除数组和元素后,对剩下的i 标签data-index 重置下顺序

    //删除
    $("#attachmentText3").on("click", ".attachment-remove", function (even) {
            //其他地方不变,省略代码...
            
           //重新排序标签data-index
          $("#attachmentText3 i").each(function (index, element) {
                  $(element).attr("data-index", index);
           });
    });

          2、进行删除时不再删除数组数据,而是将对应的下标设置成“-1”,上传遍历数组时,再进行判断不等于"-1"时才append

    //删除
    $("#attachmentText3").on("click", ".attachment-remove", function (even) {
            //将值设置为"-1"
            attachmentArray[$(this).data("index")] = "-1";
    
            //其他地方不变,省略代码...        
    });
    //上传
    $("#attachmentUploadBtn3").click(function (even) {
            //这里只能手动注入
            var formData = new FormData();
            //遍历数据,手动注入formData
            for (var i = 0; i < attachmentArray.length; i++) {
                let value = attachmentArray[i];
                if(value != "-1"){
                    formData.append("attachment", value);
                }
            }
    
            //其他地方不变,省略代码...
    });    

    后记

      最后看一下file数据、请求头、还有振奋人心的后台成功接参图

      file数据

    请求头

    成功接参

    新需求

      项目需要支持同一张单上面有多个上传组件,按照我们之前的三种方式并不满足,第一种使用了id的方式去绑定,当多个组件在同一个html的时候就不行了,第三种我们采用一个全局数组变量来存选中的file,但之前一个组件有引一次js,当多个的时候就会重复引入,后面引入的变量、方法就会覆盖前面,同时,应该用的是id,当我们调用upload方式时不知道applyId工单号对应的form是哪一个,无法绑定附件的工单号,这里改进一下,将第一种跟第三种整合一下。

      上传组件html

      使用的是thymeleaf,th:text="#{attachment.title}"是国际化,<script th:replace="common/head::static"></script>引入的是公用的js、css,上传组件的js、css写在common里面,所有的页面都会引入它们,而且只引入一次。这里给每个form表单绑定一个applyId属性,对应具体的工单号,这样我们调用upload的时候就可以找到对应的form表单

    <!DOCTYPE html>
    <html xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8"/>
        <title th:text="#{attachment.title}"></title>
        <script th:replace="common/head::static"></script>
    </head>
    <body>
    <!--
      使用方法:在任意工单页面添加此DIV
      <div th:replace="attachment/attachment::attachmentPage(${applyId})"></div>
    
      调用上传方法:Attachment.upload(${applyId});
    -->
    <div th:fragment="attachmentPage(applyId)">
        <div class="nav-bar"><span class="nav-bar-title" th:text="#{attachment.title}"></span></div>
        <form th:applyId="${applyId}" class="form-horizontal attachments-form" enctype="multipart/form-data">
            <div class='form-body'>
                <div class='form-group'>
                    <label class="control-label col-md-1">附件管理:</label>
                    <div class="col-md-4">
                        <button type="button" class="btn btn-default" onclick="Attachment.appendAttachmentInput(this)">
                            Select File
                        </button>
                    </div>
                </div>
                <div class='form-group'>
                    <label class="control-label col-md-1">附件列表:</label>
                    <div class="col-md-10 attachments-list"></div>
                </div>
            </div>
        </form>
    </div>
    </body>
    </html>

      其他任意html调用

      thymeleaf的传值方式之一,与组件html的 th:fragment="attachmentPage(applyId)" 配合使用,后面就可以这样使用 th:applyId="${applyId}"

            <div th:replace="attachment/attachment::attachmentPage(123456)"></div>
            <div th:replace="attachment/attachment::attachmentPage(111111)"></div>

      common.js  上传组件部分

      removeAttachmentInputListener,监听×号的点击事件,要在common.js执行一次。

    /**
     * 三、附件上传的方法
     */
    var Attachment = {
        //上传附件
        upload: function (applyId) {
            //终止上传
            if (!applyId) {
                layer.msg(i18n('attachment.applyid.is.null'));
                return;
            }
    
            //添加附件
            var formData = new FormData();
            $("form[applyId='"+applyId+"']").find("input[name='attachment']").each(function (index, element) {
                //过滤操作:input框有值,才append到formData
                if ($(element).val()) {
                    formData.append("attachment",element.files[0]);
                }
            });
    
            //追加applyId到formData
            formData.append("applyId", applyId);
    
            //执行上传
            $.ajax({
                url: ctx + "/attachment/upload",
                type: "post",
                data: formData,
                processData: false,
                contentType: false,
                success: function (data) {
                    if (checkResult(data)) {
                        console.log('附件上传成功:', data);
                    } else {
                        throw e;
                    }
                },
                error: function (e) {
                    console.log('附件上传失败');
                    throw e;
                }
            });
        },
        //添加附件
        appendAttachmentInput: function (btn) {
            //先追加html
            $(btn).parents('.attachments-form').find(".attachments-list").append("<div><input type="file" name="attachment" class="hidden"/></div>");
    
            //最新追加的input
            var attachments = $(btn).parents('.attachments-form').find(".attachments-list").find("input[name='attachment']");
    
            //绑定input的change事件,注意:当我们点击取消或×号时并不触发,但是无所谓,我们在upload方法进行过滤空的input就可以了
            attachments[attachments.length - 1].onchange = function(){
                var fileName = $(this).val();
                if (fileName) {
                    $(this).parent("div").append("<p class='attachment-text-p'>" + fileName + "</p><i class="fa fa-times attachment-remove"></i>");
                }else{
                    $(this).parent("div").remove();
                }
            };
    
            //触发最新的input的click
            attachments[attachments.length - 1].click();
        },
        //删除附件
        removeAttachmentInputListener: function () {
            $(".attachments-form").on("click", ".attachment-remove", function (even) {
                $(this).parent().remove();
            });
        }
    };

      common.css 上传组件部分

        .attachment-remove {
            font-size: 25px;
            color: red;
            margin-left: 5px;
            cursor: pointer;
        }
        
        .attachment-text-p {
            border: 1px solid #c2cad8;
            padding: 5px 5px;
            margin: 0;
            float: left;
            height: 30px;
            width: 90%;
            margin-top: 5px;
        }
        
        .attachment-text-p + i {
            float: left;
            line-height: 30px !important;
            margin-top: 5px;
        }

    新需求效果

    报错记录:org.apache.tomcat.util.http.fileupload.FileUploadBase$FileSizeLimitExceededException: The field images exceeds its maximum permitted size of 1048576 bytes.

    解决:调大http的最大上传大小

    string:
      http:
        multipart:
          max-file-size: 5Mb #单个文件大小
          max-request-size: 50Mb #总大小
    string:
      servlet:
        multipart:
          max-file-size: 5Mb #单个文件大小
          max-request-size: 50Mb #总大小

      导出文件到浏览器

      2019-10-24补充:上传、下载通常是密不可分的两个功能,这里记录一下如何导出文件到浏览器然后下载到本地

      前端js

    //数据数组,ids
    let data = [1,2,3,4];
    //ajax不支持下载类型,使用location.href或者表单提交
    //window.location.href,get提交,数据会暴露在URL,相对不安全
    //创建临时的、隐藏的form表单,post提交,数据在请求体里,相对安全
    var $form = $(document.createElement('form')).css({display: 'none'}).attr("method", "POST").attr("action", ctx + "/downLoad");
    var $input = $(document.createElement('input')).attr('name', "ids").val(JSON.stringify(data));
    $form.append($input);
    $("body").append($form);
    $form.submit();
    //提交完成后remove掉
    $form.remove();

      java后端

    @PostMapping("/downLoad")
    public ResponseEntity downLoad(String ids) throws IOException {
        //json字符串转换成对象
        List<String> idList  = new ObjectMapper().readValue(ids, TypeFactory.defaultInstance().constructCollectionType(List.class, String.class));
    
        //处理、拼接数据
        List<StringBuilder> students = new ArrayList<>();
        assert idList != null;
        idList.forEach((id) -> {
            //账号-密码-区服-角色-道具
            StringBuilder str = new StringBuilder();
            SuperSearchUcidVo searchUcidVo = superSearchUcidService.get(Integer.valueOf(id)).getData();
            str.append(searchUcidVo.getUserName()).append("-");
            str.append(searchUcidVo.getPassword()).append("-");
            str.append(searchUcidVo.getDivision()).append("-");
            if(!StringUtils.isEmpty(searchUcidVo.getRoleList())){
                String[] roleList = searchUcidVo.getRoleList().split(",");
                for (String role : roleList) {
                    str.append(role).append("|");
                }
            }
            str.append("-");
            if(!StringUtils.isEmpty(searchUcidVo.getPropsList())){
                String[] propsList = searchUcidVo.getPropsList().split(",");
                for (String props : propsList) {
                    str.append(props).append("|");
                }
            }
    
            students.add(str);
        });
        StringBuilder write = new StringBuilder();
        students.forEach((str) -> write.append(str).append("
    "));
    
        //文件数据、文件名
        byte[] fileBytes = write.toString().getBytes("GBK");
        String fileName = "GameAccountExport_" + new Date().getTime() + ".txt";
    
        //设置响应头
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
        headers.setContentDispositionFormData("attachment", fileName);
    
        //下载文件
        return new ResponseEntity<>(fileBytes, headers, HttpStatus.CREATED);
    }

      效果

    版权声明

    作者:huanzi-qch
    若标题中有“转载”字样,则本文版权归原作者所有。若无转载字样,本文版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接,否则保留追究法律责任的权利.


    捐献、打赏

    请注意:作者五行缺钱,如果喜欢这篇文章,请随意打赏!

    支付宝

    微信


    QQ群交流群

    QQ群交流群
    有事请加群,有问题进群大家一起交流!

  • 相关阅读:
    案例20:地下汽车库消防设施配置案例分析
    jsonp 跨域
    Cookie 工具类
    spring md5 加密
    sso 登录业务逻辑
    maven tomcat 插件
    <url-pattern>/</url-pattern> 拦截请求
    sso 系统分析
    FreeMarker 页面静态化解决方案
    FreeMarker 整合 springmvc
  • 原文地址:https://www.cnblogs.com/huanzi-qch/p/9853067.html
Copyright © 2011-2022 走看看