你給一家在線教育平臺(tái)做「課程視頻批量上傳」功能。
需求聽(tīng)起來(lái)很樸素:講師后臺(tái)一次性拖 20 個(gè) 4K 視頻,瀏覽器要穩(wěn)、要快、要能斷網(wǎng)續(xù)傳。
你第一版直接 <input type="file">
+ FormData
,結(jié)果上線當(dāng)天就炸:
- 講師 A 上傳 4.7 GB 的
.mov
,Chrome 直接 內(nèi)存溢出 崩潰; - 講師 B 網(wǎng)斷了 3 分鐘,重新上傳發(fā)現(xiàn)進(jìn)度條歸零,心態(tài)跟著歸零;
- 運(yùn)營(yíng)同學(xué)瘋狂 @ 前端:“你們是不是沒(méi)做分片?”
解決方案:三層防線,把 4 GB 切成 2 MB 的“薯片”
1. 表面用法:分片 + 并發(fā),瀏覽器再也不卡
const CHUNK_SIZE = 2 * 1024 * 1024;
export async function* sliceFile(file) {
let cur = 0;
while (cur < file.size) {
yield file.slice(cur, cur + CHUNK_SIZE);
cur += CHUNK_SIZE;
}
}
import pLimit from 'p-limit';
const limit = pLimit(5);
export async function upload(file) {
const hash = await calcHash(file);
const tasks = [];
for await (const chunk of sliceFile(file)) {
tasks.push(limit(() => uploadChunk({ hash, chunk })));
}
await Promise.all(tasks);
await mergeChunks(hash, file.name);
}
逐行拆解:
sliceFile
用 file.slice
生成 Blob 片段,不占額外內(nèi)存;p-limit
控制并發(fā),避免 100 個(gè)請(qǐng)求同時(shí)打爆瀏覽器;calcHash
用 WebWorker 算 MD5,頁(yè)面不卡頓(后面細(xì)講)。
2. 底層機(jī)制:斷點(diǎn)續(xù)傳到底續(xù)在哪?
角色 | 存儲(chǔ)位置 | 內(nèi)容 | 生命周期 |
---|
前端 | IndexedDB | hash → 已上傳分片索引數(shù)組 | 瀏覽器本地,清緩存即失效 |
后端 | Redis / MySQL | hash → 已接收分片索引數(shù)組 | 可配置 TTL,支持跨端續(xù)傳 |
sequenceDiagram
participant F as 前端
participant B as 后端
F->>B: POST /prepare {hash, totalChunks}
B-->>F: 200 OK {uploaded:[0,3,7]}
loop 上傳剩余分片
F->>B: POST /upload {hash, index, chunkData}
B-->>F: 200 OK
end
F->>B: POST /merge {hash}
B-->>F: 200 OK
Note over B: 按順序?qū)懘疟P(pán)
- 前端先
POST /prepare
帶 hash + 總分片數(shù); - 后端返回已上傳索引
[0, 3, 7]
; - 前端跳過(guò)這 3 片,只傳剩余;
- 全部完成后
POST /merge
,后端按順序?qū)懘疟P(pán)。
3. 設(shè)計(jì)哲學(xué):把“上傳”做成可插拔的協(xié)議
interface Uploader {
prepare(file: File): Promise<PrepareResp>;
upload(chunk: Blob, index: number): Promise<void>;
merge(): Promise<string>;
}
我們實(shí)現(xiàn)了三套:
BrowserUploader
:純前端分片;TusUploader
:遵循 tus.io 協(xié)議,天然斷點(diǎn)續(xù)傳;AliOssUploader
:直傳 OSS,用 OSS 的斷點(diǎn) SDK。
方案 | 并發(fā)控制 | 斷點(diǎn)續(xù)傳 | 秒傳 | 代碼量 |
---|
自研 | 手動(dòng) | 自己實(shí)現(xiàn) | 手動(dòng) | 300 行 |
tus | 內(nèi)置 | 協(xié)議級(jí) | 需后端 | 100 行 |
OSS | 內(nèi)置 | SDK 級(jí) | 自動(dòng) | 50 行 |
應(yīng)用擴(kuò)展:拿來(lái)即用的配置片段
1. WebWorker 算 Hash(防卡頓)
importScripts('spark-md5.min.js');
self.onmessage = ({ data: file }) => {
const spark = new SparkMD5.ArrayBuffer();
const reader = new FileReaderSync();
for (let i = 0; i < file.size; i += CHUNK_SIZE) {
spark.append(reader.readAsArrayBuffer(file.slice(i, i + CHUNK_SIZE)));
}
self.postMessage(spark.end());
};
2. 環(huán)境適配
環(huán)境 | 適配點(diǎn) |
---|
瀏覽器 | 需兼容 Safari 14 以下無(wú) File.prototype.slice (用 webkitSlice 兜底) |
Node | 用 fs.createReadStream 分片,Hash 用 crypto.createHash('md5') |
Electron | 渲染進(jìn)程直接走瀏覽器方案,主進(jìn)程可復(fù)用 Node 邏輯 |
舉一反三:3 個(gè)變體場(chǎng)景
- 秒傳
上傳前先算 hash → 調(diào)后端 /exists?hash=xxx
→ 已存在直接返回 URL,0 流量完成。 - 加密上傳
在 uploadChunk
里加一層 AES-GCM
加密,后端存加密塊,下載時(shí)由前端解密。 - P2P 協(xié)同上傳
用 WebRTC 把同局域網(wǎng)學(xué)員的瀏覽器變成 CDN,分片互傳后再統(tǒng)一上報(bào),節(jié)省 70% 出口帶寬。
小結(jié)
大文件上傳的核心不是“傳”,而是“斷”。
把 4 GB 切成 2 MB 的薯片,再配上一張能續(xù)命的“進(jìn)度表”,瀏覽器就能穩(wěn)穩(wěn)地吃下任何體積的視頻。
?轉(zhuǎn)自https://juejin.cn/post/7530868895768838179
該文章在 2025/8/5 8:52:55 編輯過(guò)