FileUploader 大文件上传
大文件上传组件支持分片上传、断点续传、进度追踪等功能,可以高效地上传大文件。
特性
- ✅ 文件分片上传,支持大文件
- ✅ 断点续传,网络中断后可继续上传
- ✅ 并发控制,可配置并发数量
- ✅ 实时进度追踪
- ✅ 拖拽上传支持
- ✅ 错误重试机制
- ✅ 文件大小验证
- ✅ Web Worker:哈希/分片等 CPU 密集计算放子线程,不阻塞主线程与进度 UI
- ✅ SSR 兼容
基础用法
拖拽文件到此处或
import { FileUploader } from '@enterprise-ui/react19';
<FileUploader
uploadUrl="/api/upload"
onProgress={(progress, file) => {
console.log(`${file.name}: ${progress}%`);
}}
onSuccess={(file, response) => {
console.log('上传成功:', response);
}}
onError={(file, error) => {
console.error('上传失败:', error);
}}
/>配置分片大小和并发数
可以通过 chunkSize 和 concurrency 属性配置分片大小和并发数量。
拖拽文件到此处或
<FileUploader
uploadUrl="/api/upload"
chunkSize={5 * 1024 * 1024} // 5MB 分片
concurrency={5} // 5个并发
/>文件大小限制
可以通过 maxSize 属性限制文件大小。
拖拽文件到此处或
<FileUploader
uploadUrl="/api/upload"
maxSize={100 * 1024 * 1024} // 最大 100MB
/>多文件上传
设置 multiple 属性支持多文件上传。
拖拽文件到此处或
<FileUploader
uploadUrl="/api/upload"
multiple // 支持多文件
/>文件类型限制
可以通过 accept 属性限制文件类型。
拖拽文件到此处或
<FileUploader
uploadUrl="/api/upload"
accept="image/*" // 只接受图片
/>实现原理详解
1. 文件分片原理
大文件上传的核心思想是将文件分割成多个小分片,每个分片独立上传。这样可以:
- 避免单次请求超时
- 支持断点续传
- 提高上传成功率
- 支持并发上传,提升速度
分片算法:
// 1. 计算分片数量
const chunkSize = 2 * 1024 * 1024; // 2MB
const totalChunks = Math.ceil(file.size / chunkSize);
// 2. 创建分片数组
const chunks: ChunkInfo[] = [];
for (let i = 0; i < totalChunks; i++) {
const start = i * chunkSize;
const end = Math.min(start + chunkSize, file.size);
chunks.push({
index: i,
blob: file.slice(start, end), // Blob.slice API
uploaded: false,
});
}关键技术点:
- Blob.slice():浏览器原生 API,性能高效,不占用内存
- 分片大小选择:2MB 是平衡点,太小增加请求数,太大增加失败风险
- 最后一片处理:使用
Math.min()确保不超出文件大小
2. 文件唯一标识生成
每个文件需要唯一标识,用于服务端识别和断点续传。使用 SHA-256 哈希算法生成。
生成算法:
async function generateFileId(file: File): Promise<string> {
// 使用文件名 + 大小 + 修改时间生成唯一标识
const data = `${file.name}-${file.size}-${file.lastModified}`;
const encoder = new TextEncoder();
const dataBuffer = encoder.encode(data);
// SHA-256 哈希
const hashBuffer = await crypto.subtle.digest('SHA-256', dataBuffer);
const hashArray = Array.from(new Uint8Array(hashBuffer));
// 转换为十六进制字符串
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
}为什么这样设计:
- 文件名:区分不同文件
- 文件大小:确保文件完整性
- 修改时间:检测文件是否被修改
- SHA-256:确保唯一性和安全性
3. 并发控制原理
并发控制是平衡上传速度和服务器压力的关键。使用队列 + Promise 实现。
并发控制算法:
// 并发上传实现
async function uploadFile(file: File, chunks: ChunkInfo[]) {
const concurrency = 3; // 并发数
const uploadQueue: Promise<void>[] = [];
let uploadedCount = 0;
// 递归上传函数
const uploadNext = async () => {
// 找到下一个未上传的分片
const pendingChunk = chunks.find(chunk => !chunk.uploaded);
if (!pendingChunk) return;
try {
// 上传分片
await uploadChunk(file, pendingChunk);
pendingChunk.uploaded = true;
uploadedCount++;
// 更新进度
updateProgress((uploadedCount / chunks.length) * 100);
// 继续上传下一个
if (uploadedCount < chunks.length) {
await uploadNext();
} else {
// 所有分片上传完成,合并文件
await mergeChunks(file);
}
} catch (error) {
// 错误重试(简化版)
await uploadNext();
}
};
// 启动并发上传
for (let i = 0; i < Math.min(concurrency, chunks.length); i++) {
uploadQueue.push(uploadNext());
}
// 等待所有上传完成
await Promise.all(uploadQueue);
}并发控制策略:
- 队列机制:维护上传队列,控制同时进行的上传数
- Promise.all:等待所有并发任务完成
- 动态调度:一个分片完成后立即开始下一个
- 错误隔离:单个分片失败不影响其他分片
并发数选择:
- 3-5 个:平衡速度和服务器压力,推荐值
- 1 个:最安全,但速度慢
- 10+ 个:速度快,但可能压垮服务器
4. 断点续传原理
断点续传的核心是:服务端记录已上传的分片,客户端只上传未完成的分片。
实现流程:
// 1. 生成文件唯一标识
const fileId = await generateFileId(file);
// 2. 检查已上传分片(服务端接口)
const response = await fetch(`/api/upload/check?fileId=${fileId}`);
const { uploadedChunks } = await response.json();
// uploadedChunks: [0, 1, 2, 5, 6] // 已上传的分片索引
// 3. 标记已上传的分片
chunks.forEach((chunk, index) => {
if (uploadedChunks.includes(index)) {
chunk.uploaded = true;
}
});
// 4. 只上传未完成的分片
const chunksToUpload = chunks.filter(chunk => !chunk.uploaded);
// 5. 继续上传
await uploadChunks(chunksToUpload);服务端实现要点:
- 分片存储:每个分片单独存储,使用 fileId + chunkIndex 作为 key
- 状态记录:记录每个文件的上传状态(Redis/数据库)
- 完整性校验:合并前校验所有分片是否完整
- 过期清理:定期清理未完成的上传任务
5. 进度计算原理
进度计算需要考虑分片上传的异步特性,确保进度准确反映实际上传情况。
进度计算算法:
// 精确的进度计算
let uploadedBytes = 0;
const totalBytes = file.size;
chunks.forEach((chunk, index) => {
if (chunk.uploaded) {
uploadedBytes += chunk.blob.size;
}
});
// 计算百分比
const progress = Math.round((uploadedBytes / totalBytes) * 100);
// 或者使用分片数量(更简单)
const progress = Math.round((uploadedCount / totalChunks) * 100);进度更新时机:
- 分片上传完成:立即更新进度
- 使用防抖:避免频繁更新 UI
- 合并阶段:合并时进度保持 99%,合并完成后 100%
6. 错误处理和重试
重试策略:
// 指数退避重试
async function uploadChunkWithRetry(
chunk: ChunkInfo,
maxRetries = 3
): Promise<void> {
let retries = 0;
while (retries < maxRetries) {
try {
await uploadChunk(chunk);
return; // 成功则返回
} catch (error) {
retries++;
if (retries >= maxRetries) {
throw error; // 达到最大重试次数,抛出错误
}
// 指数退避:1s, 2s, 4s
const delay = Math.pow(2, retries) * 1000;
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}错误处理策略:
- 网络错误:自动重试,指数退避
- 服务器错误:5xx 错误重试,4xx 错误不重试
- 超时处理:设置请求超时时间,超时后重试
- 用户取消:支持取消上传,清理状态
7. 文件合并原理
所有分片上传完成后,需要通知服务端合并文件。
合并流程:
// 1. 所有分片上传完成
if (uploadedCount === totalChunks) {
// 2. 通知服务端合并
const response = await fetch('/api/upload/merge', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
fileId: fileId,
fileName: file.name,
totalChunks: totalChunks,
}),
});
// 3. 服务端合并逻辑(伪代码)
// - 按索引顺序读取所有分片
// - 合并成完整文件
// - 保存到最终位置
// - 删除临时分片
// - 返回文件访问 URL
}8. Web Worker 与主线程分工
大文件场景下,文件哈希(MD5/SHA)、分片时的读取与计算等属于 CPU 密集操作, 若在主线程执行会阻塞 UI,导致进度条卡顿、页面无响应。使用 Web Worker 把这类计算放到子线程,主线程只负责 UI 更新和网络请求,体验更流畅。
适合放进 Worker 的职责:
- 文件/分片哈希计算:用于秒传(服务端已有相同 hash 则跳过上传)、分片完整性校验。大文件全量算 hash 非常耗 CPU,必须放 Worker。
- 分片元信息计算:如根据
file.size和chunkSize计算分片索引、起止位置等纯计算,可放 Worker 减少主线程压力(若与 hash 同处可一并做)。
保留在主线程的职责:
- DOM 与进度 UI:进度条、状态文案、取消按钮等。
- 网络请求:分片上传、合并接口、查询已上传分片等,因
fetch/XMLHttpRequest在主线程更易与现有上传逻辑配合。 - 文件读取与分片 Blob 创建:
File.slice()得到 Blob 通常在主线程即可;若后端要求「按分片先算 hash 再上传」,则 hash 在 Worker 算,主线程只拿结果去请求。
通信与数据约定:
- 主线程 → Worker:传入
ArrayBuffer(FileReader.readAsArrayBuffer读出的分片)或分片索引 + 配置,由 Worker 计算该分片的 hash。 - Worker → 主线程:返回
{ chunkIndex, hash }或全量 hash;主线程把 hash 填入上传参数或用于秒传判断。 - 大文件全量 hash 可拆成「按分片算 hash,再在 Worker 或主线程做一次合并」(如简单拼接再 hash),避免一次性把整个文件读进内存。
降级与兼容:
- 不支持
Worker的环境(如部分老旧浏览器):可在主线程同步计算 hash(仅建议小文件),或跳过 hash 仅用分片上传 + 合并,并提示「秒传与完整性校验不可用」。 - Worker 内不要依赖
window、document,只做纯计算;哈希库需选支持 Worker 的(如 Web Crypto API 或可在 Worker 里跑的 MD5/SHA 实现)。
简要流程(分片 hash 在 Worker):
// 主线程:读取分片,交给 Worker 算 hash
const buffer = await file.slice(start, end).arrayBuffer();
worker.postMessage({ type: 'hash', chunkIndex: i, buffer }, [buffer]);
// Worker 内:计算 hash 后回传
self.onmessage = async (e) => {
const { chunkIndex, buffer } = e.data;
const hash = await computeHash(buffer); // 使用 Web Crypto 或 MD5/SHA 库
self.postMessage({ chunkIndex, hash });
};
// 主线程:收到 hash 后上传该分片(或先汇总再上传)
worker.onmessage = (e) => {
const { chunkIndex, hash } = e.data;
uploadChunkWithHash(chunkIndex, hash);
};总结:大文件上传文档里把「Web Worker」写清楚,能体现你在性能与体验上的考虑:哈希与重计算不阻塞主线程,进度条和交互保持流畅,同时为秒传和校验留好扩展点。
工作原理
1. 文件分片
大文件会被分割成多个小分片(默认 2MB),每个分片独立上传。
2. 并发控制
通过队列控制同时上传的分片数量,避免服务器压力过大。默认并发数为 3。
3. 断点续传
每个文件都有唯一标识(基于文件名、大小、修改时间生成),服务端记录已上传的分片。 上传失败后,只上传未完成的分片。
4. 进度计算
根据已上传的分片数量计算整体进度,实时更新进度条。
服务端接口要求
组件需要服务端提供以下接口:
1. 分片上传接口
POST /api/upload
FormData:
- file: Blob (分片文件)
- chunkIndex: number (分片索引)
- totalChunks: number (总分片数)
- fileId: string (文件唯一标识)
- fileName: string (文件名)
- fileSize: number (文件大小)
Response:
{
success: boolean,
chunkIndex: number
}2. 合并接口
POST /api/upload/merge
Body:
{
fileId: string,
fileName: string,
totalChunks: number
}
Response:
{
success: boolean,
url: string // 文件访问地址
}API
FileUploader Props
| 参数 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| uploadUrl | 文件上传地址 | string | - |
| chunkSize | 分片大小(字节) | number | 2 * 1024 * 1024 |
| concurrency | 并发上传数量 | number | 3 |
| multiple | 是否支持多文件 | boolean | false |
| accept | 接受的文件类型 | string | - |
| maxSize | 最大文件大小(字节) | number | - |
| onProgress | 上传进度回调 | (progress: number, file: File) => void | - |
| onSuccess | 上传成功回调 | (file: File, response: any) => void | - |
| onError | 上传失败回调 | (file: File, error: Error) => void | - |
| className | 自定义类名 | string | - |
注意事项
- 需要服务端支持分片上传和合并接口
- 文件唯一标识基于文件名、大小、修改时间生成
- 断点续传需要服务端记录已上传的分片
- 若使用 Web Worker 做文件/分片 hash(秒传、校验),需服务端支持按 hash 去重或校验
- 建议根据服务器性能调整
chunkSize和concurrency - 组件已标记
'use client',需要在客户端组件中使用