web大文件上传技术方案设计

189 阅读13分钟

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 性能优化措施

  1. 内存优化

    • 分片读取文件避免一次性加载整个文件
    • 及时释放分片引用
    • 避免冗余的文件副本
  2. 计算优化

    • Web Worker计算文件哈希,不阻塞UI线程
    • 预计算、缓存文件哈希值
    • 增量计算大文件特征
  3. 网络优化

    • 动态调整并发连接数
    • 网络状况监测与自适应控制
    • 智能分片大小调整

6.2 兼容性处理

  1. Web Worker降级处理

    • 检测浏览器是否支持Web Worker
    • 不支持时回退到主线程计算
  2. 本地存储方案

    • localStorage / sessionStorage优先
    • 不支持时回退到内存存储
    • IndexedDB作为高级存储选项
  3. XHR vs Fetch

    • 根据浏览器兼容性选择合适的请求方式
    • 提供Promise封装统一接口

7. 异常处理与边缘场景

  1. 网络波动处理

    • 自动检测断网与重连
    • 指数退避重试策略
    • 上传状态持久化
  2. 页面刷新处理

    • 状态实时保存到本地存储
    • 页面加载时检查未完成的上传任务
  3. 大规模批量上传

    • 队列管理避免资源耗尽
    • 优先级调度重要文件先上传
    • 视窗外文件暂停处理
  4. 服务器错误处理

    • 错误分类与客户端响应策略
    • 服务端异常分片清理机制

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处理密集型计算,实现了不卡顿的用户体验。本地存储机制确保了上传状态在各种异常场景下的持久化,网络监控与自适应控制保证了上传的稳定性和效率。该方案兼容大多数现代浏览器,能够应对网络中断、页面刷新等各种异常情况,同时支持批量处理,是一套完整、可靠的大文件上传解决方案。