# ParallelFileUploader - 高性能并行文件上传工具

<p align="center">
  <a href="https://www.npmjs.com/package/parallel-file-uploader">
    <img src="https://img.shields.io/npm/v/parallel-file-uploader.svg" alt="npm version">
  </a>
  <a href="https://www.npmjs.com/package/parallel-file-uploader">
    <img src="https://img.shields.io/npm/dm/parallel-file-uploader.svg" alt="npm downloads">
  </a>
  <a href="https://github.com/yemaoyang/parallel-file-uploader/blob/main/LICENSE">
    <img src="https://img.shields.io/github/license/yemaoyang/parallel-file-uploader.svg" alt="license">
  </a>
  <a href="https://coveralls.io/github/yemaoyang/parallel-file-uploader">
    <img src="https://coveralls.io/repos/github/yemaoyang/parallel-file-uploader/badge.svg" alt="coverage">
  </a>
</p>

一个功能强大、高性能的JavaScript/TypeScript文件上传工具库，专为现代Web应用设计。通过Web Worker实现真正的多线程处理，支持大文件分片并发上传、断点续传等企业级功能。

## ✨ 核心特性

- 🚀 **高性能并发上传** - 多文件、多分片并发上传，充分利用带宽
- 🧵 **Web Worker多线程** - 后台线程处理，不阻塞UI渲染
- 📦 **智能分片上传** - 自动分片，支持超大文件上传
- 🔄 **断点续传** - 网络中断自动恢复，已上传分片不重传
- 🔁 **失败自动重试** - 智能重试机制，提高上传成功率
- 📊 **实时进度监控** - 精确到字节的进度跟踪
- 🎯 **灵活的API设计** - 支持各种自定义配置和回调
- 🛡️ **文件验证** - 内置文件类型和大小验证
- 💾 **队列持久化** - 上传队列持久化到localStorage，页面刷新后可恢复
- 📝 **完整TypeScript支持** - 全面的类型定义和智能提示

### 🆕 v2.0 新增功能

- 📈 **性能监控系统** - 实时监控上传速度、内存使用、网络连接等关键指标
- 🚦 **智能速度限制** - 使用令牌桶算法实现精确的速度控制
- 💾 **队列持久化机制** - 支持将上传状态保存到本地存储，支持断点续传
- 🏗️ **模块化架构重构** - 清晰的模块划分，职责分离，易于扩展和维护
- 🧪 **完善的单元测试** - 高覆盖率的测试用例，保证代码质量
- 🎨 **详细错误分类** - 精确的错误类型分类，便于问题诊断和处理
- 🔧 **Worker管理优化** - 智能的Worker池管理，根据硬件自动调整

## 📦 安装

```bash
# 使用 npm
npm install parallel-file-uploader

# 使用 yarn
yarn add parallel-file-uploader

# 使用 pnpm
pnpm add parallel-file-uploader
```

## 🚀 快速开始

### 基础用法

```typescript
import { ParallelFileUploader } from 'parallel-file-uploader';

// 创建上传器实例
const uploader = new ParallelFileUploader({
  // 基础配置
  maxConcurrentFiles: 3,      // 同时上传3个文件
  maxConcurrentChunks: 4,     // 每个文件4个分片并发
  chunkSize: 5 * 1024 * 1024, // 5MB分片大小
  
  // 事件监听
  onFileProgress: (fileInfo) => {
    console.log(`${fileInfo.fileName}: ${fileInfo.progress}%`);
  },
  
  onFileSuccess: ({ fileInfo, data }) => {
    console.log(`${fileInfo.fileName} 上传成功`, data);
  },
  
  onFileError: (fileInfo, error) => {
    console.error(`${fileInfo.fileName} 上传失败`, error);
  }
});

// 添加文件并开始上传
const fileInput = document.getElementById('file-input') as HTMLInputElement;
fileInput.addEventListener('change', (e) => {
  const files = (e.target as HTMLInputElement).files;
  if (files) {
    uploader.addFiles(files);
  }
});
```

### 完整配置示例

```typescript
const uploader = new ParallelFileUploader({
  // 并发控制
  maxConcurrentFiles: 3,
  maxConcurrentChunks: 4,
  
  // 分片配置
  chunkSize: 5 * 1024 * 1024, // 5MB
  
  // 重试配置
  maxRetries: 3,
  retryDelay: 1000, // 1秒后重试
  
  // 文件限制
  maxFileSize: 1024 * 1024 * 1024, // 1GB
  allowedFileTypes: [
    'image/*',
    'video/*',
    'application/pdf',
    '.docx',
    '.xlsx'
  ],
  
  // 新功能配置
  enablePerformanceMonitor: true,   // 启用性能监控
  enableQueuePersistence: true,     // 启用队列持久化
  enableSpeedLimit: true,           // 启用速度限制
  maxUploadSpeed: 1024 * 1024,      // 限制上传速度为1MB/s
  persistenceKey: 'my-app-uploads', // 自定义持久化键名
  
  // 服务器交互
  sendFileInfoToServer: async (fileInfo) => {
    const response = await fetch('/api/upload/init', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        fileName: fileInfo.fileName,
        fileSize: fileInfo.fileSize,
        fileId: fileInfo.fileId
      })
    });
    const data = await response.json();
    return { isSuccess: response.ok, data };
  },
  
  sendFilePartToServer: async (fileInfo, chunkInfo) => {
    const formData = new FormData();
    formData.append('file', chunkInfo.file!);
    formData.append('fileId', fileInfo.fileId);
    formData.append('partNumber', chunkInfo.partNumber.toString());
    
    const response = await fetch('/api/upload/chunk', {
      method: 'POST',
      body: formData
    });
    const data = await response.json();
    return { isSuccess: response.ok, data };
  },
  
  sendFileCompleteToServer: async (fileInfo) => {
    const response = await fetch('/api/upload/complete', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        fileId: fileInfo.fileId,
        parts: fileInfo.uploadInfo?.parts
      })
    });
    const data = await response.json();
    return { isSuccess: response.ok, data };
  },
  
  // 断点续传支持
  getFilePartsFromServer: async (fileInfo) => {
    const response = await fetch(`/api/upload/parts/${fileInfo.fileId}`);
    const data = await response.json();
    return { isSuccess: response.ok, data: data.parts || [] };
  },
  
  // 事件回调
  onFileAdded: (fileInfo) => {
    console.log('文件已添加:', fileInfo.fileName);
  },
  
  onFileProgress: (fileInfo) => {
    console.log(`进度: ${fileInfo.fileName} - ${fileInfo.progress}%`);
  },
  
  onFileSuccess: ({ fileInfo, data }) => {
    console.log('上传成功:', fileInfo.fileName, data);
  },
  
  onFileError: (fileInfo, error) => {
    console.error('上传失败:', fileInfo.fileName, error);
  },
  
  onAllComplete: () => {
    console.log('所有文件上传完成!');
  },
  
  // 性能监控回调
  onPerformanceUpdate: (performanceData) => {
    console.log(`当前速度: ${PerformanceMonitor.formatSpeed(performanceData.currentSpeed)}`);
    console.log(`平均速度: ${PerformanceMonitor.formatSpeed(performanceData.averageSpeed)}`);
    if (performanceData.estimatedTimeRemaining) {
      console.log(`预计剩余时间: ${PerformanceMonitor.formatTime(performanceData.estimatedTimeRemaining)}`);
    }
  }
});
```

## 📖 API 文档

### 构造函数选项

| 选项 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `maxConcurrentFiles` | `number` | `3` | 最大并发上传文件数 |
| `maxConcurrentChunks` | `number` | `3` | 每个文件最大并发分片数 |
| `chunkSize` | `number` | `5242880` | 分片大小(字节)，默认5MB |
| `maxRetries` | `number` | `3` | 分片上传失败最大重试次数 |
| `retryDelay` | `number` | `1000` | 重试延迟时间(毫秒) |
| `useWorker` | `boolean` | `true` | 是否使用Web Worker |
| `maxFileSize` | `number` | - | 最大文件大小限制(字节) |
| `allowedFileTypes` | `string[]` | - | 允许的文件类型 |
| **新增配置** | | | |
| `enablePerformanceMonitor` | `boolean` | `false` | 是否启用性能监控 |
| `enableQueuePersistence` | `boolean` | `false` | 是否启用队列持久化 |
| `enableSpeedLimit` | `boolean` | `false` | 是否启用速度限制 |
| `speedLimit` | `number` | `0` | 速度限制(字节/秒)，0表示不限制 |
| `persistenceKey` | `string` | `'parallel-uploader-queue'` | 持久化存储键名 |

### 方法

#### 基础方法

##### `addFiles(files: File[] | FileList): void`

添加文件到上传队列。

```typescript
// 从input元素添加
uploader.addFiles(inputElement.files);

// 从拖放事件添加
uploader.addFiles(event.dataTransfer.files);
```

##### `pauseFile(fileId: string): void`

暂停指定文件的上传。

##### `resumeFile(fileId: string): void`

恢复指定文件的上传。

##### `cancelFile(fileId: string): void`

取消指定文件的上传。

##### `pauseAll(): void`

暂停所有文件的上传。

##### `resumeAll(): void`

恢复所有文件的上传。

##### `cancelAll(): void`

取消所有文件的上传。

##### `getStats(): UploadStats`

获取当前上传统计信息。

```typescript
const stats = uploader.getStats();
console.log(`
  队列中: ${stats.queued}
  上传中: ${stats.active}
  已完成: ${stats.completed}
  失败: ${stats.failed}
  暂停: ${stats.paused}
`);
```

#### 新增方法

##### `getPerformanceMetrics(): PerformanceMetrics | null`

获取性能监控指标（需要启用性能监控）。

```typescript
const metrics = uploader.getPerformanceMetrics();
if (metrics) {
  console.log('当前上传速度:', ParallelFileUploader.formatSpeed(metrics.uploadSpeed));
  console.log('平均上传速度:', ParallelFileUploader.formatSpeed(metrics.averageSpeed));
  console.log('峰值速度:', ParallelFileUploader.formatSpeed(metrics.peakSpeed));
  console.log('预计剩余时间:', ParallelFileUploader.formatTime(metrics.timeRemaining));
  console.log('内存使用:', metrics.memoryUsage + 'MB');
}
```

##### `setSpeedLimit(bytesPerSecond: number): void`

动态设置上传速度限制。

```typescript
// 限制为500KB/s
uploader.setSpeedLimit(500 * 1024);

// 取消限制
uploader.setSpeedLimit(0);
```

##### `setSpeedLimitEnabled(enabled: boolean): void`

启用或禁用速度限制。

```typescript
// 禁用速度限制
uploader.setSpeedLimitEnabled(false);

// 重新启用
uploader.setSpeedLimitEnabled(true);
```

##### `destroy(): void`

销毁上传器实例，释放所有资源。

### 静态方法

#### `ParallelFileUploader.calculateFileMD5(file: File, chunkSize?: number, onProgress?: Function): Promise<string>`

计算文件的MD5哈希值。

```typescript
const md5 = await ParallelFileUploader.calculateFileMD5(file, 2097152, (progress) => {
  console.log(`MD5计算进度: ${progress}%`);
});
```

#### `ParallelFileUploader.formatSpeed(bytesPerSecond: number): string`

格式化速度显示。

```typescript
console.log(ParallelFileUploader.formatSpeed(1024)); // "1.0 KB/s"
console.log(ParallelFileUploader.formatSpeed(1048576)); // "1.0 MB/s"
```

#### `ParallelFileUploader.formatTime(seconds: number): string`

格式化时间显示。

```typescript
console.log(ParallelFileUploader.formatTime(65)); // "1分钟5秒"
console.log(ParallelFileUploader.formatTime(3665)); // "1小时1分钟"
```

### 事件回调

| 回调 | 参数 | 说明 |
|------|------|------|
| `onFileAdded` | `(fileInfo: FileInfo)` | 文件添加到队列时触发 |
| `onFileProgress` | `(fileInfo: FileInfo)` | 文件上传进度更新时触发 |
| `onFileSuccess` | `({ fileInfo, data })` | 文件上传成功时触发 |
| `onFileError` | `(fileInfo: FileInfo, error: Error)` | 文件上传失败时触发 |
| `onFileComplete` | `({ fileInfo, data })` | 文件上传完成时触发(无论成功或失败) |
| `onAllComplete` | `()` | 所有文件上传完成时触发 |
| `onFileRejected` | `(file: File, reason: string)` | 文件被拒绝时触发 |
| **新增回调** | | |
| `onPerformanceUpdate` | `(metrics: PerformanceMetrics)` | 性能指标更新时触发 |

### 服务端交互回调

| 回调 | 参数 | 返回值 | 说明 |
|------|------|-------|------|
| `sendFileInfoToServer` | `(fileInfo: FileInfo)` | `Promise<Response>` | 初始化文件上传 |
| `sendFilePartToServer` | `(fileInfo, chunkInfo)` | `Promise<Response>` | 上传文件分片 |
| `sendFileCompleteToServer` | `(fileInfo)` | `Promise<Response>` | 完成文件上传 |
| `getFilePartsFromServer` | `(fileInfo)` | `Promise<Response>` | 获取已上传分片(断点续传) |
| `sendPauseToServer` | `(fileInfo)` | `Promise<Response>` | 通知服务器暂停上传 |

### 类型定义

#### FileInfo

```typescript
interface FileInfo {
  fileId: string;
  fileName: string;
  fileSize: number;
  uploadedSize: number;
  progress: number;
  status: UploadStepEnum;
  file: File;
  errorMessage?: string;
  lastUpdated?: number;
  mimeType?: string;
  totalChunks?: number;
  uploadInfo?: {
    parts?: Array<FilePartInfo>;
    md5?: string;
    [key: string]: any;
  };
  uploadData?: any;
}
```

#### PerformanceMetrics

```typescript
interface PerformanceMetrics {
  uploadSpeed: number;      // 当前上传速度（字节/秒）
  averageSpeed: number;     // 平均上传速度（字节/秒）
  timeRemaining: number;    // 预计剩余时间（秒）
  memoryUsage?: number;     // 内存使用量（MB）
  peakSpeed: number;        // 峰值速度（字节/秒）
  totalBytesUploaded: number; // 总上传字节数
  startTime: number;        // 开始时间
  activeConnections: number; // 活动连接数
}
```

#### ErrorType

```typescript
enum ErrorType {
  NETWORK = 'NETWORK',
  FILE_TOO_LARGE = 'FILE_TOO_LARGE',
  FILE_TYPE_NOT_ALLOWED = 'FILE_TYPE_NOT_ALLOWED',
  SERVER_ERROR = 'SERVER_ERROR',
  UNKNOWN = 'UNKNOWN',
}
```

## 🎯 高级用法

### 断点续传

```typescript
const uploader = new ParallelFileUploader({
  // 提供获取已上传分片的接口
  getFilePartsFromServer: async (fileInfo) => {
    const response = await fetch(`/api/upload/parts/${fileInfo.fileId}`);
    const data = await response.json();
    return { isSuccess: response.ok, data: data.parts };
  }
});
```

### 秒传实现

```typescript
const uploader = new ParallelFileUploader({
  sendFileInfoToServer: async (fileInfo) => {
    // 计算文件MD5
    const md5 = await ParallelFileUploader.calculateFileMD5(fileInfo.file);
    
    const response = await fetch('/api/upload/check', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ md5, fileName: fileInfo.fileName })
    });
    
    const data = await response.json();
    
    // 如果文件已存在，设置skipUpload标记
    if (data.exists) {
      data.skipUpload = true;
    }
    
    return { isSuccess: response.ok, data };
  }
});
```

### 性能监控示例

```typescript
const uploader = new ParallelFileUploader({
  enablePerformanceMonitor: true,
  onPerformanceUpdate: (metrics) => {
    // 更新UI显示
    document.getElementById('upload-speed').textContent = 
      ParallelFileUploader.formatSpeed(metrics.uploadSpeed);
    
    document.getElementById('time-remaining').textContent = 
      ParallelFileUploader.formatTime(metrics.timeRemaining);
    
    document.getElementById('progress-bar').style.width = 
      `${(metrics.totalBytesUploaded / totalSize) * 100}%`;
  }
});
```

### 队列持久化

```typescript
const uploader = new ParallelFileUploader({
  enableQueuePersistence: true,
  persistenceKey: 'my-app-uploads'
});

// 页面刷新后，可以从localStorage恢复队列
// 注意：由于File对象无法序列化，需要配合UI让用户重新选择文件
```

### 动态速度控制

```typescript
const uploader = new ParallelFileUploader({
  enableSpeedLimit: true,
  speedLimit: 0 // 初始不限速
});

// 根据网络状况动态调整
function adjustSpeedBasedOnNetwork() {
  const connection = (navigator as any).connection;
  if (connection) {
    switch (connection.effectiveType) {
      case '4g':
        uploader.setSpeedLimit(0); // 不限速
        break;
      case '3g':
        uploader.setSpeedLimit(500 * 1024); // 500KB/s
        break;
      case '2g':
        uploader.setSpeedLimit(100 * 1024); // 100KB/s
        break;
      default:
        uploader.setSpeedLimit(200 * 1024); // 200KB/s
    }
  }
}
```

## 🏗️ 项目结构

```
parallel-file-uploader/
├── src/
│   ├── index.ts              # 主入口文件
│   ├── type.ts               # 类型定义
│   ├── worker.ts             # Web Worker文件
│   └── modules/              # 功能模块
│       ├── FileManager.ts    # 文件管理
│       ├── ChunkManager.ts   # 分片管理
│       ├── WorkerManager.ts  # Worker管理
│       ├── UploadManager.ts  # 上传逻辑
│       ├── PerformanceMonitor.ts # 性能监控
│       ├── QueuePersistence.ts   # 队列持久化
│       └── SpeedLimiter.ts   # 速度限制
├── tests/                    # 单元测试
├── examples/                 # 示例代码
│   ├── basic/               # 基础示例
│   └── advanced/            # 高级示例
└── dist/                    # 构建输出
```

## 🧪 测试

```bash
# 运行测试
npm test

# 运行测试并生成覆盖率报告
npm run test:coverage

# 运行测试并监听文件变化
npm run test:watch
```

## 🏗️ 服务端实现参考

### 初始化上传接口

```javascript
app.post('/api/upload/init', async (req, res) => {
  const { fileName, fileSize, fileId } = req.body;
  
  // 检查文件是否已存在（秒传）
  const existingFile = await checkFileExists(fileName);
  if (existingFile) {
    return res.json({ 
      success: true, 
      data: { 
        skipUpload: true,
        url: existingFile.url 
      }
    });
  }
  
  // 创建上传会话
  const session = await createUploadSession({
    fileId,
    fileName,
    fileSize,
    totalParts: Math.ceil(fileSize / CHUNK_SIZE)
  });
  
  res.json({ success: true, data: session });
});
```

### 分片上传接口

```javascript
app.post('/api/upload/chunk', async (req, res) => {
  const { fileId, partNumber } = req.body;
  const file = req.files.file;
  
  // 保存分片
  const etag = await saveChunk(fileId, partNumber, file.data);
  
  res.json({ 
    success: true, 
    data: { etag, partNumber }
  });
});
```

### 完成上传接口

```javascript
app.post('/api/upload/complete', async (req, res) => {
  const { fileId, parts } = req.body;
  
  // 合并分片
  const fileUrl = await mergeChunks(fileId, parts);
  
  res.json({ 
    success: true, 
    data: { url: fileUrl }
  });
});
```

### 获取已上传分片接口（断点续传）

```javascript
app.get('/api/upload/parts/:fileId', async (req, res) => {
  const { fileId } = req.params;
  
  // 获取已上传的分片信息
  const parts = await getUploadedParts(fileId);
  
  res.json({ 
    success: true, 
    data: { parts }
  });
});
```

## 🤝 贡献指南

欢迎贡献代码！请查看 [CONTRIBUTING.md](CONTRIBUTING.md) 了解详情。

## 📄 许可证

本项目采用 [MIT](LICENSE) 许可证。

## 🙋 常见问题

### Q: 如何处理跨域问题？

A: 确保服务端正确设置了CORS头：

```javascript
app.use((req, res, next) => {
  res.header('Access-Control-Allow-Origin', '*');
  res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS');
  res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
  next();
});
```

### Q: Worker文件加载失败怎么办？

A: 工具会自动降级到主线程模式。您也可以手动禁用Worker：

```typescript
const uploader = new ParallelFileUploader({
  useWorker: false
});
```

### Q: 如何优化上传性能？

A:

1. 调整并发数：根据网络和服务器能力调整 `maxConcurrentFiles` 和 `maxConcurrentChunks`
2. 优化分片大小：网络好时增大 `chunkSize`，网络差时减小
3. 使用性能监控：通过 `enablePerformanceMonitor` 监控并调优
4. 启用Worker：确保 `useWorker: true` 以使用多线程

### Q: 队列持久化有什么限制？

A:

1. localStorage 通常有 5-10MB 的大小限制
2. File 对象无法序列化，刷新后需要重新选择文件
3. 建议只用于保存上传进度，配合UI实现完整的断点续传

## 📞 联系方式

- GitHub Issues: [github.com/yemaoyang/parallel-file-uploader/issues](https://github.com/yemaoyang/parallel-file-uploader/issues)
- Email: <346751186@qq.com>

## 🌟 Star History

[![Star History Chart](https://api.star-history.com/svg?repos=yemaoyang/parallel-file-uploader&type=Date)](https://star-history.com/#yemaoyang/parallel-file-uploader&Date)
