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);
  }}
/>

配置分片大小和并发数

可以通过 chunkSizeconcurrency 属性配置分片大小和并发数量。

拖拽文件到此处或

<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.sizechunkSize 计算分片索引、起止位置等纯计算,可放 Worker 减少主线程压力(若与 hash 同处可一并做)。

保留在主线程的职责:

  • DOM 与进度 UI:进度条、状态文案、取消按钮等。
  • 网络请求:分片上传、合并接口、查询已上传分片等,因 fetch/XMLHttpRequest 在主线程更易与现有上传逻辑配合。
  • 文件读取与分片 Blob 创建File.slice() 得到 Blob 通常在主线程即可;若后端要求「按分片先算 hash 再上传」,则 hash 在 Worker 算,主线程只拿结果去请求。

通信与数据约定:

  • 主线程 → Worker:传入 ArrayBufferFileReader.readAsArrayBuffer 读出的分片)或分片索引 + 配置,由 Worker 计算该分片的 hash。
  • Worker → 主线程:返回 { chunkIndex, hash } 或全量 hash;主线程把 hash 填入上传参数或用于秒传判断。
  • 大文件全量 hash 可拆成「按分片算 hash,再在 Worker 或主线程做一次合并」(如简单拼接再 hash),避免一次性把整个文件读进内存。

降级与兼容:

  • 不支持 Worker 的环境(如部分老旧浏览器):可在主线程同步计算 hash(仅建议小文件),或跳过 hash 仅用分片上传 + 合并,并提示「秒传与完整性校验不可用」。
  • Worker 内不要依赖 windowdocument,只做纯计算;哈希库需选支持 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分片大小(字节)number2 * 1024 * 1024
concurrency并发上传数量number3
multiple是否支持多文件booleanfalse
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 去重或校验
  • 建议根据服务器性能调整 chunkSizeconcurrency
  • 组件已标记 'use client',需要在客户端组件中使用