欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

AJAX异步多文件上传

程序员文章站 2022-06-03 08:51:49
...

目录

需求描述

相关技术

后台接口

前端代码

截图展示

后台代码

upload接口

upload_mulite接口

遇到的问题

1.图片实时预览文件名问题

2.ajaxfileUpLoad.js的onchange事件只响应一次

延伸思考

参考文献:


需求描述

想实现一个可以多选异步提交并实时预览照片的功能。就像发微信朋友圈那样,可以一次性选择多张图片,选择后的图片实时的预览显示,对于预览的图片还可以进行删除操作。

相关技术

实现异步文件上传的功能,此前主要接触过两种,趁着这次机会整理下。

  1. ajaxfileupload.js
  2. 使用FormData

这两种方案最终实现的原理其实都一样的,都是通过异步提交form表单的方式,区别在于ajaxfileupload.js进行了更多的封装,通过将相关入参、file都append到from中然后再放到ifream里进行提交,从而实现页面无刷新的异步提交。FormData更加通用灵活些,通过new 一个FromData对象,然后往里面append记录,然后通过正常ajax请求就可以完成异步提交。

除此之外,还是用了amazeUI的图片画廊组件,主要用于图片的预览,同时还使用了amazeUI的加载进度条效果,来展示上传进度。

后台接口

后台接口可以直接使用MultipartParser类进行处理,也可以通过使用Jfinal的getFiles方法进行处理,其实都是一样的,getFiles方法里面还是通过对MultipartParser的处理来完成的。

前端代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>选择多个图片并展示到的画廊中</title>
    <link rel="stylesheet" type="text/css" href="../assets/amazeui/css/amazeui.min.css">
    <style>
        .del_img {
            position: absolute;
            right: 9px;
            bottom: 1px;
            font-size: 2rem;
            color: white;
            opacity: 0.2;
        filter(alpha = 20);
        }

        .del_img:hover {
            opacity: 1;
        filter(alpha = 100)
        }
    </style>
</head>
<body>
    <div style="padding: 1rem 0 1rem 1rem">
        <input id="mFile" name="mFile" type="file"  accept="image/*" multiple style="display: none">
        <img src="../assets/images/add1.png" style="width: 50px;height: 50px;" οnclick="chooseFile();">
    </div>
    <ul data-am-widget="gallery"
        class="am-gallery am-avg-sm-2 am-avg-md-3 am-avg-lg-4 am-gallery-imgbordered"
        data-am-gallery="{ pureview: true }"
        id="ul_im">
        <li style="display: none"></li>
    </ul>

    <div class="am-panel am-panel-default">
        <div class="am-panel-bd">
            <button type="button" class="am-btn am-btn-primary am-radius" οnclick="submitFiles();">点击提交</button>
        </div>
    </div>
    <script type="text/javascript" src="../assets/amazeui/js/jquery.min.js"></script>
    <script type="text/javascript" src="../assets/amazeui/js/amazeui.min.js"></script>
    <script type="text/javascript" src="../assets/js/ajaxfileupload.js"></script>
    <script type="text/javascript">
        $(function () {
            if (!window.FileReader) {
                alert("您的浏览器不支持在线图片预览,请使用谷歌或者火狐浏览器!");
            }
            $("#mFile").change(function (ev) {
                //uploadFileWithAjaxFileUpLoad(ev);
                uploadFileWithFormData(ev);
            });
        })

        function delImg(obj, name) {
            //主要用于禁止事件传到到外围,之前主要因为删除按钮也是img会造成打开画廊的情况,现在使用span下面三行就可以省略掉
            var e = window.event || arguments.callee.caller.arguments[0];
            e.preventDefault();
            e.stopPropagation();
            //从缩略图里删除对应图片
            $(obj).parents('li').remove();
            //下面的代码主要用于删除画廊里的图片,不然画廊预览的时候你会发现已经被删除的图片还在预览大图画廊里
            var oo = "li[data-title='" + name + "']";
            $(oo).remove();
            //请求网络删除tmp下的临时文件
            $.ajax({
                url: 'http://localhost:8007/LZY/comm/delFile',
                type: 'post',
                data: {
                    f_name: name
                },
                dataType: 'json',
                contentType: 'application/x-www-form-urlencoded;charset=utf-8',
                success: function (data) {
                    console.dir(data);
                    alert("删除成功!");
                },
                error: function (data) {
                    console.dir(data);
                }
            })
        }

        function getli(src, i_name) {
            console.log(i_name);
            var tl = '<li><div style="position: relative;"><div class="am-gallery-item">' +
                '<img src="' + src + '" alt="' + i_name + '"/></div>' +
                '<span class="am-icon-times-circle del_img"  οnclick="delImg(this,' + "'" + i_name + "'" + ');">' +
                '</span></div></li>';
            return tl;
        }

        function chooseFile() {
            //$("#mFile").click();
            $("#mFile").trigger("click");
        }

        function uploadFileWithAjaxFileUpLoad(ev) {
            $.AMUI.progress.start();
            $.ajaxFileUpload(
                {
                    url: 'http://localhost:8007/LZY/comm/upload_mulite', //提交的路径
                    type: 'post',
                    secureuri: false, // 是否启用安全提交,默认为false
                    fileElementId: 'mFile', // file控件id
                    dataType: 'JSON',
                    success: function (data, status) {
                        var files = ev.target.files;
                        var tb = [];
                        for (var i = 0; i < files.length; i++) {
                            var file = files[i];
                            var reader = new FileReader();  // 创建FileReader对象
                            reader.readAsDataURL(file); // 读取file对象,读取完毕后会返回result 图片base64格式的结果
                            /*
                                这里有一个异常问题,记录下,reader.onload是异步的,会出现循环已经跑完了,
                                但是reader.onload还没一个一个执行。如果我们像在reader.onload的function里传
                                外部参数,例如把文件名传过去,你会神奇的发现,文件名永远都是最后一个文件的文件名。
                                因为循环始终比reader快的多。
                                为了解决这个问题,我只能暂时先把reader的结果按照顺序存到数组里,最后再统一生成
                                dom。
                             */
                            reader.onload = function () {
                                tb.push(this.result);
                                if (tb.length == files.length) {
                                    //说明全部生成成功,下面再来刷入dom
                                    for (var i = 0; i < files.length; i++) {
                                        $("#ul_im").append(getli(tb[i], files[i].name));
                                    }
                                }
                            }
                        }
                        $.AMUI.progress.done();
                    },
                    // complete: function (xq) {
                    //     $("#mFile").on("change", function (ev) {
                    //         uploadFileWithAjaxFileUpLoad(ev);
                    //     })
                    // },
                    error: function () {
                        $.AMUI.progress.done();
                    }
                });
        }

        function uploadFileWithFormData(ev) {
            $.AMUI.progress.start();
            var form = new FormData();
            var files = ev.target.files;
            for (var i = 0; i < files.length; i++) {
                form.append("file" + i, files[i]);
            }
            $.ajax({
                url: "http://localhost:8007/LZY/comm/upload",
                type: "post",
                data: form,
                processData: false,//不处理发送的数据
                contentType: false,
                success: function (result) {
                    $.AMUI.progress.done();

                    var files = ev.target.files;
                    var tb = [];
                    for (var i = 0; i < files.length; i++) {
                        var file = files[i];
                        var reader = new FileReader();  // 创建FileReader对象
                        reader.readAsDataURL(file); // 读取file对象,读取完毕后会返回result 图片base64格式的结果
                        /*
                            这里有一个异常问题,记录下,reader.onload是异步的,会出现循环已经跑完了,
                            但是reader.onload还没一个一个执行。如果我们像在reader.onload的function里传
                            外部参数,例如把文件名传过去,你会神奇的发现,文件名永远都是最后一个文件的文件名。
                            因为循环始终比reader快的多。
                            为了解决这个问题,我只能暂时先把reader的结果按照顺序存到数组里,最后再统一生成
                            dom。
                         */
                        reader.onload = function () {
                            tb.push(this.result);
                            if (tb.length == files.length) {
                                //说明全部生成成功,下面再来刷入dom
                                for (var i = 0; i < files.length; i++) {
                                    $("#ul_im").append(getli(tb[i], files[i].name));
                                }
                            }
                        }
                    }
                },
                error: function () {
                    $.AMUI.progress.done();
                }
            });
        }

        //提交文件
        function submitFiles() {

        }

    </script>
</body>
</html>

截图展示

AJAX异步多文件上传

点击图片可以预览查看大图,并且可以通过左右箭头循环查看。这个预览效果非常好,另外,如果图片非常大,amazeUI的图片画廊组件还支持各种优化方案,具体请移步这里

后台代码

 public void upload()
    {
        ResultJson         rj    = new ResultJson();
        List<UploadFile> rList = getFiles();
        List<String> rtList = new ArrayList<>();
        for (UploadFile uFile : rList)
        {
            String fileName   = uFile.getFileName();
            String uploadPath = uFile.getUploadPath();//获取保存文件的文件夹
            System.out.println(uploadPath);
            String filePath  = uploadPath + "/" + fileName;//保存文件的路径
            String rfileName = FileUtil.changeFileName(filePath);//为了避免相同
            rtList.add(rfileName);
            System.out.println(fileName+"---->"+rfileName);
        }
        rj.setCode(CONST.SUCCESS);
        rj.setDescription("新增成功!");
        rj.setResult(rtList);
        renderJson(rj);
    }

    public void upload_mulite()
    {
        try{
            MultipartParser mp = new MultipartParser(getRequest(), 50 * 1024 * 1024, false, false, "UTF-8");
            Part part = null;
            while ((part = mp.readNextPart()) != null)
            {
                if (part.isFile())
                {
                    FilePart filePart = (FilePart) part;
                    String fileName = FileUtil.randomFile(filePart.getFileName());
                    filePart.writeTo(new File(fileName));
                }
            }
        }catch (Exception e)
        {
            e.printStackTrace();
        }
        renderJson(MessageUtil.successJson());
    }

    //删除图片,这里是删除再tmp里的图片,在提交的时候,不至于产生脏数据。
    public void delFile()
    {
        String f_name = getPara("f_name");
        if (CommTool.strIsEmpty(f_name))
        {
            renderJson(MessageUtil.defaultEmptyJson());
            return;
        }
        FileUtil.delFile(FileUtil.getDefaultFileName(f_name));
        renderJson(MessageUtil.successJson());
    }

后台主要写了三个方法,其中:

upload接口

该接口使用的Jfinal的getFIles()方法来获取前端提交的文件的,这个接口只有在使用FormData()方式的时候才可以正常使用。如果使用uploadFileWithAjaxFileUpLoad方法会有bug,因为getFiles()方法默认是根据input来获取参数信息的,也就是说只能获取到最后一个文件,前面的文件无法取到。但是如果是单文件上传,则没这个问题。

upload_mulite接口

该接口是比较通用的处理方案,通过对MultipartParser对象的处理,循环获取所有的参数和对象,并按需处理。可以同时支持FormData提交也可以支持ajaxFileUpLoad方式提交。

遇到的问题

1.图片实时预览文件名问题

通过代码可以看到,实时预览的方案其实是通过FileReader来实现的,具体是把文件读取出来,然后转换成图片的base64格式,直接丢给img的src来展示图片。通过给input对象添加onchange事件来监控文件的变化信息,从而将图片文件实时的预览,在预览的同时,我还想把每一个文件的文件名记录到图片画廊的alt参数中。
根据需求比较直观的处理方案就是在ajax提交成功之后,将文件实时的预览,并生成图片画廊的dom元素,我之前想的是在reader.onload中通过循环来以此的把文件名传进去同时生成每一个文件的dom元素,然后append到ul上,后来发现,所有文件的文件名总是最后一个文件的文件名!

原因在于:reader.onload是异步的,那么会出现外层循环文件已经跑完了,但是reader.onload还没有一个一个执行下去。如果我们像在reader.onload的function里传外部参数,例如把文件名传过去,你会神奇的发现,文件名永远都是最后一个文件的文件名。因为循环始终比reader快的多。为了解决这个问题,我只能暂时先把reader的结果按照顺序存到数组里,最后再统一生成
dom。

2.ajaxfileUpLoad.js的onchange事件只响应一次

该问题主要原因在于,插件把原来绑定了onchange时间的input转移到了ifream的form中,然后重新clone了一个一模一样的input放到原来的位置,但是克隆的input并没有绑定onchange时间,所以你会发现第二次再选择文件的时候,不响应提交事件了。

解决办法有两种,第一种是在代码中注释掉的complete方法,在ajaxfileupload提交成功完成之后,重新对input绑定onchange事件。第二种是修改ajaxfileupload.js的源码,主要修改createUploadForm方法中clone()为clone(true),添加ture参数意味在拷贝input的同时把对应的事件也拷贝了。具体代码如下:

    createUploadForm: function(id, fileElementId,data)
	{
		//create form	
		var formId = 'jUploadForm' + id;
		var fileId = 'jUploadFile' + id;
		var form = jQuery('<form  action="" method="POST" name="' + formId + '" id="' + formId + '" enctype="multipart/form-data"></form>');	
		var oldElement = jQuery('#' + fileElementId);
		//var newElement = jQuery(oldElement).clone();//修改前
        var newElement = jQuery(oldElement).clone(true);//修改后
		jQuery(oldElement).attr('id', fileId);
		jQuery(oldElement).before(newElement);
		jQuery(oldElement).appendTo(form);
		//set attributes
		jQuery(form).css('position', 'absolute');
		jQuery(form).css('top', '-1200px');
		jQuery(form).css('left', '-1200px');
		if (data) { for (var i in data) { $('<input type="hidden" name="' + i + '" value="' + data[i] + '" />').appendTo(form); } }
		jQuery(form).appendTo('body');
		return form;
    },

其实仔细观察这段代码,你就能清晰看到ajaxfileupload插件的实现原理。

延伸思考

假如页面有多个input file,如果做到这种方式的一次性多个文件提交呢?

其实非常简单,对于FormData的方式是最简单的,无非是把每一个input的file获取到append到FormData中,然后还按照上面的提交即可。

但是对于ajaxfileupload插件而言,就需要进行插件的改造了,不过改造也不复杂,通过对上面的createUploadForm方法的研究不难发现,我们只需要把所有的input file的id传过来,在这里把所有的input放到ifream下的from表单中即可。通过这种方式的改造,ajaxfileupload插件也可以使用jfinal的getFiles()方法获取所有文件,具体改造可以参考参考文献中的链接。

参考文献:

【1】ajaxfileupload插件多文件上上传改造

【2】ajaxfileupload插件多文件上上传改造

【3】amazeUI的图片画廊

【4】FileReader实时预览图片

【5】CSS鼠标经过图片变亮,移开变变暗效果