parallel-file-uploader
Version:
高性能并行文件上传工具,支持大文件分片上传、断点续传、Web Worker多线程处理
711 lines (552 loc) • 21.2 kB
Markdown
# 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
[](https://star-history.com/#yemaoyang/parallel-file-uploader&Date)