12.5.1 文件拆开传——分片上传原理:大文件切分与并行传输
一句话破题
分片上传就是把大文件切成固定大小的小块,分别上传后在服务端拼接——这样即使某块失败也只需重传那一块。
本质还原
前端分片实现
typescript
const CHUNK_SIZE = 5 * 1024 * 1024; // 5MB
interface Chunk {
index: number;
blob: Blob;
start: number;
end: number;
}
function createChunks(file: File): Chunk[] {
const chunks: Chunk[] = [];
let start = 0;
let index = 0;
while (start < file.size) {
const end = Math.min(start + CHUNK_SIZE, file.size);
chunks.push({
index,
blob: file.slice(start, end),
start,
end,
});
start = end;
index++;
}
return chunks;
}
// 使用示例
const file = document.getElementById('file').files[0];
const chunks = createChunks(file);
console.log(`文件大小: ${file.size}, 分片数: ${chunks.length}`);并行上传
typescript
async function uploadChunk(
chunk: Chunk,
fileId: string,
onProgress?: (percent: number) => void
): Promise<void> {
const formData = new FormData();
formData.append('chunk', chunk.blob);
formData.append('index', chunk.index.toString());
formData.append('fileId', fileId);
await fetch('/api/upload/chunk', {
method: 'POST',
body: formData,
});
}
async function uploadFile(file: File) {
// 1. 初始化上传,获取 fileId
const { fileId } = await fetch('/api/upload/init', {
method: 'POST',
body: JSON.stringify({ fileName: file.name, fileSize: file.size }),
headers: { 'Content-Type': 'application/json' },
}).then((r) => r.json());
// 2. 创建分片
const chunks = createChunks(file);
// 3. 并行上传(限制并发数)
const concurrency = 3;
for (let i = 0; i < chunks.length; i += concurrency) {
const batch = chunks.slice(i, i + concurrency);
await Promise.all(batch.map((chunk) => uploadChunk(chunk, fileId)));
console.log(`已上传 ${Math.min(i + concurrency, chunks.length)} / ${chunks.length}`);
}
// 4. 通知服务端合并
await fetch('/api/upload/merge', {
method: 'POST',
body: JSON.stringify({ fileId, totalChunks: chunks.length }),
headers: { 'Content-Type': 'application/json' },
});
}服务端处理
typescript
// app/api/upload/chunk/route.ts
import { writeFile, mkdir } from 'fs/promises';
import { join } from 'path';
export async function POST(req: Request) {
const formData = await req.formData();
const chunk = formData.get('chunk') as File;
const index = formData.get('index') as string;
const fileId = formData.get('fileId') as string;
const chunkDir = join(process.cwd(), 'uploads', fileId);
await mkdir(chunkDir, { recursive: true });
const buffer = Buffer.from(await chunk.arrayBuffer());
await writeFile(join(chunkDir, index), buffer);
return Response.json({ success: true });
}
// app/api/upload/merge/route.ts
import { readdir, readFile, writeFile, rm } from 'fs/promises';
import { join } from 'path';
export async function POST(req: Request) {
const { fileId, totalChunks } = await req.json();
const chunkDir = join(process.cwd(), 'uploads', fileId);
// 按顺序读取并合并分片
const chunks: Buffer[] = [];
for (let i = 0; i < totalChunks; i++) {
const chunkPath = join(chunkDir, i.toString());
chunks.push(await readFile(chunkPath));
}
const mergedBuffer = Buffer.concat(chunks);
await writeFile(join(process.cwd(), 'uploads', `${fileId}.file`), mergedBuffer);
// 清理临时分片
await rm(chunkDir, { recursive: true });
return Response.json({ success: true });
}AI 协作指南
- 核心意图:让 AI 帮你实现分片上传功能。
- 需求定义公式:
"请帮我实现一个支持大文件分片上传的组件,包括前端分片逻辑和后端接收合并,使用 Next.js App Router。" - 关键术语:
分片 (chunk)、并行上传、合并 (merge)、Blob.slice()
避坑指南
- 分片大小选择:太小会增加请求数,太大可能超时。通常 1-10MB。
- 并发控制:不要一次性上传所有分片,会导致浏览器卡顿。
- 临时文件清理:记得清理上传失败或超时的临时分片。
