举一反三,如何解决前端大文件上传功能

370 阅读4分钟

以下是针对一次性上传 大文件(10G,100G)的前端方案代码,每一行都附有详细注释,解释其作用和运行逻辑。


1. 文件分片

将大文件分割成固定大小的块(如 5MB),便于分片上传。

/**
 * 将文件分割成固定大小的块
 * @param {File} file - 需要上传的文件
 * @param {number} chunkSize - 每个分片的大小(单位:字节)
 * @returns {Array<Blob>} - 分片后的文件块数组
 */
function splitFile(file, chunkSize = 5 * 1024 * 1024) {
  const chunks = []; // 存储分片后的文件块
  let start = 0; // 分片的起始位置

  // 循环分割文件
  while (start < file.size) {
    // 使用 File.slice() 方法截取文件块
    const chunk = file.slice(start, start + chunkSize);
    chunks.push(chunk); // 将分片添加到数组中
    start += chunkSize; // 更新起始位置
  }

  return chunks;
}

2. 并发控制

通过队列机制控制同时上传的文件数量,避免浏览器崩溃或网络阻塞。

const MAX_CONCURRENT_UPLOADS = 5; // 最大并发上传数
const uploadQueue = []; // 上传队列
let activeUploads = 0; // 当前正在上传的文件数

/**
 * 将文件加入上传队列
 * @param {File} file - 需要上传的文件
 */
function enqueueUpload(file) {
  uploadQueue.push(file); // 将文件加入队列
  processQueue(); // 处理队列中的文件
}

/**
 * 处理上传队列
 */
function processQueue() {
  // 如果当前上传数未达到最大并发数,并且队列中有文件
  if (activeUploads < MAX_CONCURRENT_UPLOADS && uploadQueue.length > 0) {
    const file = uploadQueue.shift(); // 从队列中取出一个文件
    activeUploads++; // 增加当前上传数
    uploadFile(file).finally(() => {
      activeUploads--; // 上传完成后减少当前上传数
      processQueue(); // 继续处理队列中的文件
    });
  }
}

3. 断点续传

在上传前查询已上传的分片,跳过已上传的部分。

/**
 * 上传文件
 * @param {File} file - 需要上传的文件
 */
async function uploadFile(file) {
  const fileId = generateFileId(file); // 生成唯一文件 ID
  const { uploadedChunks } = await getUploadedChunks(fileId); // 获取已上传的分片信息
  const chunks = splitFile(file); // 分片文件

  // 遍历所有分片
  for (let i = 0; i < chunks.length; i++) {
    // 如果分片未上传,则上传
    if (!uploadedChunks.includes(i)) {
      await uploadChunk(fileId, chunks[i], i); // 上传分片
    }
  }

  await completeUpload(fileId); // 通知后端合并分片
}

4. 上传分片

使用 axios 上传分片,并监听上传进度。

/**
 * 上传分片
 * @param {string} fileId - 文件唯一 ID
 * @param {Blob} chunk - 分片文件
 * @param {number} index - 分片索引
 */
function uploadChunk(fileId, chunk, index) {
  const formData = new FormData(); // 创建 FormData 对象
  formData.append('file', chunk); // 添加分片文件
  formData.append('fileId', fileId); // 添加文件 ID
  formData.append('chunkIndex', index); // 添加分片索引

  // 使用 axios 上传分片
  return axios.post('/upload', formData, {
    onUploadProgress: (progressEvent) => {
      // 计算上传进度百分比
      const percent = Math.round((progressEvent.loaded / progressEvent.total) * 100);
      updateProgress(fileId, index, percent); // 更新进度
    }
  });
}

5. 错误处理

捕获上传过程中的错误,并支持重试机制。

/**
 * 上传分片(支持重试)
 * @param {string} fileId - 文件唯一 ID
 * @param {Blob} chunk - 分片文件
 * @param {number} index - 分片索引
 * @param {number} retries - 剩余重试次数
 */
async function uploadChunkWithRetry(fileId, chunk, index, retries = 3) {
  try {
    await uploadChunk(fileId, chunk, index); // 尝试上传分片
  } catch (error) {
    if (retries > 0) {
      // 如果还有重试次数,则重试
      return uploadChunkWithRetry(fileId, chunk, index, retries - 1);
    } else {
      throw error; // 重试次数用尽,抛出错误
    }
  }
}

6. 进度展示

实时展示每个文件的上传进度。

/**
 * 更新上传进度
 * @param {string} fileId - 文件唯一 ID
 * @param {number} index - 分片索引
 * @param {number} percent - 上传进度百分比
 */
function updateProgress(fileId, index, percent) {
  const file = files.find(f => f.id === fileId); // 查找对应的文件
  if (file) {
    file.progress = percent; // 更新进度
    renderProgress(); // 重新渲染进度条
  }
}

7. 用户体验优化

支持拖拽上传和进度条展示。

<div
  id="dropzone"
  ondragover="event.preventDefault()"
  ondrop="handleDrop(event)"
>
  拖拽文件到这里
</div>

<div v-for="file in files" :key="file.id">
  <div>{{ file.name }}</div>
  <progress :value="file.progress" max="100"></progress>
  <div v-if="file.error">
    上传失败: {{ file.error }}
    <button @click="retryUpload(file)">重试</button>
  </div>
</div>
/**
 * 处理文件拖拽事件
 * @param {Event} event - 拖拽事件
 */
function handleDrop(event) {
  event.preventDefault(); // 阻止默认行为
  const files = event.dataTransfer.files; // 获取拖拽的文件
  handleFiles(files); // 处理文件
}

/**
 * 处理文件上传
 * @param {FileList} files - 需要上传的文件列表
 */
function handleFiles(files) {
  for (let i = 0; i < files.length; i++) {
    const file = files[i];
    file.id = generateFileId(file); // 生成唯一文件 ID
    file.progress = 0; // 初始化上传进度
    enqueueUpload(file); // 加入上传队列
  }
}

8. 后端接口设计

后端需要提供以下接口支持分片上传和断点续传:

  1. 获取已上传的分片
    • 请求:GET /uploadedChunks?fileId=123
    • 响应:{ uploadedChunks: [0, 1, 2] }
  2. 上传分片
    • 请求:POST /upload
    • 参数:fileId, chunkIndex, file(分片文件)
  3. 合并分片
    • 请求:POST /completeUpload
    • 参数:fileId

总结

通过以上代码和详细注释,可以清晰地理解如何实现一次性上传 大文件。该方案通过分片上传、并发控制、断点续传和进度展示等技术手段,确保上传过程的高效性和可靠性,同时提供友好的用户体验。