大文件上传
- Web Worker 使用教程 https://www.ruanyifeng.com/blog/2018/07/web-worker.html
- Vite 中使用 WebWorker https://kuizuo.cn/vite-webworker/
- vue 中使用 web worker https://www.jianshu.com/p/b00ef894c507
- 前端超大大文件上传实现以及优化 https://juejin.cn/post/6986188684605259783#heading-12
需求分析
针对大文件上传,最少做到以下几点
- 大文件切割,分片上传
- 如果有部分切片上传失败了,我们希望提醒用户重新上传,并且上传成功不需要上传
- 最好能有上传的进度提示
什么是 web worker
Web Worker 的作用,就是为 JavaScript 创造多线程环境,允许主线程创建 Worker 线程,将一些任务分配给后者运行。在主线程运行的同时,Worker 线程在后台运行,两者互不干扰。等到 Worker 线程完成计算任务,再把结果返回给主线程。这样的好处是,一些计算密集型或高延迟的任务,被 Worker 线程负担了,主线程(通常负责 UI 交互)就会很流畅,不会被阻塞或拖慢。
主线程采用new命令,调用Worker()构造函数,新建一个 Worker 线程。
var worker = new Worker('work.js');
var worker = new Worker('work.js');
Worker 线程有一些自己的全局属性和方法。
self.name: Worker 的名字。该属性只读,由构造函数指定。 self.onmessage:指定message事件的监听函数。 self.onmessageerror:指定 messageerror 事件的监听函数。发送的数据无法序列化成字符串时,会触发这个事件。 self.close():关闭 Worker 线程。 self.postMessage():向产生这个 Worker 线程发送消息。 self.importScripts():加载 JS 脚本。
Worker 内部如果要加载其他脚本,有一个专门的方法importScripts()。 该方法可以同时加载多个脚本
importScripts('script1.js');
importScripts('script1.js', 'script2.js');
importScripts('script1.js');
importScripts('script1.js', 'script2.js');
进度条功能
上传进度分两种,一个是每个切片的上传进度,另一个是整个文件的上传进度,而整个文件的上传进度是基于每个切片上传进度计算而来,所以我们先实现切片的上传进度
切片进度条
XMLHttpRequest 原生支持上传进度的监听,只需要监听 upload.onprogress 即可,我们在原来的 request 基础上传入 onProgress 参数,给 XMLHttpRequest 注册监听事件
每一个切片都需要对应一个上传的进度,这个时候应该写一个方法针对切片对象进行进度条的除了,
// item是我们的chunk对象
createProgressHandler(item) {
return (e) => {
item.percentage = parseInt(String((e.loaded / e.total) * 100));
};
},
// item是我们的chunk对象
createProgressHandler(item) {
return (e) => {
item.percentage = parseInt(String((e.loaded / e.total) * 100));
};
},
文件的进度条
将每个切片已上传的部分累加,除以整个文件的大小,就能得出当前文件的上传进度,所以这里使用 Vue 计算属性
computed: {
// 针对每一个 chunk的进度 计算出总的上传进度
uploadPercentage() {
if (!this.file || !this.chunkList.length) return 0;
const loaded = this.chunkList.map((item) => item.size * item.percentage).reduce((acc, cur) => acc + cur);
return parseInt((loaded / this.file.size).toFixed(2));
},
},
复制代码
computed: {
// 针对每一个 chunk的进度 计算出总的上传进度
uploadPercentage() {
if (!this.file || !this.chunkList.length) return 0;
const loaded = this.chunkList.map((item) => item.size * item.percentage).reduce((acc, cur) => acc + cur);
return parseInt((loaded / this.file.size).toFixed(2));
},
},
复制代码
大文件上传的基本功能就差不多完成了。
文件秒传
这个功能的意思就是说,我们在文件上传之前,去问一下服务器,你有没有这个文件呀,你没有的话我就开始上传,你要是有的话我就偷个懒,用你有的我就不上传了
所以需要实现一个检测接口(verify),去询问服务器有没有这个文件,因为我们之前是计算过文件的 hash的,能保证文件的唯一性。就用这个hash就能唯一的判断这个文件。所以这个接口的思路也很简单,就是判断我们的 target
目录下是否存在这个文件
断点续传
断点续传的意思就是我们上传的时候如果文件上传失败了,我们之后在上传一次的时候,只上传我们之前失败的文件,成功的文件我们就跳过
文件切片
选择文件是使用的input
输入框,获取到选择的值也很简单。文件切片的核心就是文件对象的slice
方法,类似数组,我们可以调用这个方法获取到文件的某一段
文件唯一值
如何告诉后端,上传的两个文件是不是同一个文件,显然,如果使用文件名作为唯一标识肯定不太好。这个时候我们想到可以使用md5
对文件加密获取唯一的hash
值
生成hash值的方法我们是调用 spark-md5
这个库,在计算hash
的时候是非常消耗计算机的CPU
的会造成浏览器的卡顿,为了优化体验我们使用 web-worker
在 worker
线程计算 hash
在 worker
线程中,接受文件切片 fileChunkList
,利用 FileReader
读取每个切片的 ArrayBuffer
并不断传入 spark-md5
中,每计算完一个切片通过 postMessage
向主线程发送一个进度事件,全部完成后将最终的 hash
发送给主线程
暂停上传
我们先自己手动实现一个按钮,点击之后就停止当前的上传情况。模拟了上传失败
这个思路肯定就是要改装我们的 request
方法,在改装之前我们需要知道 XMLHttpRequest
对象是可以自己主动停止当前的网络连接的,不知道的同学补补课。
这样我们只需要使用一个公共的数组,每一次发请求的时候都保存我们当前的这个XMLHttpRequest
对象,当请求成功之后,我们就移除这个对象,当点击暂停按钮的时候我们就遍历这个数组调用每一个XMLHttpRequest
的abrot
方法就可以取消上传了。