JavaScript实现大文件切片上传与断点续传

Cora
Cora
发布时间:2026-10-15 | 4人浏览过
原创

在 Web 开发中,处理 GB 级大文件上传需要优化用户体验并降低服务器压力。本文通过前端 Vue 示例,展示如何实现文件切片、基于内容生成 Hash 验证、利用 Web Worker 避免阻塞主线程、配合 Axios 实现暂停与恢复,以及断点续传的完整逻辑。

准备环境与基础切片

切片上传的核心在于将一个大文件切割为多个小片段(Chunk),分别发送请求到服务器,最后由服务端合并。首先,我们需要定义切片的固定大小,通常设置在 10MB 左右以平衡并发数与连接开销。

获取文件后,通过 slice 方法切割文件对象。以下是一个基础的 Vue 组件结构,定义了数据容器用于存储文件和切片信息:

<script>
const SIZE = 10 * 1024 * 1024; // 单个切片大小,约 10MB

export default {
  data: () => ({
    container: {
      file: null,
      hash: null,
      worker: null
    },
    data: [], // 存放加工好的文件切片列表
    hashPercentage: 0
  }),
  methods: {
    // 监听文件选择事件
    handleFileChange(e) {
      const [file] = e.target.files;
      if (!file) {
        this.container.file = null;
        return;
      }
      this.container.file = file;
    },

    // 根据指定大小生成切片数组
    createFileChunk(file, size = SIZE) {
      const fileChunkList = [];
      let cur = 0;
      while (cur < file.size) {
        // slice 方法返回 Blob 对象
        fileChunkList.push({ file: file.slice(cur, cur + size) });
        cur += size;
      }
      return fileChunkList;
    },

    async handleUpload() {
      if (!this.container.file) return;
      
      // 1. 生成切片
      const fileChunkList = this.createFileChunk(this.container.file);
      
      // 2. 生成文件唯一 Hash
      this.container.hash = await this.calculateHash(fileChunkList);
      
      // 3. 组装每个切片的数据(包含 Hash 标识和文件名)
      this.data = fileChunkList.map(({ file }, index) => ({
        chunk: file,
        // 这里的 hash 为 '全局文件hash-切片序号'
        hash: this.container.hash + "-" + index
      }));
      
      // 4. 执行上传
      await this.uploadChunks();
    }
  }
};
</script>

利用 Web Worker 计算文件 Hash

为了实现秒传和断点续传,必须拥有一个能准确反映文件内容的唯一标识,即 Hash 值。如果使用文件名+下标作为哈希,一旦用户重命名文件或调整上传顺序,校验将失效。因此需基于文件内容计算 MD5 等哈希值。

由于读取二进制流并计算加密哈希是 CPU 密集型任务,直接在主线程执行会导致页面卡顿(UI 冻结)。解决方案是使用 Web Worker 在后台线程进行计算。

Worker 脚本编写

创建一个独立的 JS 文件(例如 /public/hash.js),在该文件中引入 spark-md5 库,因为 Worker 内部无法直接访问 window 作用域下的变量,需通过 importScripts 导入:

// /public/hash.js
self.importScripts("/spark-md5.min.js");

// 接收主线程发来的切片数据并计算 Hash
self.onmessage = e => {
  const { fileChunkList } = e.data;
  const spark = new self.SparkMD5.ArrayBuffer();
  let percentage = 0;
  let count = 0;

  const loadNext = index => {
    const reader = new FileReader();
    // 异步读取当前切片的二进制数据
    reader.readAsArrayBuffer(fileChunkList[index].file);

    reader.onload = e => {
      count++;
      // 将读取结果追加到 Spark 对象中
      spark.append(e.target.result);

      // 进度更新与递归处理下一个切片
      if (count === fileChunkList.length) {
        self.postMessage({
          percentage: 100,
          hash: spark.end() // 最终生成的完整文件 Hash
        });
        self.close();
      } else {
        percentage += 100 / fileChunkList.length;
        self.postMessage({ percentage });
        loadNext(count);
      }
    };
  };
  loadNext(0);
};

在主线程调用 Worker

在 Vue 实例的方法中实例化 Worker,并通过 postMessage 发送切片列表,监听返回值:

calculateHash(fileChunkList) {
  return new Promise(resolve => {
    this.container.worker = new Worker("/hash.js");
    this.container.worker.postMessage({ fileChunkList });
    
    this.container.worker.onmessage = e => {
      const { percentage, hash } = e.data;
      this.hashPercentage = percentage;
      
      if (hash) {
        resolve(hash);
      }
    };
  });
}

构建并发上传逻辑

当准备好所有切片数据后,构建请求列表。利用 FormData 封装文件片段和对应的元数据(如文件名为防止冲突保留字段),随后使用 Axios 发起并发请求。使用 Promise.all 等待全部切片上传完成后,再通知后端合并文件。

async uploadChunks(uploadedList = []) {
  this.requestList = this.data
    // 如果有已上传列表,过滤掉已存在的切片(用于断点续传)
    .filter(({ hash }) => !uploadedList.includes(hash))
    .map(({ chunk, hash }) => {
      const formData = new FormData();
      formData.append("chunk", chunk);
      formData.append("hash", hash);
      formData.append("filename", this.container.file.name);
      return { formData };
    })
    .map(async ({ formData }) =>
      request(formData) // request 封装了具体的 Axios 调用
    );

  await Promise.all(this.requestList);
  
  // 确认所有剩余切片上传完毕后再触发合并
  await merge(this.container.file.name);
}

实现断点续传与文件秒传

完整的上传流程需要应对网络中断或重复上传场景,这就需要后端接口的配合支持。

1. 文件秒传

原理:在前端计算完文件 Hash 后,先向服务器发送请求验证该 Hash 是否已存在。若存在,证明同文件内容已上传,无需传输数据,直接提示“秒传成功”。

async handleUpload() {
  if (!this.container.file) return;
  const fileChunkList = this.createFileChunk(this.container.file);
  this.container.hash = await this.calculateHash(fileChunkList);

  // 前置检查:查询后端是否已有此文件
  const { haveExisted, uploadedList } = await verify(this.container.hash);
  
  if (haveExisted) {
    this.$message.success("秒传成功,无需上传");
    return;
  }
  
  // ... 后续继续切片上传逻辑
}

2. 暂停与中断上传

利用 Axios 支持的 AbortController 可以在中间任意时刻取消正在进行的 HTTP 请求。为了能够暂停整个批次,我们需要保留每一个请求对象的引用。

// 修改上传函数以暴露请求控制能力
async uploadChunks(uploadedList = []) {
  this.requestList = this.data
    .filter(({ hash }) => !uploadedList.includes(hash))
    .map((item) => {
      const controller = new AbortController(); // 创建控制器
      item.formData = new FormData();
      item.formData.append("chunk", item.chunk);
      item.formData.append("hash", item.hash);
      item.formData.append("filename", this.container.file.name);
      item.signal = controller.signal;
      return item;
    });

  this.requestList = await Promise.all(
    this.requestList.map(async (req) =>
      request(req.formData, req.signal).then(() => {
        // 成功后从列表中移除该项
        this.requestList.splice(this.requestList.indexOf(req), 1);
      }).catch(err => {
         // 忽略被主动中止的错误
         if(err.name !== 'CanceledError') throw err;
      })
    )
  );
}

// 暂停上传动作
handlePause() {
  this.requestList.forEach((req) => {
    if (req && req.signal) {
      const controller = new AbortController();
      req.signal = controller.signal;
      controller.abort(); // 中止请求
    }
  });
}

3. 恢复上传(断点续传)

当用户点击恢复时,前端再次向服务器请求已上传分片的状态接口(假设接口名仍为 verify),获取服务端已成功保存的切片 Hash 列表。

async handleRecovery() {
  // 1. 获取当前状态下后端已有的切片列表
  const { uploadedList } = await verify(this.container.hash);
  
  // 2. 调用带参数的上传函数,自动跳过已上传部分
  await this.uploadChunks(uploadedList);
}

逻辑汇总

  • 切片划分:根据文件大小和预设阈值切割为小块。
  • 唯一标识:通过 Worker 异步计算 MD5,确保重命名不影响校验准确性。
  • 秒传机制:先比对整体文件 Hash,命中则跳过数据传输。
  • 防丢失策略:每次上传前先拉取已存切片列表,前端 .filter() 剔除已完成项,仅补传缺失块。
  • 异常中断:借助 AbortController 释放资源并终止连接。

以上就是基于 JavaScript 实现大文件切片上传、秒传及断点续传的详细内容,更多关于 JavaScript 大文件处理的技巧资料请关注本站其它相关文章!

相关标签:

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表