大文件上传

  • 介绍常见的文件上传方式
  • 大文件上传的通病:容易超时。介绍文件分片断点续传方法。

文件上传

编码后上传

图片转base64上传

前端将需要上传的图片进行base64编码,然后提交到服务端。

var imgURL = URL.createObjectURL(file);
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
ctx.drawImage(imgURL, 0, 0);
// 获取图片的编码,然后将图片当做是一个很长的字符串进行传递
var data = canvas.toDataURL("image/jpeg", 0.5);
复制代码

服务端接收到文件后,进行base64解码,然后进行保存。

一般只在图片比较小的时候建议用base64编码,原因是编码后文本体积会比原图片更大,它将三个字节转化成四个字节。因此对于体积较大的文件来说,上传和解析的时间会增加。

读取文件转二进制格式上传

前端直接读取文件内容后以二进制格式上传

// 读取二进制文件
function readBinary(text){
   var data = new ArrayBuffer(text.length);
   var ui8a = new Uint8Array(data, 0);
   for (var i = 0; i < text.length; i++){ 
     ui8a[i] = (text.charCodeAt(i) & 0xff);
   }
   console.log(ui8a)
}

var reader = new FileReader();
reader.onload = function(){
	  readBinary(this.result) // 读取result或直接上传
}

// 把从input里读取的文件内容,放到fileReader的result字段里
reader.readAsBinaryString(file);
复制代码

form表单上传

使用form标签,并指定标签的enctype="multipart/form-data",表明表单需要上传二进制数据,并设置method="POST",。

<form action="http://localhost:8080" method="POST" enctype="multipart/form-data">
  <input type="file" name="file1">
  <input type="submit">
</form>
复制代码

使用type=submit上传文件,体验上的缺点很明显,上传完毕会刷新页面,导致页面数据和状态丢失。
早期会把表单使用iframe内嵌到页面里,提交完只刷新iframe,实现局部刷新的效果。

通过xhr,前端也可以进行异步上传文件的操作,实现无刷新上传。

FormData上传

使用FormData对象管理表单数据,再将表单数据进行异步提交。

let files = e.target.files // 获取input的file对象
let formData = new FormData(); // 构造FormData对象
formData.append('file', file);
axios.post(url, formData);
复制代码

大文件上传

以上的传输方式在文件不大时运行良好,但是在文件很大的情况下,比如一个视频文件几百上千兆,就会出现问题:

在同一个请求中,上传的数据量太大,容易导致链接超时失效,或是超过服务器可接收最大字段。而上传失败后整个文件需要重传。

文件分片 解决大文件上传问题,关键技术就是将大的文件分割成一个个小的文件,并行上传。

还原分片 接收端在接收完所有分片文件后,将一个个小文件按照顺序拼接还原。

断点续传 在传输过程中意外中断时,我们并不希望将所有文件都进行重传,而是只把上传失败的部分进行重传。

文件分片

Blob对象:表示一个不可变、原始数据的类文件对象。它包含一个重要的方法slice(),通过这个方法,我们就可以对二进制文件进行拆分。

File对象:File对象是特殊类型的Blob,它继承blob接口的方法。

文件分片关键步骤:

  • 前端将大文件进行分片,分割成一个个小文件
  • 前端并行发送分片文件
  • 服务端接收分片文件
  • 分片文件传送完毕后,前端发送一个标志结束的请求
  • 服务端接收到结束标志后,把分片文件进行合并
  • 合并完成后,删除分片文件

分片代码示例:

function sliceInPieces(file, size = 2 * 1024 * 1024) {
  const totalSize = file.size; // 文件总大小
  const chunks = [];
  let start = 0;
  let end = 0;

  while (start < totalSize) {
    end = start + size;
    
    const blob = file.slice(start, end); // 调用对象上的slice方法
    chunks.push(blob);

    start = end;
  }
  return chunks;
}
复制代码

上传分片代码示例:(如果分片比较多,并发请求的数量需要控制一下)

const file =  document.querySelector("[name=file]").files[0]; // 读取文件

const chunks = sliceInPieces(file); // 分片

const context = uuid();  // 文件唯一标识

const promiseList = [];

chunks.forEach((chunk, index) => {
  let fd = new FormData();
  fd.append("file", chunk);
  fd.append("context", context); // 带上标识
  fd.append("index", index); // 带上位置序号
  promiseList.push(axios.post(url, fd)); 
})


Promise.all(promiseList).then((res) => {
  ...
  // 全部上传完毕后通知接收端结束
	let fd = new FormData();
  fd.append("context", context);
  fd.append("chunks", chunks.length);
  axios.post(doneUrl, fd).then(res => {
    ...
  });
})
复制代码

还原分片

接收端处理分片需要注意的问题:

  • 如何识别分片文件来自同一个源文件?
  • 如何将多个分片还原成一个文件?

区分来自同一个源文件
对源文件生成一个文件唯一标识context参数,标志文件分片来自同一个源文件。在每个切片请求上把context参数带上,通知结束的接口也带上这个标记值, 接收端根据这个标记值确认接收到的分片属于同一个文件。

这个context值是文件的唯一标识,下面例举几种生成方式:

  1. 用文件名等作为标识,但是为了避免不同用户取了相同的文件名,可以再拼接上用户信息,比如uid保证唯一性。
  2. 用md5生成文件hash作为文件的唯一标识。
触发还原分片

在所有分片上传完毕后,还会额外再发送一个请求通知接收端进行拼接。
接收端根据请求中的context值,找到所有带此context标志的分片,确认分片的顺序(可以通过分片请求中加的index参数,有些也会直接拼接在context后面,接收端进行处理),根据顺序合并分片文件。

断点续传

上面我们已经了解大文件上传的方法,大文件进行分片并上传,接收端再合并还原成大文件。但在等待分片上传的过程中,我们仍然很有可能发生一些意外的情况导致部分分片上传失败,如断网或者页面关闭/刷新等。由于分片没有全部上传成功,因此无法通知接收端进行文件还原。如果再次全部重新上传,已上传成功的分片就浪费了。因此我们可以通过断点续传来进行处理。

断点续传:只对上传失败的分片进行重传。关键点是客户端如何感知哪些分片已上传成功。
前端触发重传时,根据以上传成功的分片进行筛选,只对未成功的分片进行上传。全部上传完毕后再进行合并的额外请求。

那前端如何感知已上传的分片的信息呢?

  • 客户端记录,通过locaStorage等方式保存在客户端上。
    • 优点:方便实现,且不依赖服务端。缺点:存在客户端上不保险,用户清除缓存记录就会丢失
  • 服务端记录,服务端额外提供一个查询接口给前端调用。
    • 优点:由服务端根据已接受到的分片给客户端开一个已上传的记录接口,客户端重传前进行调用。记录不容易丢失。缺点:额外的接口开销。

分片过期:在分片的步骤中,最后一步是合并完成后删除分片。如果客户端一直不调用通知上传完成的接口,这些分片就一直会保存在磁盘中,这显然是不合理的。因此,分片还需要带上有效期,超期需要被清理掉。在断点续传时,也同样需要考虑到过期的问题。

最后

微信搜索公众号Eval Studio,关注更多动态。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享