大文件上传的背景:
在某些业务中,大文件上传是一个比较重要的交互场景,如上传入库比较大的Excel表格数据、上传影音文件等。如果文件体积比较大,或者网络条件不好时,上传的时间会比较长(要传输更多的报文,丢包重传的概率也更大),用户不能刷新页面,只能耐心等待请求完成。
文件上传的4种方式:
-
formData异步上传(我常用的)
-
普通表单上传
-
文件编码上传
-
iframe无刷新页面
formData异步上传:
FormData对象主要用来组装一组用 XMLHttpRequest发送请求的键/值对,可以更加灵活地发送Ajax请求。可以使用FormData来模拟表单提交。
let files = e.target.files // 获取input的file对象
let formData = new FormData();
formData.append('file', file);
axios.post(url, formData);
普通表单上传:
使用PHP来展示常规的表单上传是一个不错的选择。首先构建文件上传的表单,并指定表单的提交内容类型为enctype="multipart/form-data",表明表单需要上传二进制数据。
<form action="/index.php" method="POST" enctype="multipart/form-data">
<input type="file" name="myfile">
<input type="submit">
</form>
文件编码上传:
其主要实现原理就是将图片转换成base64进行传递
var imgURL = URL.createObjectURL(file);
ctx.drawImage(imgURL, 0, 0);
// 获取图片的编码,然后将图片当做是一个很长的字符串进行传递
var data = canvas.toDataURL("image/jpeg", 0.5);
iframe无刷新页面:
如果需要让用户体验异步上传文件的感觉,可以通过framename指定iframe来实现。把form的target属性设置为一个看不见的iframe,那么返回的数据就会被这个iframe接受,因此只有该iframe会被刷新,至于返回结果,也可以通过解析这个iframe内的文本来获取。
大文件上传遇到的问题:
通过fromData,其实际也是在xhr中封装一组请求参数,用来模拟表单请求,无法避免大文件上传超时的问题
大文件上传需要实现的需求:
-
支持拆分上传请求(即切片)
-
支持断点续传
-
支持显示上传进度和暂停上传
文件切片的实现过程:
-
在js中,文件的file对象是Blob对象的子类,我们可以调用Blob对象的slice方法对二进制文件进行拆分。
-
我们使用的是fromData进行文件上传,需要设置两个参数。content,chunkIndex。content的组成是文件名+uid之类的组成一个唯一标识,可以根据这个判断是否重复上传了文件片段。chunkIndex 是顺序,我们上传是异步上传,所以有快有慢。我们可以根据这个index在上传全部结束后进行组装
-
mkfile接口,这个接口是我们上传了全部片段之后需要调这个接口,跟后端说接口已经调完了
相关代码:
// 拆分的方法function slice(file, piece = 1024 * 1024 * 5) {
let totalSize = file.size; // 文件总大小
let start = 0; // 每次上传的开始字节
let end = start + piece; // 每次上传的结尾字节
let chunks = []
while (start < totalSize) {
// 根据长度截取每次需要上传的数据
// File对象继承自Blob对象,因此包含slice方法
let blob = file.slice(start, end);
chunks.push(blob)
start = end;
end = start + piece;
}
return chunks
}
// 举个栗子
// 获取context,同一个文件会返回相同的值
function createContext(file) {
return file.name + file.length
}
let file = document.querySelector("[name=file]").files[0];
const LENGTH = 1024 * 1024 * 0.1;
let chunks = slice(file, LENGTH);
// 获取对于同一个文件,获取其的context
let context = createContext(file);
let tasks = [];
chunks.forEach((chunk, index) => {
let fd = new FormData();
fd.append("file", chunk);
// 传递context
fd.append("context", context);
// 传递切片索引值
fd.append("chunk", index + 1);
tasks.push(post("/mkblk.php", fd));
});
// 所有切片上传完毕后,调用mkfile接口
Promise.all(tasks).then(res => {
let fd = new FormData();
fd.append("context", context);
fd.append("chunks", chunks.length);
post("/mkfile.php", fd).then(res => {
console.log(res);
});
});
断点续传:
因我们上传的过程中可能会出现断网或者关机等外在因素,所以系统再次可以使用的时候又会重新上传。这个时候我们可以使用断点续传,断点续传是可以从已经上传部分开始继续上传未完成的部分,而没有必要从头开始上传,节省上传时间。
断点续传的实现逻辑:
-
在切片上传成功后,保存已上传的切片信息
-
当下次传输相同文件时,遍历切片列表,只选择未上传的切片进行上传
-
所有切片上传完毕后,再调用mkfile接口通知服务端进行文件合并
断点续传的实现方法:
-
可以使用localStorage将已经上传的切片信息保存在本地浏览器,然后在下次上传之前去进行比较。不过缺点是用户可能手动删除浏览器的缓存。
-
后端有个接口是专门存已经上传了的切片信息,我们可以调这个接口来知道已经上传了哪些,然后在上传没上传的文件片段
有个bug,在使用断点续传的时候,本地已经上传的部分文件出现了修改怎么办?
-
想解决的问题是怎么判断当前文件是否跟原来的文件是一样的
-
解决问题的思路(道听途说,待实践):可以将文件转为base64码,然后进行存储。在断点续传的时候,可以进行比较。可参考此博客:blog.csdn.net/hero_lxz/ar…