大文件上传如何做断点续传 - 前端篇

推荐先去看 后端篇

核心思想

  1. 文件分割
  2. 分割后并发请求
  3. 恢复传输后再将原文件分片,根据当前已发送的分片,跳过

代码实例

  • 启动服务后,需要在不刷新页面的情况下进行。如果需要刷新页面,那需要 IndexedDB 的介入
  • 可以拿 1 ~ 2M 的图片测试
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
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script
src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js"
integrity="sha512-E8QSvWZ0eCLGk4km3hxSsNmGWbLtSCSUcewDQPQWZF6pEU8GlT8a5fF32wOl1i8ftdMhssTrF/OhyGWwonTcXA=="
crossorigin="anonymous"
referrerpolicy="no-referrer"
></script>
<script
src="https://cdnjs.cloudflare.com/ajax/libs/axios/1.3.6/axios.min.js"
integrity="sha512-06NZg89vaTNvnFgFTqi/dJKFadQ6FIglD6Yg1HHWAUtVFFoXli9BZL4q4EO1UTKpOfCfW5ws2Z6gw49Swsilsg=="
crossorigin="anonymous"
referrerpolicy="no-referrer"
></script>
<title>Document</title>
</head>
<body>
<div>
<input type="file" /> <button onclick="bf.pause()">暂停上传</button
><button onclick="bf.goOn('487f7b22f68312d2c1bbc93b1aea445b')">
继续上传
</button>
<p>已经上传 <span></span> <span id="status"></span></p>

<img src="" alt="" />
</div>
<script>
class BigFiles {
inputRef = document.querySelector("input[type=file]");
statusRef = document.querySelector("#status");
progressRef = document.querySelector("span");
imgRef = document.querySelector("img");
source = axios.CancelToken.source();
// 请求队列
queue = [];
// 分片大小 (10k)
splitSize = 10 * 1024;
// 源文件
rawFiles = new Map();
// 已上传的分片
uploaded = {};

constructor() {
// 监听上传动作
this.inputRef.addEventListener("change", (e) => {
const file = e.target.files[0];
const md5code = CryptoJS.MD5(file).toString();

this.rawFiles.set(md5code, file);
this.#_uploadQueue(md5code, file);
});
}
// 暂停上传
pause = () => {
this.source.cancel();
this.statusRef.innerText = "(已暂停)";
};
// 继续上传
goOn = (md5code) => {
axios({
url: "/check_uploaded",
method: "get",
params: {
md5code,
},
}).then((res) => {
if (res.data.code !== 200) {
return;
}
const goOnFile = this.rawFiles.get(md5code);

if (goOnFile) {
// 下面是页面刷新逻辑,如果刷新还需要用IndexedDB,存储文件input选中的文件,和已上传的文件名
if (!this.queue.length) {
}
// 剔除状态没有完成的请求,再通过 continue push, 保持原分片份数长度,确保进度显示正确
this.queue = this.queue.filter((i) => i.status === "done");
/*
Q: 为什么不取后端存在的文件名?
A: 发起中断的权力在客户端,一旦取消请求,服务端那边写出的二进制文件是未传输完成的,合并出的文件会有问题!
*/
this.uploaded[md5code] = doneList.map((d) => d.filehash); // 最好存入 localStorage 中
// 不能用已取消的 abort 对象,需要重新设置
this.source = axios.CancelToken.source();
this.#_uploadQueue(md5code, goOnFile);
this.statusRef.innerText = "";
}
});
};

#_uploadQueue = (md5code, file) => {
// file.size的数字是byte就是字节为单位 size: 1 就是 1字节
let cur = 0; // 从0开始
while (cur < file.size) {
// 下次分片的开始下表的大小
const nextCur = cur + this.splitSize;
// 后端存储的分片名
const filehash = `${md5code}_${nextCur}`;
// 如果已上传就跳过
if (
this.uploaded[md5code] &&
this.uploaded[md5code].includes(filehash)
) {
cur = nextCur;
continue;
}
const formData = new FormData();
// 分割文件,可以把文件的分割,看作数组的分割
formData.append("file", file.slice(cur, (cur = nextCur)));
formData.append("hash", filehash);
const item = {
status: "",
filehash,
request: axios({
url: "/big_files",
method: "post",
data: formData,
cancelToken: this.source.token,
}).then((e) => {
item.status = "done";
const doneList = this.queue.filter((i) => i.status === "done");
// 仅做展示用,不需要精确计算
this.progressRef.innerText = `${
parseInt(doneList.length / this.queue.length) * 100
}%`;
return e;
}),
};
// 加入队列
this.queue.push(item);
}
// 并发队列,只会触发状态为 pending 的 promise
Promise.all(this.queue.map((q) => q.request)).then(() =>
this.mergeFiles(md5code)
);
};

mergeFiles = (md5code) => {
// 合并文件请求
axios({
url: "/big_files_merge",
method: "post",
data: {
hashname: md5code,
filename: this.rawFiles.get(md5code).name,
},
responseType: "blob",
onDownloadProgress: (progressEvent) => {
const complete = `${
parseInt(progressEvent.loaded / progressEvent.total) * 100
}%`;
this.statusRef.innerText = `正在合并图像, 请稍后...(${complete})`;
},
}).then((res) => {
// 显示到页面上
const URL = window.URL || window.webkitURL;
const href = URL.createObjectURL(res.data);
this.imgRef.src = href;
this.statusRef.innerText = "";
this.queue = [];
});
};
}

const bf = new BigFiles();
</script>
</body>
</html>
作者

Huasun47

发布于

2020-04-23

更新于

2020-04-23

许可协议