我们在使用 vue 做上传文件的功能的时候,有一种比较复杂的需求场景是这样的:
- 客户端本地需要要先计算文件的
md5
值,传递给后端进行比对,如果已经存在,则极速完成上传
- 文件需要分片传输,而且需要控制并发量
- 需要支持文件夹上传
下面我们就上面几个需求需要关注的重点说明一下:
- 对于本地计算 md5,可以安装库
spark-md5
,然后引入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
| import SparkMD5 from "spark-md5"; export default { methods: { getMd5(file, relativePath) { return new Promise((resolve, reject) => { let blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice, chunkSize = 1024 * 1024 * 100, chunks = Math.ceil(file.size / chunkSize), currentChunk = 0, spark = new SparkMD5.ArrayBuffer(), fileReader = new FileReader();
const loadNext = () => { let start = currentChunk * chunkSize, end = start + chunkSize >= file.size ? file.size : start + chunkSize;
fileReader.readAsArrayBuffer( blobSlice.call(file, start, end) ); };
fileReader.onload = function (e) { spark.append(e.target.result); currentChunk++;
if (currentChunk < chunks) { loadNext(); } else { console.log("finished loading"); resolve(spark.end()); } };
fileReader.onerror = function () { reject("计算失败"); console.warn("md5计算失败"); };
loadNext(); }); }, }, };
|
- 并发上传
文件需要并发上传,我们要清楚一个浏览器的重要知识点,就是一个浏览器的 TCP
并发连接数是有限制的,一般是 6
个,这样我们的并发就千万不要超过这个值,具体可以在浏览器的 network
请求列表中,查看右侧的时间线,如果有是在“开始连接”中有很大的时间占用,就说明这个并发数太大了,没有控制好。
当我们一次性选择多个文件时,当开始上传时是同时并发上传吗?当然不是的,因为如果全部文件同时上传,而每个文件又要分片上传处理,这样就达不到控制上传并发的目的,这样我们一般的做法是一次只处理一个文件,每个文件再去控制并发数来达到控制整体并发的目的。
- 文件夹与文件
对于上传文件夹,在 html
中,我们给触发元素添加 webkitdirectory
属性,需要注意的是,一般个按钮不能同时实现上传文件夹和上传文件的功能,一般做法是二选一,用不同的按钮来实现不同的目的,在有拖拽的场景时,可以通过监听拖拽事件,来实现同时处理文件和文件夹的目的,不过还是需要自已打标识
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93
| export default { mounted() { this.getRemainFileCount(); this.$nextTick(() => { let that = this; document .querySelector(".el-upload-dragger") .addEventListener("drop", (e) => { let items = e.dataTransfer.items; this.$nextTick(async () => { const fileList = [];
const processItem = async (item) => { if (item.kind === "file") { let entry = item.webkitGetAsEntry(); await that.getFileEntry(entry, fileList); } };
const promises = []; for (let i = 0; i < items.length; i++) { promises.push(processItem(items[i])); } await Promise.all(promises);
}); }); }); },
methods: { async getFileEntry(entry, fileList) { if (entry.isFile) { return new Promise((resolve, reject) => { entry.file( (file) => { if (fileList.length <= this.fileLimit) { let path = entry.fullPath.substring(1); let newFile = new File([file], file.name, { type: file.type, }); Object.defineProperty( newFile, "webkitRelativePath", { writable: false, value: path.indexOf("/") > -1 ? path : "", } ); const fileItem = { name: path, percentage: 0, raw: newFile, size: newFile.size, status: "ready", uid: newFile.uid, }; fileList.push(fileItem); } resolve(); }, (e) => { console.log(e); reject(e); } ); }); } else { let reader = entry.createReader();
let entries = await new Promise((resolve, reject) => { reader.readEntries(resolve, reject); });
let promises = entries.map((entry) => this.getFileEntry(entry, fileList) ); await Promise.all(promises); }
return true; }, }, };
|
对于文件夹的整体处理,需要关注属性 webkitRelativePath
,然后整体赋值其相对路径,让后端能够正常处理。