使用File API开发移动端压缩上传图片插件

387 阅读9分钟

       在平时的业务需求开发中,几乎每个项目都会用到上传图片的功能,鉴于移动端符合实际业务需要的上传图片开源插件较少,所以自己动手写了一个。现在把这次开发经验分享出来,与大家一起交流,另外插件也可以直接被应用到实际项目的开发中,欢迎大家去下载使用。

       目前智能手机的像素越来越高,受网络条件限制,移动端上传图片的体验会大打折扣,另外也会增加用户的流量成本,所以在上传之前先压缩势在必行。本插件主要实现了本地图片预览与删除、图片压缩及上传的功能,使用原生js实现,语法标准为ES5,插件效果如下图所示:




       压缩上传的核心实现步骤:

       1.使用对象URL实现图片数据的读取,并返回图片的DataURL(也可以使用FileReader对象实现);

       2.创建Image对象,url属性值为步骤1返回的DataURL;

       3.待Image对象加载成功之后,使用Canvas重绘图像,再调用toDataURL方法并设置压缩参数返回新的DataURL实现压缩,并且用图片数据创建节点插入到DOM实现图片预览;

        4.创建FormData对象保存DataURL格式的图片,并创建XMLHttpRequest对象向服务器发送FormData数据。


        具体实现流程如下,html、css部分直接贴代码,主要关注js部分的实现:

1、项目结构。


2、创建html结构。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>File Upload</title>
    <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <link rel="stylesheet" href="./dist/css/base.css"> 
    <link rel="stylesheet" href="./dist/css/fileupload.css"> 
</head>
<body>
    <div class="upload-container">
        <div class="upload-title">请点击“+”按钮上传图片</div>
        <div class="upload-image-picker">
            <div class="upload-image-picker-list">
                <div class="upload-flexbox">
                    <div class="upload-flexbox-item">
                        <div class="upload-image-picker-item upload-image-picker-upload-btn"><input type="file" accept="image/jpeg,image/jpg,image/png" multiple class="upload__input"></div>
                    </div>
                </div>
            </div>
        </div>
        <div class="upload-tip">提示:只能上传jpeg、jpg、png文件,每张图片不超过15M</div>
    </div>
    <div class="mask">
        <div class="loading"><div class="info"><div class="circle"></div>上传中...</div></div>
    </div>
    <script src="./dist/js/fileupload.js" charset="utf-8"></script>
</body>
</html>


3、css样式(详细代码请查看项目源码,可根据实际业务需要自行定制UI)。


4、搭建模块的基本结构。

  • 分析需求场景,可以抽象出3个自定义的API,分别是压缩等级、添加图片的接口地址、删除图片的接口地址;
  • 使用面向对象的方式设计一个构造函数FileUpload,FileUpload接收一个可配置的参数对象,同时设置默认配置参数;
  • 使用匿名自执行函数把插件逻辑封装在一个私有作用域中。

;
(function(window) {
    var FileUpload = function(options) {
        var self = this;
        // 设置默认参数
        this.options = {
            compressPercent: 0.3, //压缩比例,最大为1
            addItemApi: "", //新增图片的接口
            deleteItemApi: "" //删除图片的接口
        }
        // 扩展参数
        var extendOptions = function(target, src) {
            for (var prop in src) {
                if (src.hasOwnProperty(prop)) {
                    target[prop] = src[prop];
                }
            }
            return target;
        }
        extendOptions(this.options, options || {})
    }

    FileUpload.prototype = {  
    }

    window.FileUpload = FileUpload;
})(window);


5.查询要操作的DOM节点,并绑定事件。

  • 获取相应的DOM节点保存在this对象中;
  • 监听input元素的change事件,以及删除按钮的click事件,将事件处理程序的交互逻辑封装在构造函数的prototype对象中,FileUpload的实例能继承这些方法。

;
(function(window) {
    var FileUpload = function(options) {
        var self = this;

        // 设置默认参数
        this.options = {
            compressPercent: 0.3, //压缩比例,最大为1
            addItemApi: "", //新增图片的接口
            deleteItemApi: "" //删除图片的接口
        }

        // 扩展参数(浅拷贝)
        var shallowCopy = function(target, src) {
            for (var prop in src) {
                if (src.hasOwnProperty(prop)) {
                    target[prop] = src[prop];
                }
            }
            return target;
        }
        shallowCopy(this.options, options || {})

        //引用元素
        this.uploadFlexbox = document.querySelector(".upload-flexbox"); //图片要展示的项目容器
        this.uploadButton = document.querySelector(".upload-image-picker-upload-btn"); //上传按钮
        this.btnBelongToFlexItem = this.uploadButton.parentNode; //上传按钮所在的项目节点
        this.uploadInput = document.querySelector(".upload__input"); //input元素
        this.fragmentWrap = document.createDocumentFragment(); //文档片段容器
        this.targetNodeBelongToItem = null; //缓存点击(删除)事件的目标对象所属的节点项目
        this.mask = document.querySelector(".mask"); //loading遮罩层

        //添加input file元素的change事件处理程序
        this.uploadInput.addEventListener("change", function(event) {
            self.handleEvent(event)
        })

        //添加删除按钮的事件处理程序
        this.uploadFlexbox.addEventListener("click", function(event) {
            if (event.target.className.indexOf("upload-image-picker-item-remove") > -1) {
                self.deleteItem(event.target)
            }
        }, false)

    }

    FileUpload.prototype = {
    }

    window.FileUpload = FileUpload;
})(window);


6、封装input元素的change事件的交互函数handleEvent。

  • 获取change事件目标对应的files数组。
  • 创建FormData对象。
  • 封装对象URL的API(浏览器兼容处理)。
  • 遍历files数组,使用createObjectURL方法实现图片数据的读取,并返回图片的DataURL,设定图片尺寸的限制条件。(图片加载属于异步操作,因此while循环中请使用闭包)
  • 创建Image对象,将图片的DataURL赋给Image对象的url属性。
  • 待Image对象加载成功之后(注意图片加载成功的顺序可能与用户选取图片时的顺序不一致):
    1. 为FormData对象设置键值,使用canvas重绘图像、压缩,并且向文档片段容器中插入DOM节点;
    2. 利用计数器记录单次上传已加载完成的图片数量,当图片全部加载完成时,把文档片段容器一次性插入到DOM中,同时向服务器发送新增请求;
    3. 释放引用的对象URL,减少内存占用。

//input file元素change事件的回调函数
handleEvent: function(event) {
    var self = this;
    var uploadData,
        xhr,
        files,
        i,
        len,
        counter;

    uploadData = new FormData();
    files = event.target.files;
    i = 0;
    len = files.length;
    counter = 0; //计数器,记录每次上传后已加载成功的图片个数


    while (i < len) {

        //异步加载图片,使用闭包封装变量
        (function(j) {
            var newSrc,
                type,
                img,
                compressedDataURL,
                domFrag,
                keyName;

            //判断图片大小,不允许超过15M
            if (files[j].size / 1024 > 15 * 1024) {
                alert("上传的图片过大,请重新选择图片!");
                return;
            }
            //引用对象URL
            newSrc = self.createObjectURL(files[j]);
            type = files[j].type;
            img = new Image();
            img.src = newSrc;
            keyName = "file" + j;
            img.onload = function() {

                // 生成DataURL
                compressedDataURL = self.compress(img, type);

                //为FormData对象设置键值
                uploadData.append(keyName, compressedDataURL);

                //点击按钮新增图片项目(插入到文档片段容器中保存)
                self.addFragItem(compressedDataURL);

                //计数器自增
                counter++

                //图片全部加载完成时
                if (counter == len) {
                    // 发送新增请求
                    self.mask.style.display = "block"
                    self.sendRequest(self.options.addItemApi, uploadData)
                }

                //释放引用的对象URL,减少内存占用
                self.revokeObjectURL(newSrc)
            };
            img.onerror = function(err) {
                console.log(err, "图片加载失败")
            }

        })(i)

        i++;
    }

},


7、封装对象URL API(浏览器兼容处理)。

  • 创建对象URL;
  • 释放对象URL的引用。

//引用对象URL
createObjectURL(blob) {
    if (window.URL) {
        return window.URL.createObjectURL(blob)
    } else if (window.webkitURL) {
        return window.webkitURL.createObjectURL(blob)
    } else {
        return null
    }
},
//释放对象URL的引用
revokeObjectURL(url) {
    if (window.URL) {
        window.URL.revokeObjectURL(url)
    } else if (window.webkitURL) {
        window.webkitURL.revokeObjectURL(url)
    }
},


8、封装canvas压缩的交互函数。

  • 传入Image对象,绘制canvas图片;
  • 使用canvas的toDataURL方法将图片转成DataURL格式。

// 压缩图片,返回DataURL
compress(img, type) {
    var canvas = document.createElement("canvas");
    var ctx = canvas.getContext("2d");
    var initSize = img.src.length;
    var width = img.width;
    var height = img.height;
    canvas.width = width;
    canvas.height = height;
    // 铺底色
    ctx.fillStyle = "#fff";
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    ctx.drawImage(img, 0, 0, width, height);

    //进行压缩
    var compressed = canvas.toDataURL("image/jpeg", this.options.compressPercent);
    return compressed;
},


9、封装文档片段容器中插入节点的交互函数

  • 点击上传按钮后创建图片项目的DOM节点,并插入到文档片段容器中。
  • 修改对应元素的背景图属性,实现了图片的预览功能

//向文档片段容器中插入图片项目的DOM结构
addFragItem(url) {
    //创建DOM结构
    var domFrag = this.createDom(url);
    //插入到文档片段容器中
    this.fragmentWrap.appendChild(domFrag);
},
//点击上传按钮后创建图片项目的DOM片段
createDom(url) {
    var template = '';
    template += '<div class="upload-flexbox-item">' +
        '            <div class="upload-image-picker-item">' +
        '                <div class="upload-image-picker-item-remove"></div>' +
        '                <div class="upload-image-picker-item-content" style="background-image: url(\'' + url + '\');"></div>' +
        '            </div>' +
        '        </div>';
    var frag = document.createRange().createContextualFragment(template);
    return frag
},


10、使用XHR封装公用的请求方法。

  • 同时支持新增、删除操作;
  • 当请求为新增图片且有返回数据时,获取文件名称数组,并将文件名记录在DOM节点上(删除图片时需要传入对应图片的文件名)。
  • 当请求为删除图片时,取得当前点击事件的目标对象所对应的图片项目节点,并删除此节点。

//封装POST请求(可实现增加、刪除图片功能)
sendRequest: function(url, method, data) {
    var self = this;
    if (!url) {
        self.mask.style.display = "none"
        alert("请先设置请求接口!")
        return
    }
    if (!data) {
        data = null
    }

    var xhr = new XMLHttpRequest();
    xhr.open(method, url, true);
    xhr.onreadystatechange = function() {
        if (xhr.readyState == 4) {
            self.mask.style.display = "none"
            if (xhr.status >= 200 && xhr.status < 300 || xhr.status == 304) {
                var result = JSON.parse(xhr.responseText);
                alert(result.msg);

                if (result.data) {
                    //添加图片请求成功,把文档片段一次性插入到DOM中
                    self.uploadFlexbox.insertBefore(self.fragmentWrap, self.btnBelongToFlexItem);

                    //将文件名记录在DOM节点上
                    self.recordFileName(result.data.fileNames)
                } else {
                    //删除图片请求处理成功,删除对应的项目节点
                    var parent = self.targetNodeBelongToItem.parentNode
                    parent.removeChild(self.targetNodeBelongToItem)
                }
            } else {
                alert("请求失败")
            }

        }
    }
    xhr.send(data)
},


11、封装记录文件名称的交互函数。

  • 遍历文件名数组
  • 将文件名插入到对应的项目节点上

//请求成功后,将服务器保存的图片对应的文件名记录在DOM节点上
recordFileName: function(data) {

    //后台返回的文件数组
    var fileNames = data;
    var filesLen = fileNames.length;

    //查询项目节点的集合
    var items = document.querySelectorAll(".upload-flexbox-item");
    var itemsLen = items.length

    //只记录项目集合中本次上传成功的图片对应的节点项目
    var prevLen = itemsLen - filesLen

    //为节点项目标记文件名
    fileNames.forEach(function(item, index) {
        //-1表示排除“+”按钮所在项目
        items.item(prevLen + index - 1).setAttribute("fname", item.fname);
    })
},


12、封装删除按钮的交互函数。

  • 点击删除按钮后,使用递归查找到事件目标对象所对应的项目节点并缓存起来(待删除请求成功后再移除该节点);
  • 获取文件名拼接到请求地址,发送删除图片文件的请求。

//点击按钮删除图片项目
deleteItem(target) {
    var self = this,
        fname,
        toBeDeletedUrl; //用来保存图片的文件名

    if (!self.options.deleteItemApi) {
        alert("请先设置请求接口!")

        return
    }

    // 递归查找事件目标对象所属的项目节点
    var findParent = function(node) {
        var parent = node.parentNode;

        if (parent === self.uploadFlexbox) {

            // 取得被删除项目的图片文件名
            fname = node.getAttribute("fname");
            // 缓存点击(删除)事件目标对象所属的项目节点
            self.targetNodeBelongToItem = node
        } else {
            findParent(parent)
        }
    }
    findParent(target);

    //把文件名拼接到请求地址,删除服务器中对应的图片数据
    toBeDeletedUrl = self.options.deleteItemApi + fname

    //发送删除请求
    self.sendRequest(toBeDeletedUrl, "post")

},


13.在文档初始化之后,创建一个FileUpload实例。

  • 设置压缩等级;
  • 设置新增图片的接口,格式为"http://127.0.0.1:8080/my-project/upload/file"
  • 设置删除图片的接口,格式为"http://127.0.0.1:8080/my-project/delete/file/"

<script>
window.addEventListener("load", function() {
    var fileupload = new FileUpload({
        compressPercent: 0.3, //压缩比例,最大为1
        addItemApi: "http://127.0.0.1:8080/my-project/upload/file", //新增图片的接口
        deleteItemApi: "http://127.0.0.1:8080/my-project/delete/file/" //删除图片的接口
    });
})
</script>

        

        感谢您的阅读!附上Github项目地址,可查看完整代码:

        github.com/yuan569/Htm…