parallel-file-uploader
Version:
高性能并行文件上传工具,支持大文件分片上传、断点续传、Web Worker多线程处理
1,031 lines (812 loc) • 32.4 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>
<a href="https://github.com/yemaoyang/parallel-file-uploader/actions">
<img src="https://img.shields.io/github/actions/workflow/status/yemaoyang/parallel-file-uploader/ci.yml" alt="CI/CD">
</a>
</p>
一个功能强大、高性能的JavaScript/TypeScript文件上传工具库,专为现代Web应用设计。通过Web Worker实现真正的多线程处理,支持大文件分片并发上传、断点续传等企业级功能。
## ✨ 核心特性
- 🚀 **高性能并发上传** - 多文件、多分片并发上传,充分利用带宽
- 🧵 **Web Worker多线程** - 后台线程处理,不阻塞UI渲染
- 📦 **智能分片上传** - 自动分片,支持超大文件上传
- 🔄 **断点续传** - 网络中断自动恢复,已上传分片不重传
- 🔁 **失败自动重试** - 智能重试机制,提高上传成功率
- 📊 **实时进度监控** - 精确到字节的进度跟踪
- 🎯 **灵活的API设计** - 支持各种自定义配置和回调
- 🛡️ **文件验证** - 内置文件类型和大小验证
- 💾 **队列持久化** - 上传队列持久化到localStorage,页面刷新后可恢复
- 📝 **完整TypeScript支持** - 全面的类型定义和智能提示
### 🆕 v2.0 新增功能
- 📈 **性能监控系统** - 实时监控上传速度、内存使用、网络连接等关键指标
- 🚦 **智能速度限制** - 使用令牌桶算法实现精确的速度控制
- 💾 **队列持久化机制** - 支持将上传状态保存到本地存储,支持断点续传
- 🏗️ **模块化架构重构** - 清晰的模块划分,职责分离,易于扩展和维护
- 🧪 **完善的单元测试** - 高覆盖率的测试用例,保证代码质量
- 🎨 **详细错误分类** - 精确的错误类型分类,便于问题诊断和处理
- 🔧 **Worker管理优化** - 智能的Worker池管理,根据硬件自动调整
### 🔧 v2.0.3 最新改进
- 🐛 **增强错误处理** - 更详细的错误信息和分类,便于调试和问题定位
- 🔍 **调试模式** - 新增调试模式,提供详细的运行日志和配置信息
- ⚡ **性能优化** - 优化文件验证和分片处理逻辑,提升上传效率
- 🛠️ **配置验证** - 智能配置验证,提供优化建议和警告
- 📋 **兼容性改进** - 保持向后兼容,同时提供新的API方法
- 🎯 **代码质量** - 遵循SOLID原则和KISS原则,提高代码可维护性
## 📦 安装
```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'
],
// 新功能配置 - 默认均为false,按需启用
enablePerformanceMonitor: true, // 启用性能监控
enableQueuePersistence: true, // 启用队列持久化
enableSpeedLimit: true, // 启用速度限制
maxUploadSpeed: 1024 * 1024, // 限制上传速度为1MB/s
persistenceKey: 'my-app-uploads', // 自定义持久化键名
debugMode: true, // 启用调试模式,输出详细日志
// 服务器交互 - 必须实现这些回调
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(`当前速度: ${formatSpeed(performanceData.currentSpeed)}`);
console.log(`平均速度: ${formatSpeed(performanceData.averageSpeed)}`);
if (performanceData.estimatedTimeRemaining) {
console.log(`预计剩余时间: ${formatTime(performanceData.estimatedTimeRemaining)}`);
}
}
});
// 工具函数
function formatSpeed(bytesPerSecond: number): string {
const units = ['B/s', 'KB/s', 'MB/s', 'GB/s'];
let size = bytesPerSecond;
let unitIndex = 0;
while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024;
unitIndex++;
}
return `${size.toFixed(1)} ${units[unitIndex]}`;
}
function formatTime(milliseconds: number): string {
const seconds = Math.floor(milliseconds / 1000);
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const remainingSeconds = seconds % 60;
if (hours > 0) {
return `${hours}小时${minutes}分钟${remainingSeconds}秒`;
} else if (minutes > 0) {
return `${minutes}分钟${remainingSeconds}秒`;
} else {
return `${remainingSeconds}秒`;
}
}
```
## 📖 API 文档
### 构造函数选项
| 选项 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `maxConcurrentFiles` | `number` | `3` | 最大并发上传文件数 |
| `maxConcurrentChunks` | `number` | `3` | 每个文件最大并发分片数 |
| `chunkSize` | `number` | `5242880` | 分片大小(字节),默认5MB |
| `maxRetries` | `number` | `3` | 分片上传失败最大重试次数 |
| `retryDelay` | `number` | `1000` | 重试延迟时间(毫秒) |
| `maxFileSize` | `number` | - | 最大文件大小限制(字节) |
| `allowedFileTypes` | `string[]` | - | 允许的文件类型 |
| **新增配置** | | | |
| `enablePerformanceMonitor` | `boolean` | `false` | 是否启用性能监控 |
| `enableQueuePersistence` | `boolean` | `false` | 是否启用队列持久化 |
| `enableSpeedLimit` | `boolean` | `false` | 是否启用速度限制 |
| `maxUploadSpeed` | `number` | `0` | 速度限制(字节/秒),0表示不限制 |
| `persistenceKey` | `string` | `'parallel-uploader-queue'` | 持久化存储键名 |
| `debugMode` | `boolean` | `false` | 是否启用调试模式,输出详细日志 |
### 实例方法
#### 基础方法
##### `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}
`);
```
#### 新增方法
##### `getPerformanceData(): PerformanceData`
获取性能监控数据(需要启用性能监控)。
```typescript
const data = uploader.getPerformanceData();
console.log('当前上传速度:', data.currentSpeed, 'B/s');
console.log('平均上传速度:', data.averageSpeed, 'B/s');
console.log('峰值速度:', data.peakSpeed, 'B/s');
console.log('已传输字节数:', data.bytesTransferred);
```
##### `setSpeedLimit(bytesPerSecond: number, enabled: boolean = true): void`
动态设置上传速度限制。
```typescript
// 限制为500KB/s
uploader.setSpeedLimit(500 * 1024, true);
// 取消限制
uploader.setSpeedLimit(0, false);
```
##### `setPerformanceMonitoring(enabled: boolean): void`
启用或禁用性能监控。
```typescript
// 启用性能监控
uploader.setPerformanceMonitoring(true);
// 禁用性能监控
uploader.setPerformanceMonitoring(false);
```
##### `setQueuePersistence(enabled: boolean): void`
启用或禁用队列持久化。
```typescript
// 启用队列持久化
uploader.setQueuePersistence(true);
// 禁用队列持久化
uploader.setQueuePersistence(false);
```
##### `destroy(): void`
销毁上传器实例,释放所有资源。
##### `setDebugMode(enabled: boolean): void`
启用或禁用调试模式。
```typescript
// 启用调试模式,输出详细日志
uploader.setDebugMode(true);
// 禁用调试模式
uploader.setDebugMode(false);
```
##### `getConfiguration(): Configuration`
获取当前配置信息。
```typescript
const config = uploader.getConfiguration();
console.log('当前配置:', config);
// 输出示例:
// {
// fileManager: { maxFileSize: '1 GB', supportedTypesDescription: '所有文件类型' },
// chunkManager: { chunkSize: '5 MB' },
// features: { speedLimit: true, performanceMonitor: true, queuePersistence: true, workerSupport: true },
// limits: { maxConcurrentFiles: 3, maxConcurrentChunks: 4, maxRetries: 3 }
// }
```
### 静态方法
#### `ParallelFileUploader.calculateFileMD5(file: File, chunkSize?: number, onProgress?: Function): Promise<string>`
计算文件的MD5哈希值,支持大文件分片计算,避免内存溢出。
**参数说明:**
| 参数 | 类型 | 默认值 | 必填 | 说明 |
|------|------|--------|------|------|
| `file` | `File` | - | ✓ | 要计算MD5的文件对象 |
| `chunkSize` | `number` | `2097152` (2MB) | ✗ | 分片大小(字节),用于大文件分片计算 |
| `onProgress` | `Function` | - | ✗ | 进度回调函数,参数为进度百分比(0-100) |
**返回值:**
- `Promise<string>` - 返回32位小写的MD5哈希值
**使用场景:**
1. **文件秒传** - 上传前计算MD5,检查服务器是否已存在相同文件
2. **文件完整性校验** - 确保上传过程中文件未损坏
3. **重复文件检测** - 避免上传重复文件
4. **断点续传** - 通过MD5标识唯一文件
**基础用法:**
```typescript
// 简单计算文件MD5
const file = document.getElementById('file-input').files[0];
const md5 = await ParallelFileUploader.calculateFileMD5(file);
console.log('文件MD5:', md5); // 输出: "d41d8cd98f00b204e9800998ecf8427e"
```
**带进度回调的用法:**
```typescript
const file = document.getElementById('file-input').files[0];
const md5 = await ParallelFileUploader.calculateFileMD5(
file,
1024 * 1024, // 1MB分片
(progress) => {
console.log(`MD5计算进度: ${progress.toFixed(1)}%`);
// 更新UI进度条
document.getElementById('md5-progress').style.width = `${progress}%`;
}
);
console.log('计算完成,MD5值:', md5);
```
**实际应用示例:**
```typescript
// 文件秒传实现
async function checkFileExists(file) {
// 显示MD5计算进度
const progressBar = document.getElementById('md5-progress');
const statusText = document.getElementById('status-text');
statusText.textContent = '正在计算文件指纹...';
try {
const md5 = await ParallelFileUploader.calculateFileMD5(
file,
2 * 1024 * 1024, // 2MB分片
(progress) => {
progressBar.style.width = `${progress}%`;
statusText.textContent = `计算文件指纹: ${progress.toFixed(1)}%`;
}
);
// 检查服务器是否已存在相同文件
const response = await fetch('/api/file/check', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
md5,
fileName: file.name,
fileSize: file.size
})
});
const result = await response.json();
if (result.exists) {
statusText.textContent = '文件已存在,秒传成功!';
return { skipUpload: true, url: result.url };
} else {
statusText.textContent = '开始上传文件...';
return { skipUpload: false, md5 };
}
} catch (error) {
console.error('MD5计算失败:', error);
statusText.textContent = 'MD5计算失败,将直接上传';
return { skipUpload: false, md5: null };
}
}
// 在上传器中使用
const uploader = new ParallelFileUploader({
sendFileInfoToServer: async (fileInfo) => {
// 先检查文件是否存在
const checkResult = await checkFileExists(fileInfo.file);
if (checkResult.skipUpload) {
// 秒传成功
return {
isSuccess: true,
data: {
skipUpload: true,
url: checkResult.url
}
};
}
// 正常上传流程,携带MD5
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,
md5: checkResult.md5 // 携带预计算的MD5
})
});
const data = await response.json();
return { isSuccess: response.ok, data };
}
});
```
**大文件处理示例:**
```typescript
// 处理大文件(如视频文件)
async function calculateLargeFileMD5(file) {
const fileSize = file.size;
const fileSizeText = formatFileSize(fileSize);
console.log(`开始计算大文件MD5: ${file.name} (${fileSizeText})`);
const startTime = Date.now();
const md5 = await ParallelFileUploader.calculateFileMD5(
file,
5 * 1024 * 1024, // 5MB分片,适合大文件
(progress) => {
const elapsed = Date.now() - startTime;
const estimated = elapsed / (progress / 100);
const remaining = estimated - elapsed;
console.log(`MD5计算: ${progress.toFixed(1)}% (预计剩余 ${formatTime(remaining)})`);
}
);
const totalTime = Date.now() - startTime;
console.log(`MD5计算完成: ${md5} (耗时 ${formatTime(totalTime)})`);
return md5;
}
// 工具函数
function formatFileSize(bytes) {
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
let size = bytes;
let unitIndex = 0;
while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024;
unitIndex++;
}
return `${size.toFixed(1)} ${units[unitIndex]}`;
}
function formatTime(milliseconds) {
const seconds = Math.floor(milliseconds / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
if (hours > 0) {
return `${hours}小时${minutes % 60}分钟`;
} else if (minutes > 0) {
return `${minutes}分钟${seconds % 60}秒`;
} else {
return `${seconds}秒`;
}
}
```
**注意事项:**
1. **内存占用** - 使用分片计算避免大文件一次性加载到内存
2. **计算时间** - 大文件MD5计算可能耗时较长,建议显示进度
3. **异步处理** - 方法返回Promise,注意正确处理异步调用
4. **错误处理** - 文件读取失败时会抛出异常,建议用try-catch包装
5. **分片大小** - 分片过小会影响计算效率,过大可能占用过多内存,建议1-5MB
**性能建议:**
```typescript
// 根据文件大小动态调整分片大小
function getOptimalChunkSize(fileSize) {
if (fileSize < 10 * 1024 * 1024) { // < 10MB
return 1024 * 1024; // 1MB分片
} else if (fileSize < 100 * 1024 * 1024) { // < 100MB
return 2 * 1024 * 1024; // 2MB分片
} else if (fileSize < 1024 * 1024 * 1024) { // < 1GB
return 5 * 1024 * 1024; // 5MB分片
} else { // >= 1GB
return 10 * 1024 * 1024; // 10MB分片
}
}
// 使用动态分片大小
const optimalChunkSize = getOptimalChunkSize(file.size);
const md5 = await ParallelFileUploader.calculateFileMD5(file, optimalChunkSize);
```
### 事件回调
| 回调 | 参数 | 说明 |
|------|------|------|
| `onFileAdded` | `(fileInfo: FileInfo)` | 文件添加到队列时触发 |
| `onFileProgress` | `(fileInfo: FileInfo)` | 文件上传进度更新时触发 |
| `onFileSuccess` | `({ fileInfo, data })` | 文件上传成功时触发 |
| `onFileError` | `(fileInfo: FileInfo, error: Error)` | 文件上传失败时触发 |
| `onFileComplete` | `({ fileInfo, data })` | 文件上传完成时触发(无论成功或失败) |
| `onAllComplete` | `()` | 所有文件上传完成时触发 |
| `onFileRejected` | `(file: File, reason: string)` | 文件被拒绝时触发 |
| **新增回调** | | |
| `onPerformanceUpdate` | `(data: PerformanceData)` | 性能指标更新时触发 |
### 服务端交互回调
| 回调 | 参数 | 返回值 | 说明 |
|------|------|-------|------|
| `sendFileInfoToServer` | `(fileInfo: FileInfo)` | `Promise<ResGlobalInterface<any>>` | 初始化文件上传 |
| `sendFilePartToServer` | `(fileInfo, chunkInfo)` | `Promise<ResGlobalInterface<any>>` | 上传文件分片 |
| `sendFileCompleteToServer` | `(fileInfo)` | `Promise<ResGlobalInterface<any>>` | 完成文件上传 |
| `getFilePartsFromServer` | `(fileInfo)` | `Promise<ResGlobalInterface<FilePartInfo[]>>` | 获取已上传分片(断点续传) |
| `sendPauseToServer` | `(fileInfo)` | `Promise<ResGlobalInterface<any>>` | 通知服务器暂停上传 |
### 类型定义
#### FileInfo
```typescript
interface FileInfo {
fileId: string; // 文件唯一标识符
fileName: string; // 文件名
fileSize: number; // 文件大小(字节)
uploadedSize: number; // 已上传大小(字节)
progress: number; // 上传进度百分比 (0-100)
status: UploadStepEnum; // 文件上传状态
file: File; // 原始文件对象
errorMessage?: string; // 错误消息
lastUpdated?: number; // 最后更新时间戳
mimeType?: string; // 文件MIME类型
totalChunks?: number; // 总分片数量
uploadInfo?: {
parts?: Array<FilePartInfo>; // 已上传的分片列表
md5?: string; // 文件MD5值
[key: string]: any; // 其他扩展字段
};
uploadData?: any; // 自定义上传数据
}
```
#### PerformanceData
```typescript
interface PerformanceData {
currentSpeed: number; // 当前上传速度(字节/秒)
averageSpeed: number; // 平均上传速度(字节/秒)
peakSpeed: number; // 峰值速度(字节/秒)
activeConnections: number; // 活动连接数
bytesTransferred: number; // 总传输字节数
elapsedTime: number; // 已耗时(毫秒)
activeFiles: number; // 活动文件数
totalFiles: number; // 总文件数
timestamp: number; // 时间戳
estimatedTimeRemaining?: number; // 预计剩余时间(毫秒)
memoryUsage?: {
used: number; // 已使用内存(字节)
total: number; // 总内存(字节)
percentage: 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', // 服务器错误
CHUNK_UPLOAD_FAILED = 'CHUNK_UPLOAD_FAILED', // 分片上传失败
FILE_INITIALIZATION_FAILED = 'FILE_INITIALIZATION_FAILED', // 文件初始化失败
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 };
}
});
```
#### 🔧 partSize 兼容性增强 (v2.0.2+)
工具现在支持在没有 `partSize` 信息的情况下进行断点续传。即使后端 API 返回的分片数据中缺少 `partSize` 字段,系统也会自动计算并修复:
```typescript
// ✅ 支持这种格式的返回数据
{
"isSuccess": true,
"data": [
{
"partNumber": 1,
"etag": "6cd32d171a26ca01611b1f72f4f5664"
// partSize 缺失也没关系!系统会自动计算
},
{
"partNumber": 2,
"etag": "3bc6cd1a9f9c95468c7cbcf3b31fa1"
// 同样支持 partSize: 0 或 undefined
}
]
}
```
**智能计算逻辑:**
- 普通分片:使用配置的 `chunkSize`
- 最后分片:根据 `文件总大小 - 前面分片大小` 自动计算
- 数据验证:自动检查分片编号的合理性和 etag 的有效性
这使得工具能够兼容更多类型的后端实现,无需修改现有的服务端代码。
### 秒传实现
```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 =
formatSpeed(metrics.currentSpeed);
if (metrics.estimatedTimeRemaining) {
document.getElementById('time-remaining').textContent =
formatTime(metrics.estimatedTimeRemaining);
}
const progress = metrics.bytesTransferred / totalBytes * 100;
document.getElementById('progress-bar').style.width = `${progress}%`;
}
});
```
### 队列持久化
```typescript
const uploader = new ParallelFileUploader({
enableQueuePersistence: true,
persistenceKey: 'my-app-uploads'
});
// 页面刷新后,可以从localStorage恢复队列
// 注意:由于File对象无法序列化,需要配合UI让用户重新选择文件
```
### 动态速度控制
```typescript
const uploader = new ParallelFileUploader({
enableSpeedLimit: true,
maxUploadSpeed: 0 // 初始不限速
});
// 根据网络状况动态调整
function adjustSpeedBasedOnNetwork() {
const connection = (navigator as any).connection;
if (connection) {
switch (connection.effectiveType) {
case '4g':
uploader.setSpeedLimit(0, false); // 不限速
break;
case '3g':
uploader.setSpeedLimit(500 * 1024, true); // 500KB/s
break;
case '2g':
uploader.setSpeedLimit(100 * 1024, true); // 100KB/s
break;
default:
uploader.setSpeedLimit(200 * 1024, true); // 200KB/s
}
}
}
```
## 🏗️ 项目结构
```
parallel-file-uploader/
├── src/
│ ├── index.ts # 主入口文件
│ ├── type.ts # 类型定义
│ ├── worker.ts # Web Worker文件
│ └── modules/ # 功能模块
│ ├── index.ts # 模块导出文件
│ ├── FileManager.ts # 文件管理
│ ├── ChunkManager.ts # 分片管理
│ ├── WorkerManager.ts # Worker管理
│ ├── 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({
isSuccess: true,
data: {
skipUpload: true,
url: existingFile.url
}
});
}
// 创建上传会话
const session = await createUploadSession({
fileId,
fileName,
fileSize,
totalParts: Math.ceil(fileSize / CHUNK_SIZE)
});
res.json({ isSuccess: 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({
isSuccess: 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({
isSuccess: 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({
isSuccess: 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会自动跳过初始化。
### Q: 如何优化上传性能?
A:
1. 调整并发数:根据网络和服务器能力调整 `maxConcurrentFiles` 和 `maxConcurrentChunks`
2. 优化分片大小:网络好时增大 `chunkSize`,网络差时减小
3. 使用性能监控:通过 `enablePerformanceMonitor` 监控并调优
4. 启用Worker:确保Worker正常工作以使用多线程
### Q: 队列持久化有什么限制?
A:
1. localStorage 通常有 5-10MB 的大小限制
2. File 对象无法序列化,刷新后需要重新选择文件
3. 建议只用于保存上传进度,配合UI实现完整的断点续传
### Q: 新功能默认是否开启?
A: 所有新功能(性能监控、队列持久化、速度限制)默认都是关闭的,需要手动启用:
```typescript
const uploader = new ParallelFileUploader({
enablePerformanceMonitor: true, // 手动启用性能监控
enableQueuePersistence: true, // 手动启用队列持久化
enableSpeedLimit: true, // 手动启用速度限制
});
```
## 📞 联系方式
- 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)