vue本地计算md5与分片上传的重点

我们在使用 vue 做上传文件的功能的时候,有一种比较复杂的需求场景是这样的:

  • 客户端本地需要要先计算文件的 md5 值,传递给后端进行比对,如果已经存在,则极速完成上传
  • 文件需要分片传输,而且需要控制并发量
  • 需要支持文件夹上传

下面我们就上面几个需求需要关注的重点说明一下:

  1. 对于本地计算 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: {
// 计算文件的md5值
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++;

// 这里可以加一些实时更新md5百分比进度的逻辑

if (currentChunk < chunks) {
loadNext();
} else {
console.log("finished loading");
resolve(spark.end());
}
};

fileReader.onerror = function () {
reject("计算失败");
console.warn("md5计算失败");
};

loadNext();
});
},
},
};
  1. 并发上传

文件需要并发上传,我们要清楚一个浏览器的重要知识点,就是一个浏览器的 TCP 并发连接数是有限制的,一般是 6 个,这样我们的并发就千万不要超过这个值,具体可以在浏览器的 network 请求列表中,查看右侧的时间线,如果有是在“开始连接”中有很大的时间占用,就说明这个并发数太大了,没有控制好。

当我们一次性选择多个文件时,当开始上传时是同时并发上传吗?当然不是的,因为如果全部文件同时上传,而每个文件又要分片上传处理,这样就达不到控制上传并发的目的,这样我们一般的做法是一次只处理一个文件,每个文件再去控制并发数来达到控制整体并发的目的。

  1. 文件夹与文件

对于上传文件夹,在 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();
// 重新对待选文件列表fileList赋值,不分文件夹内的文件还是一级文件
await that.getFileEntry(entry, fileList);
}
};

// 这里要等待所有的异步都处理完后,才能下一步,不能单步await
const promises = [];
for (let i = 0; i < items.length; i++) {
promises.push(processItem(items[i]));
}
await Promise.all(promises);

// 这里可以对fileList之后的逻辑进行进一步的处理
});
});
});
},

methods: {
// 获取文件条目
async getFileEntry(entry, fileList) {
if (entry.isFile) {
return new Promise((resolve, reject) => {
entry.file(
(file) => {
// 比要求的限制多处理1个,避免过多无效地处理逻辑
if (fileList.length <= this.fileLimit) {
let path = entry.fullPath.substring(1);
// 由于子文件的webkitRelativePath为空,并且File只读,这里构造新File来赋值相对路径
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 ,然后整体赋值其相对路径,让后端能够正常处理。