首页 > 其他分享 >js实现大文件分片上传

js实现大文件分片上传

时间:2024-03-13 09:55:39浏览次数:27  
标签:文件 00 bytes js 切片 file 分片 上传

一个上传组件,需要具备的功能:

  • 需要校验文件格式
  • 可以上传任何文件,包括超大的视频文件(切片)
  • 上传期间断网后,再次联网可以继续上传(断点续传)
  • 要有进度条提示
  • 已经上传过同一个文件后,直接上传完成(秒传)

前后端分工:

前端:

  • 文件格式校验
  • 文件切片、md5计算
  • 发起检查请求,把当前文件的hash发送给服务端,检查是否有相同hash的文件
  • 上传进度计算
  • 上传完成后通知后端合并切片

后端:

  • 检查接收到的hash是否有相同的文件,并通知前端当前hash是否有未完成的上传
  • 接收切片
  • 合并所有切片

架构图如下

接下来开始具体实现

一、 格式校验

对于上传的文件,一般来说,我们要校验其格式,仅需要获取文件的后缀(扩展名),即可判断其是否符合我们的上传限制:

?
123456789//文件路径var filePath = "file://upload/test.png";//获取最后一个.的位置var index= filePath.lastIndexOf(".");//获取后缀var ext = filePath.substr(index+1);//输出结果console.log(ext);// 输出: png

但是,这种方式有个弊端,那就是我们可以随便篡改文件的后缀名,比如:test.mp4 ,我们可以通过修改其后缀名:test.mp4 -> test.png ,这样即可绕过限制进行上传。那有没有更严格的限制方式呢?当然是有的。

那就是通过查看文件的二进制数据来识别其真实的文件类型,因为计算机识别文件类型时,并不是真的通过文件的后缀名来识别的,而是通过 “魔数”(Magic Number)来区分,对于某一些类型的文件,起始的几个字节内容都是固定的,根据这几个字节的内容就可以判断文件的类型。借助十六进制编辑器,可以查看一下图片的二进制数据,我们还是以test.png为例:

由上图可知,PNG 类型的图片前 8 个字节是 0x89 50 4E 47 0D 0A 1A 0A。基于这个结果,我们可以据此来做文件的格式校验,以vue项目为例:

?
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354  <template>  <div>    <input      type="file"      id="inputFile"      @change="handleChange"    />  </div></template> <script>export default {  name: "HelloWorld",  methods: {    check(headers) {      return (buffers, options = { offset: 0 }) =>       headers.every(       (header, index) => header === buffers[options.offset + index]       );    },    async handleChange(event) {      const file = event.target.files[0];       // 以PNG为例,只需要获取前8个字节,即可识别其类型      const buffers = await this.readBuffer(file, 0, 8);       const uint8Array = new Uint8Array(buffers);       const isPNG = this.check([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);       // 上传test.png后,打印结果为true      console.log(isPNG(uint8Array))     },    readBuffer(file, start = 0, end = 2) {      // 获取文件的二进制数据,因为我们只需要校验前几个字节即可,所以并不需要获取整个文件的数据        return new Promise((resolve, reject) => {          const reader = new FileReader();           reader.onload = () => {            resolve(reader.result);          };           reader.onerror = reject;           reader.readAsArrayBuffer(file.slice(start, end));        });    }  }};</script>

以上为校验文件类型的方法,对于其他类型的文件,比如mp4,xsl等,大家感兴趣的话,也可以通过工具查看其二进制数据,以此来做格式校验。

以下为汇总的一些文件的二进制标识:

  1.JPEG/JPG - 文件头标识 (2 bytes): ff, d8 文件结束标识 (2 bytes): ff, d9
  2.TGA - 未压缩的前 5 字节 00 00 02 00 00 - RLE 压缩的前 5 字节 00 00 10 00 00
  3.PNG - 文件头标识 (8 bytes) 89 50 4E 47 0D 0A 1A 0A
  4.GIF - 文件头标识 (6 bytes) 47 49 46 38 39(37) 61
  5.BMP - 文件头标识 (2 bytes) 42 4D B M
  6.PCX - 文件头标识 (1 bytes) 0A
  7.TIFF - 文件头标识 (2 bytes) 4D 4D 或 49 49
  8.ICO - 文件头标识 (8 bytes) 00 00 01 00 01 00 20 20
  9.CUR - 文件头标识 (8 bytes) 00 00 02 00 01 00 20 20
  10.IFF - 文件头标识 (4 bytes) 46 4F 52 4D
  11.ANI - 文件头标识 (4 bytes) 52 49 46 46

二、 文件切片

假设我们要把一个1G的视频,分割为每块1MB的切片,可定义 DefualtChunkSize = 1 * 1024 * 1024,通过 spark-md5来计算文件内容的hash值。那如何分割文件呢,使用文件对象File的方法File.prototype.slice即可。

需要注意的是,切割一个较大的文件,比如10G,那分割为1Mb大小的话,将会生成一万个切片,众所周知,js是单线程模型,如果这个计算过程在主线程中的话,那我们的页面必然会直接崩溃,这时,就该我们的 Web Worker 来上场了。

Web Worker 的作用,就是为 JavaScript 创造多线程环境,允许主线程创建 Worker 线程,将一些任务分配给后者运行。在主线程运行的同时,Worker 线程在后台运行,两者互不干扰。具体的作用,不了解的同学可以自行去学些一下。这里就不展开讲了。

以下为部分关键代码:

?
1234567891011  // upload.js   // 创建一个worker对象  const worker = new worker('worker.js')  // 向子线程发送消息,并传入文件对象和切片大小,开始计算分割切片  worker.postMessage(file, DefualtChunkSize)   // 子线程计算完成后,会将切片返回主线程  worker.onmessage = (chunks) => {    ...  }

子线程代码:

?
12345678910111213141516171819202122232425262728293031323334353637383940  // worker.js   // 接收文件对象及切片大小  onmessage (file, DefualtChunkSize) => {    let blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice,      chunks = Math.ceil(file.size / DefualtChunkSize),      currentChunk = 0,      spark = new SparkMD5.ArrayBuffer(),      fileReader = new FileReader();     fileReader.onload = function (e) {      console.log('read chunk nr', currentChunk + 1, 'of');       const chunk = e.target.result;      spark.append(chunk);      currentChunk++;       if (currentChunk < chunks) {        loadNext();      else {        let fileHash = spark.end();        console.info('finished computed hash', fileHash);        // 此处为重点,计算完成后,仍然通过postMessage通知主线程        postMessage({ fileHash, fileReader })      }    };     fileReader.onerror = function () {      console.warn('oops, something went wrong.');    };     function loadNext() {      let start = currentChunk * DefualtChunkSize,        end = ((start + DefualtChunkSize) >= file.size) ? file.size : start + DefualtChunkSize;      let chunk = blobSlice.call(file, start, end);      fileReader.readAsArrayBuffer(chunk);    }     loadNext();  }

以上利用worker线程,我们即可得到计算后的切片,以及md5值。

三、 断点续传 + 秒传 + 上传进度

在拿到切片和md5后,我们首先去服务器查询一下,是否已经存在当前文件。

  • 如果已存在,并且已经是上传成功的文件,则直接返回前端上传成功,即可实现"秒传"。
  • 如果已存在,并且有一部分切片上传失败,则返回给前端已经上传成功的切片name,前端拿到后,根据返回的切片,计算出未上传成功的剩余切片,然后把剩余的切片继续上传,即可实现"断点续传"。
  • 如果不存在,则开始上传,这里需要注意的是,在并发上传切片时,需要控制并发量,避免一次性上传过多切片,导致崩溃。
?
12345678910111213141516171819202122232425262728293031323334// 检查是否已存在相同文件   async function checkAndUploadChunk(chunkList, fileMd5Value) {    const requestList = []    // 如果不存在,则上传    for (let i = 0; i < chunkList; i++) {      requestList.push(upload({ chunkList[i], fileMd5Value, i }))    }     // 并发上传    if (requestList?.length) {      await Promise.all(requestList)    }  }  // 上传chunk  function upload({ chunkList, chunk, fileMd5Value, i }) {    current = 0    let form = new FormData()    form.append("data", chunk) //切片流    form.append("total", chunkList.length) //总片数    form.append("index", i) //当前是第几片         form.append("fileMd5Value", fileMd5Value)    return axios({      method: 'post',      url: BaseUrl + "/upload",      data: form    }).then(({ data }) => {      if (data.stat) {        current = current + 1        // 获取到上传的进度        const uploadPercent = Math.ceil((current / chunkList.length) * 100)      }    })  }

所有切片上传完成后,再向后端发送一个上传完成的请求,即通知后端把所有切片进行合并,最终完成整个上传流程。
大功告成!由于篇幅有限,本文主要讲了前端的实现思路,最终落地成完整的项目,还是需要大家根据真实的项目需求来实现。

标签:文件,00,bytes,js,切片,file,分片,上传
From: https://www.cnblogs.com/mounterLove/p/18066701

相关文章

  • JS ATM练习案例(复习循环知识)
    需求:用户可以选择存钱、取钱、查看余额和退出功能。分析:1循环时反复出现提示框,所以提示框写到循环里面。2.退出的条件是4,所以是4就会结束循环3.提前准备一个金额预存储4取钱为减法操作,存钱为加法操作,查看为直接显示数额。5输入不同的值,可以用switch来执行不同操作。<!D......
  • GDPU JavaWeb JSP基础
    正式走进Javaweb大门,了解jsp及Java在前端的体现。JSP JSP,JavaServerPages是一种基于Java技术的服务器端动态网页技术,允许开发人员在HTML页面中嵌入Java代码。通过JSP,开发人员可以创建包含静态模板和动态内容的网页。当客户端请求一个包含JSP的网页时,服务器会执行其中的J......
  • [js error] SyntaxError: Unexpected token ‘{‘ (at uniFile.js?t=1710138723630:1:
    问题详情问题描述封装一个函数的时候报错问题原因SyntaxError:Unexpectedtoken‘{’(atuniFile.js?t=1710138723630:1:34)SyntaxError:意外的令牌“{”(在uniFile.js?t=1710138723630:1:34)意思是有不符合语法规范的地方在第一行34个字符的地方去到报错文件的地方查......
  • nodejs集成C++代码:手写简单的addon
    文章目录nodejs与node-gyp手写一个简单的addon编写一个简单的binding.gyp文件编写C++源文件V8版本:addon.ccnapi_api版本:addon_api.cc编译命令JS调用在这个专栏里,已经提到过web系统中c++的两大应用场景了:assembly和cef框架的应用,这两个可以说都是客户......
  • ansible-playbook剧本 yaml json jq 学习
    ansible临时命令ad-hocansible中有两种模式,分别是ad-hoc模式和playbook模式ad-hoc简而言之,就是"临时命令"https://docs.ansible.com/ansible/latest/user_guide/intro_adhoc.html临时命令非常适合您很少重复的任务。例如,如果您想在圣诞节假期关闭实验室中的所有机器。Ansib......
  • nodejs base64 转存图片文件
     nodejsbase64转存图片文件在Node.js中,您可以使用内置的fs模块来将Base64编码的字符串转换为图片文件。以下是一个简单的例子,展示如何将Base64字符串转换为PNG图片文件:  constfs=require('fs'); //假设您有一个Base64编码的字符串constbase64Stri......
  • pdf.js源码解析-PDF文件的结构
    为了了解pdf.js源码的详细结构和功能,先来看看PDF的文件结构,然后才能知道pdf.js中的代码是如何解析并且为何这样操作PDF文件的。PDF文件基本是由header、body、trailer组成。header包含了这个PDF的信息,比如PDF的版本,创建时间,以及作者等。body包含了PDF文件的实际内容,比如文本,图片,......
  • jmeter上传文件接口测试
    1.添加线程组  线程组里添加取样器---http请求,手动填写请求方式,请求地址,路径,端口号,协议,请求参数2.查看响应,获取到token值(后续上传接口必须携带token值,做身份校验数据关联)在该请求中添加--后置处理器---正则表达式提取器---提取动态token参数token:自己命名的变量名......
  • js 实现点击下拉滚动
    在JavaScript中实现点击下拉菜单后滚动到特定位置,通常用于导航栏的下拉菜单或对话框内容的自动滚动等场景。以下是一个简单的示例:假设我们有一个HTML结构,包含一个可点击的元素(如按钮)和一个需要滚动的下拉列表:<buttonid="dropdown-toggle">点击打开下拉菜单</button><div......
  • js 获取当前时间后三个月的所有日期list
    //获取三个月后的所有日期functiongetDatesOfLastThreeMonths(){consttoday=newDate();constdates=[];for(leti=0;i<=2;i++){constyear=today.getFullYear();constmonth=today.getMonth()+i;//获取当前月份之前......