JavaScript大文件切片上传实现:原理、秒传与断点续传

作者:闭包旅人

时间:2026-10-26

浏览:384

本文详解JavaScript大文件切片上传的前端实现方案。通过Web Worker异步计算文件Hash解决UI阻塞,利用并发请求分片上传。深入解析文件秒传的Hash验证机制,以及基于AbortController的断点续传(暂停与恢复)原理与代码实现,提供完整的前端处理逻辑。

文章目录

前言

本文主要介绍大文件切片上传的前端基本实现方式,并在此基础上探讨几个关键附加功能。切片上传的核心原理并不复杂,代码中也包含了详细注释,因此不再对基础部分做过多的文字赘述。后续内容将重点解析这些附加功能的实现逻辑,并展示在原有代码基础上的具体改进方案。如有疏漏之处,欢迎指正。

切片后上传

切片上传的基本原理十分直观:首先获取用户选择的文件,将其切割成多个小块,整理好每个切片所需的参数(如索引、Hash值等),然后发起并发请求进行上传。

接下来直接展示相关代码实现。

HTML结构部分

<template>
  <div>
    <input type="file" @change="handleFileChange" />
    <el-button @click="handleUpload">上传</el-button>
  </div>
</template>

JavaScript逻辑部分

<script>
const SIZE = 10 * 1024 * 1024; // 切片大小

export default {
  data: () => ({
    // 存放文件信息
    container: {
      file: null
      hash: null
    },
    data: [] // 用于存放加工好的文件切片列表
    hashPercentage: 0 // 存放hash生成进度
  }),
  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) {
        fileChunkList.push({ file: file.slice(cur, cur + size) });
        cur += size;
      }
      return fileChunkList;
    },
        
    // 生成文件hash    
    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);
          }
        };
      });
    },

   	// 切片加工(上传前预处理 为文件添加hash等)
    async handleUpload() {
      if (!this.container.file) return;
      // 切片生成
      const fileChunkList = this.createFileChunk(this.container.file);
      // hash生成
      this.container.hash = await this.calculateHash(fileChunkList);
      this.data = fileChunkList.map(({ file },index) => ({
        chunk: file,
        // 这里的hash为文件名 + 切片序号,也可以用md5对文件进行加密获取唯一hash值来代替文件名
        hash: this.container.hash + "-" + index
      }));
      await this.uploadChunks();
    }
      
    // 上传切片
   	async uploadChunks() {
     const requestList = this.data
     	// 构造formData
       .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)
       );
     await Promise.all(requestList); // 等待全部切片上传完毕
     await merge(this.container.file.name) // 发送请求合并文件
   	},
  }
};
</script>

生成Hash

无论是前端还是服务端,生成文件和切片的Hash值都是必要的步骤。此前我们可能采用“文件名 + 切片下标”作为Hash,但这种做法存在缺陷:一旦用户修改了文件名,原有的Hash匹配机制就会失效。实际上,只要文件内容未变,其Hash值应当保持不变。因此,我们需要调整Hash的生成规则,改为基于文件内容本身来计算。

为了实现这一目标,我们引入了spark-md5库,它可以根据文件内容计算出标准的Hash值。然而,直接读取超大文件内容并计算Hash会消耗大量时间,导致主线程阻塞,进而引发页面假死。为了解决这个问题,我们采用Web Worker在独立的后台线程中计算Hash,从而保证用户可以在主界面继续进行其他交互操作。

需要注意的是,实例化Web Worker时,其参数必须是JS文件的路径,且不允许跨域。因此,我们需要单独创建一个名为hash.js的文件,并将其放置在public目录下。此外,Worker线程中禁止直接访问DOM元素,但它提供了importScripts函数用于导入外部脚本。我们可以利用该函数将spark-md5库导入到Worker中。

// /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计算hash
      spark.append(e.target.result);
      if (count === fileChunkList.length) {
        self.postMessage({
          percentage: 100,
          // 获取最终hash
          hash: spark.end()
        });
        self.close();
      } else {
        percentage += 100 / fileChunkList.length;
        self.postMessage({
          percentage
        });
        // 递归计算下一个切片
        loadNext(count);
      }
    };
  };
  loadNext(0);
};

小结

  • 获取上传文件
  • 文件切片后,将每个切片存入数组:fileChunkList.push({ file: file.slice(cur, cur + size) });
  • 生成文件Hash(非必须)
  • 根据文件切片列表生成对应的请求列表
  • 并发请求
  • 待全部切片请求完成后,发送合并文件请求

文件秒传

这实际上是一种“障眼法”,旨在提升用户体验,给用户造成文件已存在的错觉。

其原理是:在文件正式上传前,先计算文件的Hash值并发送给后端进行验证。后端检查数据库中是否存在该Hash值,如果存在,则证明该文件之前已经上传过,此时直接提示用户“秒传成功”,无需重复上传文件内容。

// 切片加工(上传前预处理 为文件添加hash等)
async handleUpload() {
  if (!this.container.file) return;
  // 切片生成
  const fileChunkList = this.createFileChunk(this.container.file);
  // hash生成
  this.container.hash = await this.calculateHash(fileChunkList);
    
  // hash验证 (verify为后端验证接口请求)
  const { haveExisetd } = await verify(this.container.hash)
  // 判断
  if(haveExisetd) {
  	this.$message.success("秒传:上传成功") 
    return   
  } 
   
  this.data = fileChunkList.map(({ file },index) => ({
    chunk: file,
    // 这里的hash为文件名 + 切片序号,也可以用md5对文件进行加密获取唯一hash值来代替文件名
    hash: this.container.hash + "-" + index
  }));
  await this.uploadChunks();
}

暂停上传

实现原理是:将所有切片存储在一个数组中,每当一个切片上传完毕,便从数组中移除该项,从而确保数组中仅保留当前正在上传的文件切片。此外,为了实现暂停功能,需要能够中断正在进行的请求。这里可以使用axios库,并通过AbortController对象来中断具体的HTTP请求。

中断请求示例代码

const controller = new AbortController()
axios({
  signal: controller.signal
}).then(() => {});
// 取消请求
controller.abort()

添加暂停上传功能的代码实现

// 上传切片
async uploadChunks() {
 // 需要把requestList放到全局,因为要通过操控requestList来实现中断
 this.requestList = this.data
 	// 构造formData
   .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 }, index) =>
     request(formData).then(() => {
       // 将请求成功的请求剥离出requestList
       this.requestList.splice(index, 1)
     })
   );
 await Promise.all(this.requestList); // 等待全部切片上传完毕
 await merge(this.container.file.name) // 发送请求合并文件
},
// 暂停上传   
handlePause() {
	this.requestList.forEach((req) => {
        // 为每个请求新建一个AbortController实例
     	const controller = new AbortController();
        req.signal = controller.signal
        controller.abort()
    })
}

恢复上传

其原理是:在上传切片之前,先向后端发送请求,获取该文件已上传的切片列表。前端通过比对切片Hash,过滤掉后端已存在的切片,仅对未上传的切片发起请求。

// 切片加工(上传前预处理 为文件添加hash等)
async handleUpload() {
  if (!this.container.file) return;
  // 切片生成
  const fileChunkList = this.createFileChunk(this.container.file);
  // 文件hash生成
  this.container.hash = await this.calculateHash(fileChunkList);
  // hash验证 (verify为后端验证接口请求)
  const { haveExisetd, uploadedList } = await verify(this.container.hash)
  // 判断
  if(haveExisetd) {
  	this.$message.success("秒传:上传成功") 
    return   
  } 
  this.data = fileChunkList.map(({ file },index) => ({
    chunk: file,
    // 注:这个是切片hash   这里的hash为文件名 + 切片序号,也可以用md5对文件进行加密获取唯一hash值来代替文件名
    hash: this.container.hash + "-" + index
  }));
  await this.uploadChunks(uploadedList);
}
// 上传切片
async uploadChunks(uploadedList = []) {
 // 需要把requestList放到全局,因为要通过操控requestList来实现中断
 this.requestList = this.data
    // 过滤出来未上传的切片
   .filter(({ hash }) => !uploadedList.includes(hash))
 	// 构造formData
   .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 }, index) =>
     request(formData).then(() => {
       // 将请求成功的请求剥离出requestList
       this.requestList.splice(index, 1)
     })
   );
 await Promise.all(this.requestList); // 等待全部切片上传完毕
 // 合并之前添加一层验证 验证全部切片传送完毕
 if(uploadedList.length + this.requestList.length == this.data.length){
	await merge(this.container.file.name) // 发送请求合并文件
 }
},
// 暂停上传   
handlePause() {
	this.requestList.forEach((req) => {
        // 为每个请求新建一个AbortController实例
     	const controller = new AbortController();
        req.signal = controller.signal
        controller.abort()
    })
}
// 恢复上传
async handleRecovery() {
    //获取已上传切片列表 (verify为后端验证接口请求)
	const { uploadedList } = await verify(this.container.hash)
    await uploadChunks(uploadedList)
}

添加功能总结

  • 1. 文件秒传本质上是一个简单的验证过程:前端将文件Hash发送给后端,后端验证该文件是否存在并返回结果。若存在,则提示用户秒传成功。
  • 2. 断点续传包含暂停上传和恢复上传两个环节。暂停上传是通过维护一个未上传完毕的切片列表(即从完整列表中剔除已完成请求的切片),并对这些请求执行中断操作来实现的。恢复上传实质上也是一种验证机制:在重新上传前,将文件Hash发送给后端,后端返回已上传完成的切片列表。前端根据切片Hash过滤掉已存在的切片,仅上传剩余未完成的切片。

至此,关于JavaScript大文件切片上传的处理方法介绍完毕。如需了解更多相关JS切片上传内容,建议搜索本站历史文章或继续浏览后续相关文章。感谢大家的支持!

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章