1. 需求与挑战分析
大文件上传面临的主要挑战:
- 单个文件体积过大导致上传失败
- 长时间上传过程中断网络连接
- 页面刷新/关闭导致上传进度丢失
- 多文件并发上传资源占用问题
- 前端计算负载影响用户体验
2. 整体架构设计
graph TB
subgraph 前端
A[文件选择/拖拽] --> B[文件预处理]
B --> C[文件唯一标识计算]
C --> D[分片策略]
D --> E[上传队列]
E --> F[并发控制]
F --> G[上传分片]
H[断点续传模块] --> E
I[本地存储] <--> H
G --> J[上传完成]
G -.-> |网络错误| K[失败重试]
K --> E
end
subgraph 后端
L[接收分片] --> M[分片验证]
M --> N[临时存储]
O[分片合并] --> P[完整性校验]
P --> Q[持久化存储]
end
G --> L
J --> O
3. 核心技术方案详解
3.1 分片上传设计
将大文件分割成固定大小的块(如5MB),并发上传多个块:
graph LR
A[大文件 100MB] --> B[分片1: 0-5MB]
A --> C[分片2: 5-10MB]
A --> D[分片3: 10-15MB]
A --> E[...]
A --> F[分片20: 95-100MB]
B --> G[上传队列]
C --> G
D --> G
E --> G
F --> G
G --> H[服务器]
3.2 断点续传实现
sequenceDiagram
participant 客户端
participant 本地存储
participant 服务端
Note over 客户端: 开始上传
客户端->>本地存储: 存储文件信息和分片状态
客户端->>服务端: 上传分片1
客户端->>本地存储: 更新分片1状态为已上传
客户端->>服务端: 上传分片2
客户端->>本地存储: 更新分片2状态为已上传
Note over 客户端: 网络中断/页面刷新
Note over 客户端: 恢复上传
客户端->>本地存储: 读取文件信息和分片状态
客户端->>服务端: 查询已上传分片
服务端-->>客户端: 返回已上传分片列表
客户端->>客户端: 对比确定剩余分片
客户端->>服务端: 继续上传分片3
客户端->>本地存储: 更新分片3状态
3.3 性能优化与并发控制
graph TD
subgraph 性能优化策略
A1[Web Worker] --> A2[计算文件哈希]
B1[并发控制] --> B2[限制同时上传分片数]
C1[批量处理] --> C2[队列管理]
D1[资源管理] --> D2[动态调整并发度]
E1[预加载策略] --> E2[预先加载下一批分片]
end
subgraph 异常处理
F1[网络监测] --> F2[自动重试]
G1[分片校验] --> G2[MD5校验]
H1[超时处理] --> H2[指数退避算法]
end
4. 核心代码实现
4.1 文件分片处理
/**
* 文件分片处理函数
* @param {File} file - 原始文件对象
* @param {number} defaultChunkSize - 默认分片大小(bytes)
* @returns {Array} - 分片数组
*/
function createFileChunks(file, defaultChunkSize = 5 * 1024 * 1024) {
// 根据文件大小和类型动态计算最优分片大小
const chunkSize = calculateOptimalChunkSize(file);
const chunks = [];
const chunksCount = Math.ceil(file.size / chunkSize);
for (let i = 0; i < chunksCount; i++) {
const start = i * chunkSize;
const end = Math.min(file.size, start + chunkSize);
const chunk = file.slice(start, end);
chunks.push({
index: i,
file: chunk,
size: chunk.size,
filename: file.name,
chunkName: `${file.name}-${i}`,
progress: 0
});
}
return {
chunks,
count: chunksCount,
fileName: file.name,
fileSize: file.size,
chunkSize
};
}
/**
* 计算最优分片大小
* @param {File} file - 文件对象
* @returns {number} - 最佳分片大小(bytes)
*/
function calculateOptimalChunkSize(file) {
const fileSize = file.size;
const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
let networkFactor = 1;
// 根据网络状况调整
if (connection) {
if (connection.effectiveType === '4g') {
networkFactor = 1.5;
} else if (connection.effectiveType === '3g') {
networkFactor = 0.8;
} else if (connection.effectiveType === '2g' || connection.effectiveType === 'slow-2g') {
networkFactor = 0.5;
}
}
// 根据文件大小确定基础分片大小
let baseSize;
if (fileSize < 20 * 1024 * 1024) { // < 20MB
baseSize = 1 * 1024 * 1024; // 1MB
} else if (fileSize < 100 * 1024 * 1024) { // < 100MB
baseSize = 4 * 1024 * 1024; // 4MB
} else if (fileSize < 500 * 1024 * 1024) { // < 500MB
baseSize = 8 * 1024 * 1024; // 8MB
} else {
baseSize = 10 * 1024 * 1024; // 10MB
}
// 特定文件类型可能需要调整
const fileType = file.type.split('/')[0];
let typeFactor = 1;
if (fileType === 'video' || fileType === 'audio') {
typeFactor = 1.2; // 媒体文件使用更大的分片
} else if (fileType === 'image') {
typeFactor = 0.8; // 图片使用稍小的分片
}
// 计算最终分片大小并确保在合理范围内
const calculatedSize = Math.floor(baseSize * networkFactor * typeFactor);
const minChunkSize = 512 * 1024; // 最小512KB
const maxChunkSize = 20 * 1024 * 1024; // 最大20MB
return Math.max(minChunkSize, Math.min(calculatedSize, maxChunkSize));
}
4.2 文件唯一标识计算(Web Worker)
// worker.js
self.onmessage = function(e) {
const { file, chunkSize, useOptimized } = e.data;
// 引入SparkMD5库
importScripts('./spark-md5.min.js');
// 根据文件大小选择哈希计算策略
if (useOptimized && file.size > 50 * 1024 * 1024) {
computeOptimizedHash(file);
} else {
computeFullHash(file, chunkSize);
}
};
// 完整文件哈希计算
function computeFullHash(file, chunkSize) {
const fileReader = new FileReader();
const spark = new SparkMD5.ArrayBuffer();
const chunks = Math.ceil(file.size / chunkSize);
let currentChunk = 0;
function loadNext() {
const start = currentChunk * chunkSize;
const end = Math.min(start + chunkSize, file.size);
fileReader.readAsArrayBuffer(file.slice(start, end));
}
fileReader.onload = e => {
spark.append(e.target.result);
currentChunk++;
if (currentChunk < chunks) {
// 报告进度
self.postMessage({
progress: Math.floor((currentChunk / chunks) * 100)
});
loadNext();
} else {
// 完成并返回结果
const fileHash = spark.end();
self.postMessage({
fileHash,
success: true,
method: 'full'
});
}
};
fileReader.onerror = () => {
self.postMessage({
success: false,
error: 'File read error'
});
};
loadNext();
}
// 优化的采样哈希计算(适用于大文件)
function computeOptimizedHash(file) {
const spark = new SparkMD5.ArrayBuffer();
const fileSize = file.size;
const sampleSize = 2 * 1024 * 1024; // 2MB样本
const totalSamples = 5; // 总共取5个样本点
// 计算采样点
const samples = [];
// 头部样本
samples.push(file.slice(0, sampleSize));
// 中间样本
for (let i = 1; i < totalSamples - 1; i++) {
const position = Math.floor(fileSize * (i / totalSamples));
samples.push(file.slice(position, position + sampleSize));
}
// 尾部样本
samples.push(file.slice(fileSize - sampleSize));
// 读取并处理样本
let processedSamples = 0;
const fileReader = new FileReader();
fileReader.onload = e => {
spark.append(e.target.result);
processedSamples++;
if (processedSamples < samples.length) {
self.postMessage({
progress: Math.floor((processedSamples / samples.length) * 100)
});
fileReader.readAsArrayBuffer(samples[processedSamples]);
} else {
// 附加文件大小信息确保唯一性
const fileHash = spark.end() + '-' + fileSize;
self.postMessage({
fileHash,
success: true,
method: 'optimized'
});
}
};
fileReader.onerror = () => {
self.postMessage({
success: false,
error: 'Sample read error'
});
};
// 开始读取第一个样本
fileReader.readAsArrayBuffer(samples[0]);
}
4.3 断点续传与本地存储
/**
* 增强型上传状态管理器
* 支持LocalStorage、IndexedDB和内存存储
*/
class EnhancedStorageManager {
constructor(options = {}) {
this.options = {
storageKey: 'enhanced_file_upload_state',
preferredStorage: 'auto', // 'auto', 'indexeddb', 'localstorage', 'memory'
expirationTime: 7 * 24 * 60 * 60 * 1000, // 默认7天
...options
};
this.storageType = this.options.preferredStorage === 'auto'
? this.detectBestStorage()
: this.options.preferredStorage;
this.memoryStorage = {};
this.dbConnection = null;
// 如果使用IndexedDB,则初始化
if (this.storageType === 'indexeddb') {
this.initIndexedDB();
}
console.log(`存储管理器初始化,使用存储类型: ${this.storageType}`);
}
// 检测最佳存储方式
detectBestStorage() {
if (this.isIndexedDBSupported()) {
return 'indexeddb';
} else if (this.isLocalStorageSupported()) {
return 'localstorage';
} else {
return 'memory';
}
}
// 检测IndexedDB支持
isIndexedDBSupported() {
return window.indexedDB !== undefined;
}
// 检测LocalStorage支持
isLocalStorageSupported() {
try {
const test = '__test__';
localStorage.setItem(test, test);
localStorage.removeItem(test);
return true;
} catch (e) {
return false;
}
}
// 初始化IndexedDB
initIndexedDB() {
return new Promise((resolve, reject) => {
const request = indexedDB.open('FileUploadDB', 1);
request.onerror = (event) => {
console.error('IndexedDB初始化失败:', event);
this.storageType = this.isLocalStorageSupported() ? 'localstorage' : 'memory';
resolve(false);
};
request.onsuccess = (event) => {
this.dbConnection = event.target.result;
resolve(true);
};
request.onupgradeneeded = (event) => {
const db = event.target.result;
if (!db.objectStoreNames.contains('fileStates')) {
db.createObjectStore('fileStates', { keyPath: 'fileHash' });
}
};
});
}
// 保存上传状态
async saveUploadState(fileHash, fileInfo, uploadedChunks) {
const stateData = {
fileHash,
fileInfo,
uploadedChunks,
lastUpdated: Date.now()
};
switch (this.storageType) {
case 'indexeddb':
await this.saveToIndexedDB(stateData);
break;
case 'localstorage':
this.saveToLocalStorage(stateData);
break;
case 'memory':
default:
this.saveToMemory(stateData);
}
}
// 保存到IndexedDB
saveToIndexedDB(stateData) {
return new Promise((resolve, reject) => {
if (!this.dbConnection) {
this.initIndexedDB().then(() => this.saveToIndexedDB(stateData).then(resolve));
return;
}
const transaction = this.dbConnection.transaction(['fileStates'], 'readwrite');
const store = transaction.objectStore('fileStates');
const request = store.put(stateData);
request.onsuccess = () => resolve();
request.onerror = (err) => {
console.error('保存到IndexedDB失败:', err);
// 降级到localStorage
this.storageType = this.isLocalStorageSupported() ? 'localstorage' : 'memory';
this.saveUploadState(stateData.fileHash, stateData.fileInfo, stateData.uploadedChunks).then(resolve);
};
});
}
// 保存到LocalStorage
saveToLocalStorage(stateData) {
try {
const storageData = JSON.parse(localStorage.getItem(this.options.storageKey) || '{}');
storageData[stateData.fileHash] = stateData;
localStorage.setItem(this.options.storageKey, JSON.stringify(storageData));
} catch (error) {
console.error('保存到localStorage失败:', error);
// 降级到内存存储
this.storageType = 'memory';
this.saveToMemory(stateData);
}
}
// 保存到内存
saveToMemory(stateData) {
this.memoryStorage[stateData.fileHash] = stateData;
}
// 获取上传状态
getUploadState() {
switch (this.storageType) {
case 'localstorage':
try {
return JSON.parse(localStorage.getItem(this.options.storageKey) || '{}');
} catch (error) {
console.error('解析上传状态失败', error);
return {};
}
case 'memory':
return {...this.memoryStorage};
case 'indexeddb':
console.warn('使用indexeddb时,请使用异步的getFileState方法');
return {};
default:
return {};
}
}
// 获取文件状态
async getFileState(fileHash) {
switch (this.storageType) {
case 'indexeddb':
return await this.getFromIndexedDB(fileHash);
case 'localstorage':
return this.getFromLocalStorage(fileHash);
case 'memory':
default:
return this.getFromMemory(fileHash);
}
}
// 从IndexedDB获取
getFromIndexedDB(fileHash) {
return new Promise((resolve, reject) => {
if (!this.dbConnection) {
this.initIndexedDB().then(() => this.getFromIndexedDB(fileHash).then(resolve));
return;
}
const transaction = this.dbConnection.transaction(['fileStates'], 'readonly');
const store = transaction.objectStore('fileStates');
const request = store.get(fileHash);
request.onsuccess = (event) => {
resolve(event.target.result);
};
request.onerror = (error) => {
console.error('从IndexedDB获取失败:', error);
// 降级
this.storageType = this.isLocalStorageSupported() ? 'localstorage' : 'memory';
resolve(this.getFileState(fileHash));
};
});
}
// 从LocalStorage获取
getFromLocalStorage(fileHash) {
try {
const storageData = JSON.parse(localStorage.getItem(this.options.storageKey) || '{}');
return storageData[fileHash];
} catch (error) {
console.error('从localStorage获取失败:', error);
// 降级
this.storageType = 'memory';
return this.getFromMemory(fileHash);
}
}
// 从内存获取
getFromMemory(fileHash) {
return this.memoryStorage[fileHash];
}
// 更新分片状态
async updateChunkState(fileHash, chunkIndex, progress = 100) {
const fileState = await this.getFileState(fileHash);
if (fileState) {
if (!fileState.uploadedChunks) {
fileState.uploadedChunks = {};
}
fileState.uploadedChunks[chunkIndex] = progress;
fileState.lastUpdated = Date.now();
await this.saveUploadState(fileHash, fileState.fileInfo, fileState.uploadedChunks);
}
}
// 清理过期状态
async cleanExpiredStates() {
const now = Date.now();
const expirationTime = this.options.expirationTime;
switch (this.storageType) {
case 'indexeddb':
await this.cleanExpiredFromIndexedDB(now, expirationTime);
break;
case 'localstorage':
this.cleanExpiredFromLocalStorage(now, expirationTime);
break;
case 'memory':
this.cleanExpiredFromMemory(now, expirationTime);
break;
}
}
// 从IndexedDB清理过期状态
cleanExpiredFromIndexedDB(now, expirationTime) {
return new Promise((resolve, reject) => {
if (!this.dbConnection) {
this.initIndexedDB().then(() => this.cleanExpiredFromIndexedDB(now, expirationTime).then(resolve));
return;
}
const transaction = this.dbConnection.transaction(['fileStates'], 'readwrite');
const store = transaction.objectStore('fileStates');
const request = store.openCursor();
request.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
if (now - cursor.value.lastUpdated > expirationTime) {
store.delete(cursor.value.fileHash);
}
cursor.continue();
} else {
resolve();
}
};
request.onerror = (error) => {
console.error('清理IndexedDB过期状态失败:', error);
resolve();
};
});
}
// 从LocalStorage清理过期状态
cleanExpiredFromLocalStorage(now, expirationTime) {
try {
const storageData = JSON.parse(localStorage.getItem(this.options.storageKey) || '{}');
Object.keys(storageData).forEach(hash => {
if (now - storageData[hash].lastUpdated > expirationTime) {
delete storageData[hash];
}
});
localStorage.setItem(this.options.storageKey, JSON.stringify(storageData));
} catch (error) {
console.error('清理localStorage过期状态失败:', error);
}
}
// 从内存清理过期状态
cleanExpiredFromMemory(now, expirationTime) {
Object.keys(this.memoryStorage).forEach(hash => {
if (now - this.memoryStorage[hash].lastUpdated > expirationTime) {
delete this.memoryStorage[hash];
}
});
}
}
// 为了向后兼容,保留原来的类名
class UploadStateManager extends EnhancedStorageManager {
constructor(storageKey = 'file_upload_state') {
super({
storageKey,
preferredStorage: 'auto'
});
}
}
4.4 上传队列管理与并发控制
/**
* 上传队列管理器
*/
class UploadQueueManager {
constructor(options = {}) {
this.maxConcurrentUploads = options.maxConcurrentUploads || 3;
this.retryTimes = options.retryTimes || 3;
this.retryDelay = options.retryDelay || 1000;
this.queue = [];
this.activeUploads = 0;
this.stateManager = new UploadStateManager();
}
// 添加分片到队列
addChunksToQueue(chunks, fileHash, callbacks = {}) {
const fileState = this.stateManager.getFileState(fileHash);
const uploadedChunks = fileState ? fileState.uploadedChunks : {};
// 过滤已上传的分片
const chunksToUpload = chunks.filter(chunk => {
return !uploadedChunks[chunk.index] || uploadedChunks[chunk.index] < 100;
});
chunksToUpload.forEach(chunk => {
this.queue.push({
chunk,
fileHash,
retryCount: 0,
callbacks
});
});
this.processQueue();
}
// 处理上传队列
processQueue() {
if (this.queue.length === 0 || this.activeUploads >= this.maxConcurrentUploads) {
return;
}
while (this.queue.length > 0 && this.activeUploads < this.maxConcurrentUploads) {
const task = this.queue.shift();
this.activeUploads++;
this.uploadChunk(task);
}
}
// 上传分片
uploadChunk(task) {
const { chunk, fileHash, retryCount, callbacks } = task;
const formData = new FormData();
formData.append('chunk', chunk.file);
formData.append('hash', fileHash);
formData.append('chunkIndex', chunk.index);
formData.append('filename', chunk.filename);
formData.append('totalChunks', chunk.totalChunks);
const xhr = new XMLHttpRequest();
// 进度监听
xhr.upload.onprogress = event => {
if (event.lengthComputable) {
const percentComplete = Math.floor((event.loaded / event.total) * 100);
if (callbacks.onProgress) {
callbacks.onProgress(chunk.index, percentComplete);
}
// 更新本地存储中的进度
this.stateManager.updateChunkState(fileHash, chunk.index, percentComplete);
}
};
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
this.activeUploads--;
// 更新进度为100%
this.stateManager.updateChunkState(fileHash, chunk.index, 100);
if (callbacks.onChunkSuccess) {
callbacks.onChunkSuccess(chunk.index);
}
// 检查是否所有分片都已上传成功
const fileState = this.stateManager.getFileState(fileHash);
const allChunksUploaded = Object.values(fileState.uploadedChunks).every(progress => progress === 100);
if (allChunksUploaded && callbacks.onComplete) {
// 通知服务器合并分片
this.mergeChunks(fileHash, chunk.filename, callbacks.onComplete);
} else {
this.processQueue();
}
} else {
this.handleUploadError(task);
}
};
xhr.onerror = () => {
this.handleUploadError(task);
};
// 超时处理
xhr.timeout = 30000; // 30秒
xhr.ontimeout = () => {
this.handleUploadError(task);
};
// 发送请求
xhr.open('POST', '/api/upload/chunk', true);
xhr.send(formData);
}
// 处理上传错误
handleUploadError(task) {
this.activeUploads--;
// 指数退避算法 - 随着重试次数增加,延迟也会增加
if (task.retryCount < this.retryTimes) {
const delay = this.retryDelay * Math.pow(2, task.retryCount);
setTimeout(() => {
this.queue.unshift({
...task,
retryCount: task.retryCount + 1
});
this.processQueue();
}, delay);
if (task.callbacks.onError) {
task.callbacks.onError(task.chunk.index, `上传失败,${this.retryTimes - task.retryCount}次重试后再次尝试`);
}
} else {
if (task.callbacks.onError) {
task.callbacks.onError(task.chunk.index, '上传失败,已达到最大重试次数');
}
this.processQueue();
}
}
// 通知服务器合并分片
mergeChunks(fileHash, filename, onComplete) {
const xhr = new XMLHttpRequest();
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
try {
const response = JSON.parse(xhr.responseText);
if (response.success && onComplete) {
onComplete(response);
}
} catch (error) {
console.error('解析合并响应失败', error);
}
}
};
xhr.open('POST', '/api/upload/merge', true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.send(JSON.stringify({
fileHash,
filename
}));
}
// 暂停所有上传
pauseAll() {
// 清空队列
this.queue = [];
this.activeUploads = 0;
}
// 根据网络状况动态调整并发数
adjustConcurrency(networkQuality) {
// 根据网络质量调整并发数
// networkQuality: 0-1, 0为最差,1为最好
const minConcurrency = 1;
const maxConcurrency = 6;
this.maxConcurrentUploads = Math.floor(minConcurrency + networkQuality * (maxConcurrency - minConcurrency));
this.processQueue();
}
}
4.5 网络状态监控与自动处理
/**
* 网络监控与自动处理
*/
class NetworkMonitor {
constructor(uploadManager) {
this.uploadManager = uploadManager;
this.isOnline = navigator.onLine;
// 初始化网络监听
this.setupEventListeners();
// 每10秒检查一次网络质量
setInterval(() => this.checkNetworkQuality(), 10000);
}
setupEventListeners() {
// 监听在线状态变化
window.addEventListener('online', () => {
this.isOnline = true;
this.uploadManager.processQueue(); // 恢复上传
});
window.addEventListener('offline', () => {
this.isOnline = false;
this.uploadManager.pauseAll(); // 暂停上传
});
// 监听页面可见性变化
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
// 页面重新可见时,重新检查网络并恢复上传
if (this.isOnline) {
this.uploadManager.processQueue();
}
}
});
// 监听页面卸载事件,保存当前状态
window.addEventListener('beforeunload', () => {
// 保存当前上传状态
// 已由UploadStateManager实时保存
});
}
// 检查网络质量
async checkNetworkQuality() {
if (!this.isOnline) return;
try {
const startTime = Date.now();
const response = await fetch('/api/ping');
const endTime = Date.now();
if (response.ok) {
const latency = endTime - startTime;
let quality;
// 根据延迟估算网络质量
if (latency < 100) {
quality = 1; // 极佳
} else if (latency < 200) {
quality = 0.8; // 良好
} else if (latency < 500) {
quality = 0.6; // 一般
} else if (latency < 1000) {
quality = 0.4; // 较差
} else {
quality = 0.2; // 很差
}
// 调整并发数
this.uploadManager.adjustConcurrency(quality);
}
} catch (error) {
console.error('网络质量检测失败', error);
this.uploadManager.adjustConcurrency(0.2); // 网络不佳,降低并发
}
}
}
4.6 主控制器整合
/**
* 文件上传主控制器
*/
class FileUploader {
constructor(options = {}) {
this.options = {
chunkSize: 5 * 1024 * 1024, // 默认5MB
maxConcurrentUploads: 3,
retryTimes: 3,
retryDelay: 1000,
...options
};
this.stateManager = new UploadStateManager();
this.queueManager = new UploadQueueManager({
maxConcurrentUploads: this.options.maxConcurrentUploads,
retryTimes: this.options.retryTimes,
retryDelay: this.options.retryDelay
});
this.networkMonitor = new NetworkMonitor(this.queueManager);
this.worker = null;
// 初始化worker
this.initWorker();
}
// 初始化Web Worker
initWorker() {
if (window.Worker) {
this.worker = new Worker('hash-worker.js');
}
}
// 检查文件是否已存在于服务器
async checkFileExists(fileHash) {
try {
const response = await fetch(`/api/upload/verify-file?hash=${fileHash}`);
const result = await response.json();
return result.exists;
} catch (error) {
console.error('检查文件存在性失败', error);
return false;
}
}
// 上传文件
async uploadFile(file, callbacks = {}) {
const fileObj = {
id: 'file-' + Date.now() + Math.random().toString(36).substring(2),
name: file.name,
size: file.size,
type: file.type,
lastModified: file.lastModified
};
// 计算文件哈希
const fileHash = await this.calculateFileHash(file, callbacks.onHashProgress);
if (!fileHash) {
if (callbacks.onError) {
callbacks.onError('计算文件哈希失败');
}
return;
}
// 检查文件是否已存在于服务器
const fileExists = await this.checkFileExists(fileHash);
if (fileExists) {
if (callbacks.onComplete) {
callbacks.onComplete({
success: true,
message: '文件已存在,无需重新上传',
fileHash,
skipUpload: true
});
}
return;
}
// 检查是否有未完成的上传
const existingState = this.stateManager.getFileState(fileHash);
// 创建分片
const { chunks, count } = createFileChunks(file, this.options.chunkSize);
// 存储文件状态
const uploadedChunks = existingState ? existingState.uploadedChunks : {};
this.stateManager.saveUploadState(fileHash, fileObj, uploadedChunks);
// 添加到上传队列
this.queueManager.addChunksToQueue(chunks, fileHash, {
onProgress: (chunkIndex, progress) => {
if (callbacks.onChunkProgress) {
callbacks.onChunkProgress(chunkIndex, progress);
}
// 计算总体进度
if (callbacks.onTotalProgress) {
const totalProgress = this.calculateTotalProgress(fileHash);
callbacks.onTotalProgress(totalProgress);
}
},
onChunkSuccess: (chunkIndex) => {
if (callbacks.onChunkSuccess) {
callbacks.onChunkSuccess(chunkIndex);
}
},
onError: (chunkIndex, error) => {
if (callbacks.onChunkError) {
callbacks.onChunkError(chunkIndex, error);
}
},
onComplete: (response) => {
if (callbacks.onComplete) {
callbacks.onComplete(response);
}
}
});
}
// 计算文件哈希
calculateFileHash(file, onProgress) {
return new Promise((resolve, reject) => {
// 检查是否支持Worker
if (this.worker) {
const useOptimizedHash = file.size > 50 * 1024 * 1024 && this.options.useOptimizedHash !== false;
// 发送文件到Worker计算哈希
this.worker.postMessage({
file,
chunkSize: this.options.chunkSize,
useOptimized: useOptimizedHash
});
this.worker.onmessage = (e) => {
if (e.data.progress) {
if (onProgress) {
const method = e.data.method || (useOptimizedHash ? '优化采样' : '完整');
onProgress(e.data.progress, method);
}
} else if (e.data.fileHash && e.data.success) {
if (onProgress) {
onProgress(100, e.data.method === 'optimized' ? '优化采样' : '完整');
}
resolve(e.data.fileHash);
} else {
reject(new Error('文件哈希计算失败'));
}
};
this.worker.onerror = (err) => {
console.error('Web Worker错误:', err);
// 发生错误时降级到主线程
this.calculateHashInMainThread(file, onProgress)
.then(resolve)
.catch(reject);
};
} else {
// 不支持Worker时在主线程计算
this.calculateHashInMainThread(file, onProgress)
.then(resolve)
.catch(reject);
}
});
}
// 在主线程计算哈希的降级方法
calculateHashInMainThread(file, onProgress) {
return new Promise((resolve) => {
// 对于大文件使用简易哈希
if (file.size > 10 * 1024 * 1024) {
if (onProgress) onProgress(50, '简易');
// 使用文件名、大小、修改时间和随机数创建一个简易哈希
setTimeout(() => {
const simpleHash = `${file.name}-${file.size}-${file.lastModified}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
if (onProgress) onProgress(100, '简易');
resolve(simpleHash);
}, 100);
} else {
// 小文件使用更精确的哈希计算方法
// 加载SparkMD5库(如果外部已加载则省略此步骤)
if (typeof SparkMD5 === 'undefined') {
const script = document.createElement('script');
script.src = './spark-md5.min.js';
document.body.appendChild(script);
script.onload = () => this.computeHashForSmallFile(file, onProgress, resolve);
script.onerror = () => {
const fallbackHash = `${file.name}-${file.size}-${file.lastModified}-${Date.now()}`;
resolve(fallbackHash);
};
} else {
this.computeHashForSmallFile(file, onProgress, resolve);
}
}
});
}
// 为小文件计算哈希
computeHashForSmallFile(file, onProgress, resolve) {
const blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice;
const chunkSize = 2 * 1024 * 1024; // 2MB
const chunks = Math.ceil(file.size / chunkSize);
let currentChunk = 0;
const spark = new SparkMD5.ArrayBuffer();
const fileReader = new FileReader();
fileReader.onload = (e) => {
spark.append(e.target.result);
currentChunk++;
if (currentChunk < chunks) {
if (onProgress) onProgress(Math.floor((currentChunk / chunks) * 100), '主线程');
loadNext();
} else {
if (onProgress) onProgress(100, '主线程');
resolve(spark.end());
}
};
fileReader.onerror = () => {
// 读取错误时使用备用方法
const fallbackHash = `${file.name}-${file.size}-${Date.now()}`;
resolve(fallbackHash);
};
function loadNext() {
const start = currentChunk * chunkSize;
const end = Math.min(start + chunkSize, file.size);
fileReader.readAsArrayBuffer(blobSlice.call(file, start, end));
}
loadNext();
}
// 计算总体上传进度
calculateTotalProgress(fileHash) {
const fileState = this.stateManager.getFileState(fileHash);
if (!fileState) {
return 0;
}
const uploadedChunks = fileState.uploadedChunks;
const totalChunks = Object.keys(uploadedChunks).length;
if (totalChunks === 0) {
return 0;
}
// 计算总进度
let totalProgress = Object.values(uploadedChunks).reduce((sum, progress) => sum + progress, 0);
return Math.floor(totalProgress / totalChunks);
}
// 暂停上传
pauseUpload() {
this.queueManager.pauseAll();
}
// 恢复上传
resumeUpload(fileHash, file, callbacks = {}) {
// 检查状态
const fileState = this.stateManager.getFileState(fileHash);
if (fileState) {
// 重新创建分片
const { chunks } = createFileChunks(file, this.options.chunkSize);
// 继续上传
this.queueManager.addChunksToQueue(chunks, fileHash, callbacks);
} else {
// 没有找到状态,重新上传
this.uploadFile(file, callbacks);
}
}
}
4.7 后端接口设计(Node.js示例)
// 服务端代码示例 (Express.js)
const express = require('express');
const multer = require('multer');
const fs = require('fs');
const path = require('path');
const app = express();
// 配置文件上传
const upload = multer({
dest: path.join(__dirname, 'uploads/chunks')
});
// 接收分片
app.post('/api/upload/chunk', upload.single('chunk'), (req, res) => {
const { hash, chunkIndex, filename, totalChunks } = req.body;
// 确保目录存在
const chunkDir = path.join(__dirname, 'uploads/chunks', hash);
if (!fs.existsSync(chunkDir)) {
fs.mkdirSync(chunkDir, { recursive: true });
}
// 保存分片
const chunkPath = path.join(chunkDir, chunkIndex);
fs.renameSync(req.file.path, chunkPath);
res.json({
success: true,
message: '分片上传成功'
});
});
// 合并分片
app.post('/api/upload/merge', express.json(), async (req, res) => {
const { fileHash, filename } = req.body;
// 分片目录
const chunkDir = path.join(__dirname, 'uploads/chunks', fileHash);
const filePath = path.join(__dirname, 'uploads/files', filename);
// 确保目录存在
if (!fs.existsSync(path.dirname(filePath))) {
fs.mkdirSync(path.dirname(filePath), { recursive: true });
}
try {
// 获取所有分片
const chunks = fs.readdirSync(chunkDir).sort((a, b) => a - b);
// 创建写入流
const writeStream = fs.createWriteStream(filePath);
// 合并分片
for (const chunk of chunks) {
const chunkPath = path.join(chunkDir, chunk);
const buffer = fs.readFileSync(chunkPath);
writeStream.write(buffer);
// 可选:删除分片
fs.unlinkSync(chunkPath);
}
writeStream.end();
// 可选:删除分片目录
fs.rmdirSync(chunkDir);
res.json({
success: true,
message: '文件合并成功',
url: `/uploads/files/${filename}`
});
} catch (error) {
console.error('文件合并错误', error);
res.status(500).json({
success: false,
message: '文件合并失败'
});
}
});
// 验证已上传的分片
app.get('/api/upload/verify', (req, res) => {
const { hash } = req.query;
// 分片目录
const chunkDir = path.join(__dirname, 'uploads/chunks', hash);
if (!fs.existsSync(chunkDir)) {
return res.json({
success: true,
uploaded: false,
chunks: []
});
}
// 获取已上传的分片
const uploadedChunks = fs.readdirSync(chunkDir);
res.json({
success: true,
uploaded: true,
chunks: uploadedChunks
});
});
app.listen(3000, () => {
console.log('服务已启动,监听端口 3000');
});
5. 完整的使用方式
// 前端用法示例
document.addEventListener('DOMContentLoaded', () => {
const fileInput = document.getElementById('file-input');
const uploadBtn = document.getElementById('upload-btn');
const progressDiv = document.getElementById('progress');
// 初始化上传器
const uploader = new FileUploader({
chunkSize: 5 * 1024 * 1024, // 5MB
maxConcurrentUploads: 3
});
uploadBtn.addEventListener('click', () => {
const files = fileInput.files;
if (files.length === 0) return;
Array.from(files).forEach(file => {
// 显示进度UI
const fileProgressElem = document.createElement('div');
fileProgressElem.className = 'file-progress';
fileProgressElem.innerHTML = `
<div class="file-name">${file.name}</div>
<div class="progress-bar">
<div class="progress-inner" style="width: 0%"></div>
</div>
<div class="progress-text">0%</div>
<div class="chunk-progress"></div>
`;
progressDiv.appendChild(fileProgressElem);
const progressInner = fileProgressElem.querySelector('.progress-inner');
const progressText = fileProgressElem.querySelector('.progress-text');
const chunkProgress = fileProgressElem.querySelector('.chunk-progress');
// 开始上传
uploader.uploadFile(file, {
onHashProgress: (progress) => {
progressText.textContent = `计算哈希: ${progress}%`;
},
onChunkProgress: (chunkIndex, progress) => {
const chunkElem = chunkProgress.querySelector(`.chunk-${chunkIndex}`);
if (!chunkElem) {
const elem = document.createElement('div');
elem.className = `chunk-item chunk-${chunkIndex}`;
elem.textContent = `分片${chunkIndex}: 0%`;
chunkProgress.appendChild(elem);
} else {
chunkElem.textContent = `分片${chunkIndex}: ${progress}%`;
}
},
onTotalProgress: (progress) => {
progressInner.style.width = `${progress}%`;
progressText.textContent = `${progress}%`;
},
onComplete: (response) => {
progressText.textContent = '上传完成';
progressInner.style.width = '100%';
progressInner.style.backgroundColor = '#4caf50';
},
onError: (error) => {
progressText.textContent = `错误: ${error}`;
progressInner.style.backgroundColor = '#f44336';
}
});
});
});
});
6. 性能与兼容性优化
6.1 性能优化措施
-
内存优化:
- 分片读取文件避免一次性加载整个文件
- 及时释放分片引用
- 避免冗余的文件副本
-
计算优化:
- Web Worker计算文件哈希,不阻塞UI线程
- 预计算、缓存文件哈希值
- 增量计算大文件特征
-
网络优化:
- 动态调整并发连接数
- 网络状况监测与自适应控制
- 智能分片大小调整
6.2 兼容性处理
-
Web Worker降级处理:
- 检测浏览器是否支持Web Worker
- 不支持时回退到主线程计算
-
本地存储方案:
- localStorage / sessionStorage优先
- 不支持时回退到内存存储
- IndexedDB作为高级存储选项
-
XHR vs Fetch:
- 根据浏览器兼容性选择合适的请求方式
- 提供Promise封装统一接口
7. 异常处理与边缘场景
-
网络波动处理:
- 自动检测断网与重连
- 指数退避重试策略
- 上传状态持久化
-
页面刷新处理:
- 状态实时保存到本地存储
- 页面加载时检查未完成的上传任务
-
大规模批量上传:
- 队列管理避免资源耗尽
- 优先级调度重要文件先上传
- 视窗外文件暂停处理
-
服务器错误处理:
- 错误分类与客户端响应策略
- 服务端异常分片清理机制
8. 用户交互增强体验
8.1 进度报告与可视化优先级管理
/**
* 进度报告与可视化优先级
*/
class UserInterfaceManager {
constructor(options = {}) {
this.options = {
useIntersectionObserver: true,
prioritizeVisibleFiles: true,
showDetailedProgress: true,
...options
};
this.fileElements = new Map(); // 存储文件元素引用
this.observer = null;
if (this.options.useIntersectionObserver && 'IntersectionObserver' in window) {
this.setupVisibilityObserver();
}
}
// 设置可视区域监控
setupVisibilityObserver() {
this.observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
const fileId = entry.target.dataset.fileId;
if (entry.isIntersecting) {
// 文件进入视窗,提高优先级
this.onFileVisible(fileId);
} else {
// 文件离开视窗,降低优先级
this.onFileInvisible(fileId);
}
});
}, {threshold: 0.1});
}
// 注册文件元素
registerFileElement(fileId, element, callbacks = {}) {
this.fileElements.set(fileId, {
element,
callbacks,
isVisible: true,
priority: 'normal'
});
// 添加数据标识
element.dataset.fileId = fileId;
// 如果启用了IntersectionObserver,开始观察
if (this.observer) {
this.observer.observe(element);
}
}
// 文件可见时回调
onFileVisible(fileId) {
const fileData = this.fileElements.get(fileId);
if (fileData) {
fileData.isVisible = true;
fileData.priority = 'high';
// 通知优先级变化
if (fileData.callbacks.onPriorityChange) {
fileData.callbacks.onPriorityChange(fileId, 'high');
}
}
}
// 文件不可见时回调
onFileInvisible(fileId) {
const fileData = this.fileElements.get(fileId);
if (fileData) {
fileData.isVisible = false;
fileData.priority = 'low';
// 通知优先级变化
if (fileData.callbacks.onPriorityChange) {
fileData.callbacks.onPriorityChange(fileId, 'low');
}
}
}
// 更新文件进度
updateFileProgress(fileId, progress, { chunkIndex, chunkProgress } = {}) {
const fileData = this.fileElements.get(fileId);
if (!fileData) return;
const { element } = fileData;
// 更新总进度条
const progressBar = element.querySelector('.progress-inner');
if (progressBar) {
progressBar.style.width = `${progress}%`;
}
// 更新进度文本
const progressText = element.querySelector('.progress-text');
if (progressText) {
progressText.textContent = `${Math.floor(progress)}%`;
}
// 如果提供了分片信息且启用了详细进度
if (chunkIndex !== undefined && chunkProgress !== undefined && this.options.showDetailedProgress) {
this.updateChunkProgress(element, chunkIndex, chunkProgress);
}
}
// 更新分片进度
updateChunkProgress(element, chunkIndex, progress) {
const chunkContainer = element.querySelector('.chunk-progress');
if (!chunkContainer) return;
let chunkElement = chunkContainer.querySelector(`.chunk-${chunkIndex}`);
// 如果分片元素不存在,创建一个
if (!chunkElement) {
chunkElement = document.createElement('div');
chunkElement.className = `chunk-item chunk-${chunkIndex}`;
chunkContainer.appendChild(chunkElement);
// 限制显示的分片数量,避免DOM过大
const maxVisibleChunks = 10;
if (chunkContainer.children.length > maxVisibleChunks) {
// 只保留最近的几个分片显示
const childrenArray = Array.from(chunkContainer.children);
childrenArray.slice(0, childrenArray.length - maxVisibleChunks).forEach(child => {
if (!child.classList.contains('chunk-summary')) {
child.style.display = 'none';
}
});
// 添加或更新摘要信息
let summary = chunkContainer.querySelector('.chunk-summary');
if (!summary) {
summary = document.createElement('div');
summary.className = 'chunk-summary';
chunkContainer.insertBefore(summary, chunkContainer.firstChild);
}
const hiddenCount = childrenArray.length - maxVisibleChunks;
summary.textContent = `${hiddenCount}个早期分片已完成`;
}
}
// 更新分片显示
chunkElement.innerHTML = `
<span class="chunk-label">分片${chunkIndex+1}</span>
<div class="chunk-progress-bar">
<div class="chunk-progress-inner" style="width:${progress}%"></div>
</div>
<span class="chunk-percent">${Math.floor(progress)}%</span>
`;
}
// 更新文件状态
updateFileStatus(fileId, status, message = '') {
const fileData = this.fileElements.get(fileId);
if (!fileData) return;
const { element } = fileData;
// 更新状态标签
const statusElement = element.querySelector('.file-status');
if (statusElement) {
// 移除所有状态类
statusElement.classList.remove(
'status-queued',
'status-uploading',
'status-completed',
'status-failed',
'status-paused'
);
// 添加当前状态类
statusElement.classList.add(`status-${status}`);
// 更新状态文本
statusElement.textContent = this.getStatusText(status, message);
}
// 根据状态更新按钮状态
this.updateActionButtonStates(element, status);
}
// 更新操作按钮状态
updateActionButtonStates(element, status) {
const startBtn = element.querySelector('.start-btn');
const pauseBtn = element.querySelector('.pause-btn');
const cancelBtn = element.querySelector('.cancel-btn');
if (startBtn) {
startBtn.disabled = status !== 'queued' && status !== 'paused';
}
if (pauseBtn) {
pauseBtn.disabled = status !== 'uploading';
}
}
// 获取状态文本
getStatusText(status, message = '') {
const statusTexts = {
'calculating': '计算中',
'queued': '排队中',
'uploading': '上传中',
'paused': '已暂停',
'completed': '已完成',
'error': message || '失败',
'waiting': '等待中'
};
return statusTexts[status] || status;
}
// 清理资源
cleanup() {
if (this.observer) {
this.fileElements.forEach((data, fileId) => {
if (data.element) {
this.observer.unobserve(data.element);
}
});
}
this.fileElements.clear();
}
}
8.2 拖放上传增强
/**
* 增强的拖放上传管理
*/
class EnhancedDragAndDropManager {
constructor(container, options = {}) {
this.container = container;
this.options = {
dropZoneSelector: '#drop-zone',
activeClass: 'drag-active',
hoverClass: 'drag-hover',
onFilesDrop: null,
fileTypes: null,
maxFileSize: null,
...options
};
this.dropZone = this.container.querySelector(this.options.dropZoneSelector);
if (!this.dropZone) {
this.dropZone = this.container;
}
this.setupEventListeners();
}
// 初始化事件监听
setupEventListeners() {
// 阻止浏览器默认行为
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
this.container.addEventListener(eventName, this.preventDefaults, false);
document.body.addEventListener(eventName, this.preventDefaults, false);
});
// 处理拖拽进入区域
this.container.addEventListener('dragenter', this.handleDragEnter.bind(this), false);
document.body.addEventListener('dragenter', this.handleBodyDragEnter.bind(this), false);
// 处理拖拽在区域上方
this.container.addEventListener('dragover', this.handleDragOver.bind(this), false);
// 处理拖拽离开区域
this.container.addEventListener('dragleave', this.handleDragLeave.bind(this), false);
document.body.addEventListener('dragleave', this.handleBodyDragLeave.bind(this), false);
// 处理放下文件
this.dropZone.addEventListener('drop', this.handleDrop.bind(this), false);
}
// 阻止默认行为
preventDefaults(e) {
e.preventDefault();
e.stopPropagation();
}
// 处理拖拽进入容器
handleDragEnter(e) {
this.container.classList.add(this.options.activeClass);
if (this.dropZone) {
this.dropZone.classList.add(this.options.hoverClass);
}
}
// 处理拖拽进入body(用于处理跨容器拖拽)
handleBodyDragEnter(e) {
if (!this.dragEnterCount) {
this.dragEnterCount = 0;
}
this.dragEnterCount++;
if (this.dragEnterCount === 1) {
// 添加整个页面的指示
document.body.classList.add('drag-anywhere');
}
}
// 处理拖拽悬停
handleDragOver(e) {
this.container.classList.add(this.options.activeClass);
if (this.dropZone) {
this.dropZone.classList.add(this.options.hoverClass);
}
}
// 处理拖拽离开
handleDragLeave(e) {
// 仅当鼠标离开容器时才移除类
const rect = this.container.getBoundingClientRect();
const x = e.clientX;
const y = e.clientY;
// 检查鼠标是否真的离开了元素
if (x < rect.left || x >= rect.right || y < rect.top || y >= rect.bottom) {
this.container.classList.remove(this.options.activeClass);
if (this.dropZone) {
this.dropZone.classList.remove(this.options.hoverClass);
}
}
}
// 处理拖拽离开body
handleBodyDragLeave(e) {
this.dragEnterCount--;
if (this.dragEnterCount === 0) {
document.body.classList.remove('drag-anywhere');
}
}
// 处理文件放下
handleDrop(e) {
this.container.classList.remove(this.options.activeClass);
if (this.dropZone) {
this.dropZone.classList.remove(this.options.hoverClass);
}
// 重置全局计数器
this.dragEnterCount = 0;
document.body.classList.remove('drag-anywhere');
const dt = e.dataTransfer;
const files = Array.from(dt.files);
// 如果设置了文件类型限制,进行过滤
let validFiles = files;
if (this.options.fileTypes) {
validFiles = files.filter(file => {
// 检查文件类型
const extension = file.name.split('.').pop().toLowerCase();
return this.options.fileTypes.includes(extension);
});
// 如果有文件被过滤掉,显示警告
if (validFiles.length < files.length) {
this.showTypeWarning(files, validFiles);
}
}
// 如果设置了大小限制,进行过滤
if (this.options.maxFileSize) {
const tooBigFiles = validFiles.filter(file => file.size > this.options.maxFileSize);
if (tooBigFiles.length > 0) {
this.showSizeWarning(tooBigFiles);
validFiles = validFiles.filter(file => file.size <= this.options.maxFileSize);
}
}
// 如果有有效文件,触发回调
if (validFiles.length > 0 && this.options.onFilesDrop) {
this.options.onFilesDrop(validFiles);
}
}
// 显示文件类型警告
showTypeWarning(allFiles, validFiles) {
const invalidCount = allFiles.length - validFiles.length;
console.warn(`${invalidCount}个文件因类型不符合要求而被过滤`);
// 这里可以添加更友好的UI提示
// ...
}
// 显示文件大小警告
showSizeWarning(tooBigFiles) {
const maxSize = this.formatSize(this.options.maxFileSize);
console.warn(`${tooBigFiles.length}个文件超出大小限制(${maxSize})`);
// 这里可以添加更友好的UI提示
// ...
}
// 格式化文件大小
formatSize(bytes) {
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
if (bytes < 1024 * 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
return (bytes / (1024 * 1024 * 1024)).toFixed(1) + ' GB';
}
// 销毁管理器,移除事件监听
destroy() {
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
this.container.removeEventListener(eventName, this.preventDefaults, false);
document.body.removeEventListener(eventName, this.preventDefaults, false);
});
this.container.removeEventListener('dragenter', this.handleDragEnter.bind(this), false);
document.body.removeEventListener('dragenter', this.handleBodyDragEnter.bind(this), false);
this.container.removeEventListener('dragover', this.handleDragOver.bind(this), false);
this.container.removeEventListener('dragleave', this.handleDragLeave.bind(this), false);
document.body.removeEventListener('dragleave', this.handleBodyDragLeave.bind(this), false);
this.dropZone.removeEventListener('drop', this.handleDrop.bind(this), false);
}
}
8.3 自适应布局与响应式设计
/**
* 响应式上传组件
*/
class ResponsiveUploader {
constructor(container, options = {}) {
this.container = container;
this.options = {
breakpoints: {
mobile: 480,
tablet: 768,
desktop: 1024
},
mobileLayout: 'stack', // 'stack', 'compact'
...options
};
this.currentBreakpoint = this.getCurrentBreakpoint();
// 初始化响应式布局
this.initResponsiveLayout();
// 监听屏幕尺寸变化
this.setupResizeListener();
}
// 获取当前断点
getCurrentBreakpoint() {
const width = window.innerWidth;
const { breakpoints } = this.options;
if (width <= breakpoints.mobile) return 'mobile';
if (width <= breakpoints.tablet) return 'tablet';
if (width <= breakpoints.desktop) return 'desktop';
return 'large';
}
// 初始化响应式布局
initResponsiveLayout() {
// 清除现有布局类
['mobile', 'tablet', 'desktop', 'large'].forEach(bp => {
this.container.classList.remove(`uploader-${bp}`);
});
// 添加当前布局类
this.container.classList.add(`uploader-${this.currentBreakpoint}`);
// 应用移动端特殊布局
if (this.currentBreakpoint === 'mobile') {
this.container.classList.add(`mobile-layout-${this.options.mobileLayout}`);
} else {
this.container.classList.remove('mobile-layout-stack', 'mobile-layout-compact');
}
// 调整UI元素
this.adjustUIForBreakpoint();
}
// 根据断点调整UI元素
adjustUIForBreakpoint() {
const fileItems = this.container.querySelectorAll('.file-item');
fileItems.forEach(item => {
const actions = item.querySelector('.file-actions');
const progress = item.querySelector('.progress-container');
const status = item.querySelector('.file-status');
switch (this.currentBreakpoint) {
case 'mobile':
// 移动端布局调整
if (this.options.mobileLayout === 'compact') {
// 紧凑布局 - 行内显示更多信息
actions.classList.add('inline-actions');
item.classList.add('compact-view');
// 仅显示重要按钮
const minorButtons = actions.querySelectorAll('.minor-action');
minorButtons.forEach(btn => btn.classList.add('hidden-mobile'));
} else {
// 堆叠布局 - 垂直排列更清晰
actions.classList.remove('inline-actions');
item.classList.remove('compact-view');
}
break;
case 'tablet':
// 平板布局
actions.classList.add('inline-actions');
item.classList.add('semi-compact');
break;
default:
// 桌面布局 - 完整视图
actions.classList.remove('inline-actions');
item.classList.remove('compact-view', 'semi-compact');
}
});
}
// 设置屏幕尺寸变化监听
setupResizeListener() {
// 使用节流函数避免过多调用
let resizeTimer;
window.addEventListener('resize', () => {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(() => {
const newBreakpoint = this.getCurrentBreakpoint();
if (newBreakpoint !== this.currentBreakpoint) {
this.currentBreakpoint = newBreakpoint;
this.initResponsiveLayout();
}
}, 250);
});
}
}
8.4 智能优先级队列与视窗优化
/**
* 智能上传优先级队列
*/
class SmartUploadQueue {
constructor(options = {}) {
this.options = {
maxConcurrentUploads: 3,
visibilityBasedPriority: true,
chunkBuffer: 2, // 预加载下一批分片的数量
adaptiveConcurrency: true,
networkMonitor: null,
...options
};
this.queue = []; // 待上传的任务
this.activeUploads = new Map(); // 活动的上传任务
this.completedTasks = new Map(); // 已完成的任务
this.priorityMap = new Map(); // 文件优先级映射
// 视窗内文件队列(高优先级)
this.visibleQueue = [];
// 视窗外文件队列(低优先级)
this.backgroundQueue = [];
// 如果提供了网络监测器,使用它
if (this.options.networkMonitor) {
this.options.networkMonitor.onQualityChange = (quality) => {
this.adjustConcurrencyBasedOnNetwork(quality);
};
}
}
// 添加上传任务
addUploadTask(fileId, file, chunks, callbacks = {}) {
// 创建文件任务
const fileTask = {
id: fileId,
file,
chunks: [...chunks], // 复制一份避免引用问题
callbacks,
progress: 0,
remainingChunks: chunks.length,
completedChunks: 0,
priority: 'normal',
state: 'queued'
};
// 添加到适当的队列
this.addToQueue(fileTask);
// 检查是否可以开始上传
this.processQueue();
return fileId;
}
// 添加到队列
addToQueue(fileTask) {
const priority = this.getPriorityForFile(fileTask.id);
fileTask.priority = priority;
if (priority === 'high') {
this.visibleQueue.push(fileTask);
// 高优先级队列按文件大小排序,小文件优先
this.visibleQueue.sort((a, b) => a.file.size - b.file.size);
} else {
this.backgroundQueue.push(fileTask);
}
}
// 处理队列
processQueue() {
if (this.activeUploads.size >= this.options.maxConcurrentUploads) {
return; // 已达到最大并发上传数
}
// 优先处理可视区域内的文件
let taskToProcess = this.visibleQueue.shift() || this.backgroundQueue.shift();
if (!taskToProcess) {
return; // 队列为空
}
// 开始上传任务
this.startUploadTask(taskToProcess);
// 继续处理队列,直到达到并发上限或队列为空
if (this.activeUploads.size < this.options.maxConcurrentUploads) {
this.processQueue();
}
}
// 开始上传任务
startUploadTask(fileTask) {
fileTask.state = 'uploading';
this.activeUploads.set(fileTask.id, fileTask);
// 通知任务开始
if (fileTask.callbacks.onStart) {
fileTask.callbacks.onStart(fileTask.id);
}
// 开始上传分片
this.processNextChunks(fileTask);
}
// 处理下一批分片
processNextChunks(fileTask) {
// 如果任务已暂停或取消,不继续处理
if (fileTask.state !== 'uploading') {
return;
}
// 预加载下一批分片
const chunksToUpload = fileTask.chunks.splice(0, this.options.chunkBuffer);
// 如果没有更多分片,任务已完成
if (chunksToUpload.length === 0) {
if (fileTask.remainingChunks === 0) {
this.completeTask(fileTask);
}
return;
}
// 上传分片
chunksToUpload.forEach(chunk => {
this.uploadChunk(fileTask, chunk);
});
}
// 上传单个分片
uploadChunk(fileTask, chunk) {
// 这里会调用实际的上传方法
// ...上传分片的代码...
// 模拟上传完成
setTimeout(() => {
// 更新进度
fileTask.completedChunks++;
fileTask.remainingChunks--;
fileTask.progress = (fileTask.completedChunks / (fileTask.completedChunks + fileTask.remainingChunks)) * 100;
// 通知进度更新
if (fileTask.callbacks.onProgress) {
fileTask.callbacks.onProgress(fileTask.id, fileTask.progress, {
chunkIndex: chunk.index,
chunkProgress: 100
});
}
// 检查是否需要加载更多分片
if (fileTask.chunks.length > 0 && fileTask.chunks.length <= this.options.chunkBuffer) {
this.processNextChunks(fileTask);
}
// 检查是否已完成
if (fileTask.remainingChunks === 0) {
this.completeTask(fileTask);
}
}, 100 + Math.random() * 900); // 模拟随机上传时间
}
// 完成任务
completeTask(fileTask) {
fileTask.state = 'completed';
this.activeUploads.delete(fileTask.id);
this.completedTasks.set(fileTask.id, fileTask);
// 通知任务完成
if (fileTask.callbacks.onComplete) {
fileTask.callbacks.onComplete(fileTask.id);
}
// 处理下一个任务
this.processQueue();
}
// 设置文件优先级
setFilePriority(fileId, priority) {
this.priorityMap.set(fileId, priority);
// 如果文件正在队列中,更新其位置
this.updateQueuePriorities();
}
// 更新队列优先级
updateQueuePriorities() {
// 重新组织可视和背景队列
const allQueued = [...this.visibleQueue, ...this.backgroundQueue];
this.visibleQueue = [];
this.backgroundQueue = [];
// 根据优先级重新分配
allQueued.forEach(task => {
const priority = this.getPriorityForFile(task.id);
task.priority = priority;
if (priority === 'high') {
this.visibleQueue.push(task);
} else {
this.backgroundQueue.push(task);
}
});
// 排序
this.visibleQueue.sort((a, b) => a.file.size - b.file.size);
}
// 获取文件优先级
getPriorityForFile(fileId) {
return this.priorityMap.get(fileId) || 'normal';
}
// 暂停任务
pauseTask(fileId) {
const activeTask = this.activeUploads.get(fileId);
if (activeTask) {
activeTask.state = 'paused';
this.activeUploads.delete(fileId);
// 重新添加到队列,但放到末尾
if (activeTask.priority === 'high') {
this.visibleQueue.push(activeTask);
} else {
this.backgroundQueue.push(activeTask);
}
// 通知任务暂停
if (activeTask.callbacks.onPause) {
activeTask.callbacks.onPause(fileId);
}
// 检查是否需要开始新任务
this.processQueue();
}
}
// 根据网络质量调整并发数
adjustConcurrencyBasedOnNetwork(quality) {
if (!this.options.adaptiveConcurrency) return;
// quality: 0-1, 0很差,1很好
const minConcurrency = 1;
const maxConcurrency = 8;
this.options.maxConcurrentUploads = Math.floor(
minConcurrency + (maxConcurrency - minConcurrency) * quality
);
// 如果当前活动上传少于新的并发限制,尝试开始新的上传
if (this.activeUploads.size < this.options.maxConcurrentUploads) {
this.processQueue();
}
}
// 根据视窗可见性优化上传
optimizeBasedOnVisibility(fileId, isVisible) {
if (!this.options.visibilityBasedPriority) return;
const priority = isVisible ? 'high' : 'low';
this.setFilePriority(fileId, priority);
// 如果文件不可见且正在上传,考虑暂停它来优先处理可见文件
if (!isVisible && this.activeUploads.has(fileId) && this.visibleQueue.length > 0) {
const activeTask = this.activeUploads.get(fileId);
// 只有当有更高优先级的任务等待时才暂停
if (activeTask && this.visibleQueue.length > 0) {
this.pauseTask(fileId);
}
}
}
}
9. 总结
高性能大文件上传方案通过分片上传、断点续传、并发控制等核心技术,解决了大文件上传的各种痛点。使用Web Worker处理密集型计算,实现了不卡顿的用户体验。本地存储机制确保了上传状态在各种异常场景下的持久化,网络监控与自适应控制保证了上传的稳定性和效率。该方案兼容大多数现代浏览器,能够应对网络中断、页面刷新等各种异常情况,同时支持批量处理,是一套完整、可靠的大文件上传解决方案。