js系列-大文件上传

510 阅读4分钟

大文件上传的背景:

在某些业务中,大文件上传是一个比较重要的交互场景,如上传入库比较大的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…