关于大文件分块上传的背景,这里推荐一篇文章。没想到是字节跳动的前端面试题。
字节跳动面试官:请你实现一个大文件上传和断点续传mp.weixin.qq.com
这篇文章对大文件分块上传整体的实现思路描述得比较全面,然而在实际的业务开发中还会遇到一些细节问题。
比如:文件分块后,直接用
Promise.all
发起所有上传请求是否会有性能问题?
由于 HTTP1.x 不支持多路复用,所以浏览器每发起一个 HTTP请求就会打开一个 TCP 会话。对于大多数浏览器,每个主机(域名)最多同时支持
6个 TCP 连接。也就意味着客户端每次最多只能并发
6个请求。当请求数超过限制后,后续的请求必须得排队等候。有些面试也会问怎么优化请求资源加载,常见的做法就是增加新的域名分区。
目前,市面上大部分还是以 HTTP1.x 为主,使用 Promise.all 并发所有请求,是会阻塞后续的请求,假设此时用户在网页有其他的请求交互,那么界面就会有一直处于 loading 状态直到原先的排队请求都得到响应。 (提示正在等待可用的套接字...)
我们既想同时上传多个小块文件,又不能越过并行6个请求的限制,就需要进行队列管理。
假设,我们每次并发 4 个上传文件的请求,这样能确保还剩 2 个连接数以支持用户在页面进行其他的交互。
这4个请求里,任意一个请求得到响应之后,马上发起下一个请求,以充分利用请求的资源。
我们再完善下业务场景,此时若一文件体积为 50M,我们以 5M 为一块,那么则需要上传 10 个文件块。
ATM(异步任务管理队列)
我们来通过引入一个 ATM 来帮助我们管理这些事情。
x-atmgithub.com
yarn add x-atm
// 设定最大并行数为 4
const ATM from 'x-atm'
const atm = new ATM({maxParallel: 4})
// 假设来源是拖拽上传
const file = event.originalEvent.dataTransfer.files[0]
// 设定上传路径
const uploadUrl = 'https://xxxx'
// 上传特定的文件块
function singleTask(start, end) {
return function() {
const xhr = new XMLHttpRequest();
xhr.open('POST', uploadUrl);
xhr.send(file.slice(start, end));
return new Promise((resolve, reject) => {
xhr.onreadystatechange = function() {
if (xhr.readyState !== 4) {
return;
}
if (xhr.status >= 200 && xhr.status < 400) {
resolve(xhr.statusText);
} else if (xhr.status >= 400) {
reject(xhr.status);
} else if (xhr.status === 0) {
reject(xhr.status);
}
};
});
}
}
const FIVE_M = 5242880; // 5M
for(let i = 0; i < 10; i++) {
let tempStart = start;
let tempEnd = end;
// 上传 [0, 5M], [5M, 10M], ... [45M, 50M]
const asyncTask = singleTask(FIVE_M * i, FIVE_M * (i + 1))
atm.push(asyncTask)
}
atm.start() // 执行异步任务
这样,我们就能满足我们管理任务队列的基本需求。
为便于大家理解,这里提供一个 demo。
async task manager demo - CodeSandboxcodesandbox.io
失败重传
若其中的某个文件块由于一些原因上传失败了,我们需要重新上传该文件块。
那么,我们可以启动
严格模式
,以确保异步任务完成
const atm = new ATM({maxParallel: 4, strict: true, maxRetry: 3})
strict
表示,我们必须要让每个异步的任务被
resolve
才算这个任务执行成功;
maxRetry
表示,假设这个异步任务执行失败了,它可以尝试再次执行的次数。
查询进度
也许我们想在每个任务完成后,查询大文件上传进度。我们可以在这个任务后面添加 resolve 回调。
const asyncTask = singleTask(FIVE_M * i, FIVE_M * (i + 1))
asyncTask.resolve = () => {
const finished = atm.query().finished; // 已经执行了多少个分块上传的任务
const failed = atm.query().failed; // 当前有多少个分块上传失败了
const count = atm.query().count; // 一定有多少个分块
// 我们可以通过 finished - failed 得到已经成功上传的分块
}
其他功能
除此之外,ATM 还提供暂停异步任务和恢复执行的操作。(假设我们想暂停上传某个大文件的话)
atm.stop()
atm.continue()
结语
ATM 可以帮助我们并发特定数量的异步任务,还提供处理异步执行失败的重试操作的机制,这是整个功能的核心。
如果你的网站能预期后续用户会访问很多资源,那么你可以用 ATM 在后台悄悄加载某些资源,待用户下次再请求时,就可以从缓存中读取数据。
参考资料- 《Web性能权威指南》