大文件上传,其实是前端对异步编程掌控力的一次深水区测试。
你以为只是拖个文件框、调个接口而已?
真到了项目里,面对几个 G 的文件,老板一句话:
"不能卡、要能重传、并发别炸服务器、还能暂停",
你顿时发现:这不是前端上传了,这是在搞一套并发调度系统。
今天我们就一步步从 实际需求 出发,深入剖析:
- 🤹♂️ Promise 并发控制
- 🔄 返回 Promise 的本质意义
- 🧭 上传队列与暂停机制
📦 步骤一:切片上传,异步起手式
首先我们要把大文件分成小块上传:
ini
function sliceFile(file: File, chunkSize = 5 * 1024 * 1024): Blob[] {
const chunks: Blob[] = [];
let cur = 0;
while (cur < file.size) {
chunks.push(file.slice(cur, cur + chunkSize));
cur += chunkSize;
}
return chunks;
}
一张视频动辄几百兆、甚至几个 G,切一切就是上百个切片。
我第一反应就是------用 Promise.all
干一波并发!
ini
const chunks = sliceFile(file);
Promise.all(chunks.map(chunk => uploadChunk(chunk)));
结果浏览器直接卡得像在做压力测试......
❓为啥不行?不是 Promise 并发挺快的吗?
✅ 答案:并发不等于"无限放飞"
- 浏览器同域并发请求有限制(Chrome 默认 6 个)
- 你 100 个切片一把梭过去,前面排队、后面卡死,服务器也爆了
我们需要一种更稳健的方式:
"我每次最多只跑 N 个,跑完一个再顶上一个"
这就是我们接下来要实现的:并发控制队列
🔨 步骤二:上传一个切片任务
我们先写上传函数:
typescript
function uploadChunk(chunk: Blob, index: number): () => Promise<number> {
return () =>
new Promise((resolve, reject) => {
const form = new FormData();
form.append('file', chunk);
form.append('index', index.toString());
fetch('/upload-chunk', {
method: 'POST',
body: form
})
.then(res => res.ok ? resolve(index) : reject(new Error('上传失败')))
.catch(reject);
});
}
等等,为啥要返回 () => Promise
?我不能直接写成这样吗?
typescript
function uploadChunk(chunk: Blob, index: number): Promise<number> { ... } // ❓为啥不直接 Promise?
✅ 答案来了:我们要的是"待执行的任务",不是"已经在跑的请求"
队列是一个任务调度器 ,它得有"控制执行"的权力。
如果你直接给它一个已经启动的 Promise,它根本控制不住。
arduino
queue.add(uploadChunk(chunk, i)); // ❌ 这时候请求已经发出去了
而这样:
scss
queue.add(() => uploadChunk(chunk, i)); // ✅ 是个"待触发的异步任务"
只有队列说:"你现在可以上了",它才会执行。
🎛️ 步骤三:实现 UploadQueue,并发控制器
我们希望它做到:
- 同时最多执行 N 个任务
- 一个完成再启动下一个
- 可暂停/恢复
先实现基础调度功能:
kotlin
class UploadQueue {
private max: number;
private active = 0;
private queue: Array<() => Promise<any>> = [];
constructor(maxConcurrent = 3) {
this.max = maxConcurrent;
}
add(task: () => Promise<any>) {
this.queue.push(task);
this.run();
}
private run() {
if (this.active >= this.max || this.queue.length === 0) return;
const task = this.queue.shift();
if (!task) return;
this.active++;
task()
.catch(err => console.error('任务失败:', err))
.finally(() => {
this.active--;
this.run(); // 下一个
});
// 多个任务并发填满
this.run();
}
}
🧪 用法演示
ini
const chunks = sliceFile(file);
const queue = new UploadQueue(4);
chunks.forEach((chunk, i) => {
queue.add(uploadChunk(chunk, i)); // ✅ 注意是 () => Promise
});
❓此时并发真的控制住了吗?
是的,我们控制了 active
数量,每完成一个才补一个,始终同时最多 N 个上传,不会压死浏览器。
🔁 步骤四:上传失败要重试怎么办?
别担心,我们改 uploadChunk,为它加一层重试:
typescript
function uploadWithRetry(chunk: Blob, index: number, retry = 3): () => Promise<number> {
return () =>
new Promise((resolve, reject) => {
const attempt = (n: number) => {
uploadChunk(chunk, index)()
.then(resolve)
.catch(err => {
if (n > 0) {
console.warn(`第 ${index} 片上传失败,剩余重试 ${n}`);
attempt(n - 1);
} else {
reject(err);
}
});
};
attempt(retry);
});
}
现在使用时,只需替换掉任务:
arduino
queue.add(uploadWithRetry(chunk, i, 3));
你会发现:真正的链式控制、流程调度,全靠 Promise 的"返回 Promise"能力支撑。
⏸️ 步骤五:暂停上传,怎么实现?
这个问题我们一开始也困惑了:
❓"暂停"是能让正在上传的请求停下来吗?
并不能!
JavaScript 中 fetch()
一旦发出请求,除非你用 AbortController
,否则无法"中途终止"。
✅ 我们的暂停机制是这样:
- 给队列加个
paused
标志 - 正在执行的任务继续跑完
- 队列 不再启动新的任务,达到"逻辑暂停"效果
kotlin
pause() {
this.paused = true;
}
resume() {
if (!this.paused) return;
this.paused = false;
this.run();
}
private run() {
if (this.paused || this.active >= this.max || this.queue.length === 0) return;
...
}
你点暂停后,当前 3 个任务继续跑,队列停住不再拉新任务 ,
恢复时再继续往下执行。
这就像电梯暂停"进人",但已经上去的人还是要先送完的。
✅ 总结:Promise,不只是"异步工具",而是"异步调度框架"
能力 | Promise 的体现 |
---|---|
并发调度 | 控制同时跑几个任务 |
延迟执行 | 返回 () => Promise |
流程控制 | then() 返回另一个 Promise |
错误处理 | catch + 自定义重试逻辑 |
状态控制 | 用暂停标志管理流控 |
🧩 最后一问:如果我要做"真暂停",怎么办?
可以结合 AbortController
,每个上传任务挂上一个 controller
,调用 .abort()
即可。
但这会引入新的复杂度:
- controller 要单独存起来
- 被取消的请求需要重新 push 回队列
- UI 状态也要响应变化
🪄 最后一口鸡汤:不要滥用 async/await,它掩盖了真正的异步本质
在这个上传场景中,如果你只会 async/await,那么你很难写出一个结构清晰的上传队列系统。
而当你真正理解 Promise:
- 你就能写出精确控制并发数的调度器
- 你就能编排嵌套逻辑,实现"失败重试 + 流程中断"
- 你就能写出高性能、高可控的上传工具,而不是"看起来能用"的那种 demo