大文件上传是需要前端和后端同时参与才可以实现的。
大文件上传整体思路:文件切片 和 断点续传
input type="file"
绑定一个change
事件,在回调中通过事件对象的e.target.files
拿到这个文件对象,进行文件对象的slice方法,进行切片,一个大文件就转换成多个小文件了。input type="file"
以及他的change 事件拿到当前上传文件的文件对象e.target.files
createFileChunk(file, size = SIZE) {
const fileChunkList = [];
let cur = 0;
while (cur < file.size) {
fileChunkList.push({ file: file.slice(cur, cur + size) });
cur += size;
}
return fileChunkList;
}
注意:循环截取.slice方法不是数组的slice方法,而是文件对象原型上的slice方法,继承自 Blob的slice
以小文件的形式 向后端发请求 Promise.all
upload
断点续传要实现的就是 上传过的切片 不用重复上传,后端一查这个切片上传过了 就是当上传成功了。
断点续传的原理在于 服务端需要记住已上传的切片,这样下次上传就可以跳过之前已上传的部分
file.name + "-" + index
===> 利用 spark-MD5
插件,生成和文件内容县官的 hash 值,只要内容不变,hash 值就不变。根据文件内容生成hash
spark-MD5 插件,可以根据文件内容生成hash值,这个值只和文件内容相关。
FileReader
是一个文件解析器 const reader = new FileReader()readAsArrayBuffer()
读取文件的内容,读取文件完成时,会触发onload事件,文件内容在e.target.result
// 导入脚本
// import script for encrypted computing
// self代表子线程自身,即子线程的全局对象。
Worker 内部如果要加载其他脚本,有一个专门的方法importScripts()。
self.importScripts("/spark-md5.min.js");
// 生成文件 hash
// create file hash
使用self.onmessage指定监听函数的参数是一个事件对象,它的data属性包含主线程发来的数据。
self.onmessage = e => {
const { fileChunkList } = e.data;
// 生成 spark 实例, SparkMD5.ArrayBuffer
const spark = new self.SparkMD5.ArrayBuffer();
let percentage = 0;
let count = 0;
const loadNext = index => {
const reader = new FileReader();// FileReader 文件解析器
reader.readAsArrayBuffer(fileChunkList[index].file);
// reader.readAsArrayBuffer 可以读取文件的内容
// 当文件读取完成的时候 会触发 onload 把这个文件的buffer数据流
reader.onload = e => {
count++;
spark.append(e.target.result);
if (count === fileChunkList.length) {
self.postMessage({
percentage: 100,
hash: spark.end()// 所有的内容读取完整之后 会返回一个hash
});
self.close();
} else {
percentage += 100 / fileChunkList.length;
// self.postMessage()方法用来向主线程发送消息。
self.postMessage({
percentage
});
loadNext(count);
}
};
};
loadNext(0);
};
使用spark-MD5为切片文件添加唯一标识时,因为该插件会先读取文件内容,才能生成标识,而读取内容是异步操作,会造成线程阻塞,影响后面UI交互,后面的任务只能等待着
因此在项目中采用了webWorker来解决该问题,首先创建一个子线程与主线程进行交互,将读取文件内容生成hash标识的操作交给woker子线程,最终在将结果返回主线程,主线程就可以正常交互,不会被阻塞或拖慢。
项目中封装了一个 calculateHash方法,这个方法内部创建一个 new worker开启一个子线程并传入了这个Worker 子线线程所要执行的任务
这个方法接收一个fileChunkList参数,代表切片的文件对象
通过返回一个promise对象,在promise内部去new Woker("hash.js")
通过worker.postMessage({fileChunkList})将通过文件对象.slice划分的切片文件传递给子线程
子线程内部通过self.onmessage去接收主线程传递的数据,最终这个切片文件对象会出现在self.onmessage回调函数的形参的e.data属性中,其中self代表子线程的全局对象,也就是子线程自身
因为要使用 spark-MD5 插件,根据内容生成hash标识,所以要先在子线程js文件中通过self.importScripts()加载我们需要的 spark-MD5 插件,这样当子线程接收到切片对象之后,就
可以 new self.SparkMD5.ArrayBuffer() 生成spark实例
然后在内部封装了一个loadNext方法,该方法内部会去new FileReader(),创建文件解析器
接着调用reader.readAsArrayBuffer(fileChunkList[index].file) index==>count
reader.readAsArrayBuffer 可以读取文件的内容 ,并转换成buffer数据流,当文件读取完成的时候,会触发 onload事件,刚刚读取文件的buffer数据流就存在e.target.result属性中
然后通过 spark.append 通过spark实例调用append(e.target.result)传入对应的buffer数据流,
接下来判断是否等于要读取文件的长度,如果等于,代表内容读取完毕
就直接通过self.postMessage({hash:spark.end()}) 所有的内容读取完整之后 通过 spark的 end()方法, 生产对应的hash值,并发送给主线程,并调用self.close()关闭worker节省系统资源
如果没有读取完毕,则继续调用loadNext(count)方法,传入count,这个count是自增之后的值,以次类推,直到读取完毕
最后主线程中定义了一个 worker.onmessage监听函数去监听子线程返回的值,也就是hash标识,同样也会出现在e.data属性中,最终通过resolve(hash)即可
hash.js
// 导入脚本
// import script for encrypted computing
self.importScripts("/spark-md5.min.js");
// 生成文件 hash
// create file hash
// new worker
self.onmessage = e => {
const { fileChunkList } = e.data;
const spark = new self.SparkMD5.ArrayBuffer();
let percentage = 0;
let count = 0;
const loadNext = index => {
const reader = new FileReader();
reader.readAsArrayBuffer(fileChunkList[index].file);
reader.onload = e => {
count++;
spark.append(e.target.result);
if (count === fileChunkList.length) {
self.postMessage({
percentage: 100,
hash: spark.end()
});
self.close();
} else {
percentage += 100 / fileChunkList.length;
self.postMessage({
percentage
});
loadNext(count);
}
};
};
loadNext(0);
};
// 生成文件 hash(web-worker)
// use web-worker to calculate hash
calculateHash(fileChunkList) {
return new Promise(resolve => {
this.container.worker = new Worker("/hash.js");
this.container.worker.postMessage({ fileChunkList });
this.container.worker.onmessage = e => {
const { percentage, hash } = e.data;
this.hashPercentage = percentage;
if (hash) {
resolve(hash);
}
};
});
}
FileReader.readAsArrayBuffer() - Web API 接口参考 | MDNhttps://developer.mozilla.org/zh-CN/docs/Web/API/FileReader/readAsArrayBuffer
Web Worker 使用教程 - 阮一峰的网络日志https://www.ruanyifeng.com/blog/2018/07/web-worker.html
FileReader - Web API 接口参考 | MDNhttps://developer.mozilla.org/zh-CN/docs/Web/API/FileReader