在平时的业务需求开发中,几乎每个项目都会用到上传图片的功能,鉴于移动端符合实际业务需要的上传图片开源插件较少,所以自己动手写了一个。现在把这次开发经验分享出来,与大家一起交流,另外插件也可以直接被应用到实际项目的开发中,欢迎大家去下载使用。
目前智能手机的像素越来越高,受网络条件限制,移动端上传图片的体验会大打折扣,另外也会增加用户的流量成本,所以在上传之前先压缩势在必行。本插件主要实现了本地图片预览与删除、图片压缩及上传的功能,使用原生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对象加载成功之后(注意图片加载成功的顺序可能与用户选取图片时的顺序不一致):
- 为FormData对象设置键值,使用canvas重绘图像、压缩,并且向文档片段容器中插入DOM节点;
- 利用计数器记录单次上传已加载完成的图片数量,当图片全部加载完成时,把文档片段容器一次性插入到DOM中,同时向服务器发送新增请求;
- 释放引用的对象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项目地址,可查看完整代码: