当前位置:网站首页 > 技术博客 > 正文

大文件快传助手



这几天看了关于几篇关于大文件上传的文章,都用到了从零实现了大文件上传中的切片上传、断点续传、秒传、暂停等功能,其实既是面试题,也是在实际业务中会用到的功能,今天我就从0开始写一篇关于大文件上传的详细总结,也是借鉴了以下两篇文章的精髓做出的总结。字节跳动面试官:请你实现一个大文件上传和断点续传 、 全网最全面的"大文件上传"

在实际业务中,甲方爸爸突然说:我要上传一个10G的学习资源👲

你:???💀☠👻💩😅

一般情况下,前端上传文件就是new FormData,然后把文件 append 进去,然后post发送给后端就完事了,但是文件越大,上传的文件也就越长,如果在上传过程中,突然被网管拔了网线,又或者电脑突然中木马了,又或者请求超时,等待过久等等情况,十分影响甲方大哥的体验。

所以这时候就要用到大文件上传了,确保能让用户在上传的时候想停就停(暂停功能),就算断网了也能继续接着上传(断点上传),如果是之前上传过这个文件了(服务器还存着),就不需要做二次上传了(秒传

  • Web Worker (多线程)
  • Vue2 + vue-cli5(基础版功能)
  • Vue3 + Vite(完整版功能)
  • Node + express (后端简单实现)

根据难易程度我写了两个版本的大文件上传功能,vue2是基础版,只有切片上传功能,不做其他复杂功能处理,而vue3会进行全部功能的实现。这样方便理解的同时,可以根据实际业务需求去封装成组件,满足不同场景的需求,同时我会对涉及到的技术知识点进行进一步的讲解,包括 Web Worker 跟 Node.js。

关于 Web Worker 如果想要了解更多可以先参考阮一峰大佬的 Web Worker 使用教程,以及一些具体用例 一文彻底了解Web Worker。

我们都知道,如果让浏览器中JS有大量计算时,会造成 UI 阻塞,出现界面卡顿、掉帧等情况,严重时还会出现页面卡死等情况(其实很容易触发,例如让你计算十万条数据)。

这时候该怎么办呢?该 Web Worker 登场了,在HTML5的新规范中,实现了 Web Worker 来引入 js 的 “多线程” 技术, 可以让我们在页面主运行的 js 线程中,加载运行另外单独的一个或者多个 js 线程。

总结就是: Web Worker专门处理复杂计算的,从此让前端拥有后端的计算能力

所以 Web Worker 对大文件上传来说有什么用呢?假如我们把 10G 的文件要进行切割成每块1MB的小文件,那岂不是会生成10万个切片,所以这时候就需要用到 Web Worker 了。

具体用法在代码中我会解释,又或者可以参考上面两篇文章(注意 webpack4 跟 webpack5 使用方法不一样,具体要看官方文档使用说明,下面我只针对 跟 去使用)

在实现代码之前,先把流程捋一下:首先前端拿到超大文件,需要把文件进行切割分成固定大小的切片,再通过把所有的切片传给后端,后端拿到切片后,处理每一个切片并返回是否处理成功给前端,等把所有的切片都上传完后,后端再把所有的切片合并成一个完整的文件,代表大文件上传完成!

好,那http请求就得用到 ,当然你也可以使用原生 去做请求,以下就直接使用 axios去实现了。

考虑到要把文件切片,那怎么知道每一个文件的唯一标识呢?我们可以用到 去计算文件的。这样如果就算文件同名那它的唯一标识也会不同,又或者文件内容更改后得到的hash值也会不同。

因为是多个切片多个接口请求,那我们怎么判断我们的上传进度呢?有些同学可能会说,可以实现接口请求进度,可以通过去计算。但是! onUploadPrigress返回的进度只是文件切片从前端上传到服务器这一过程的进度而已,并不代表上传到服务器就绝对上传成功了!

image.png

可以看到后端报错造成上传失败,这样计算的进度条也是100%,所以文件完全上传到了服务器 !== 上传成功

但是能不能让后端得到切片并处理完后再返回值通知前端该切片已经上传成功了呢?这样就可以通过 去计算单个文件的上传进度条

也可以通过 去计算。

image.png

切片核心是利用 方法,和数组的 slice 方法相似,文件的 slice 方法可以返回,同时利用 结合 去计算文件hash值。

因为要用到 spark-md5 ,但是Worker内部需要用 去加载其他脚本,所以需要自行下载spark-md5源码单独作为js文件方便在Worker内部引入。(在github源代码中也有)

在Worker内部引入脚本方式

其实 worker 在内部不仅仅可以使用importScripts()引入脚本,也可以使用的引入方式

将 Worker 脚本作为静态资源:

在public中分别新建 跟 文件,至于为什么要放在在public中,worker需要与主线程的脚本文件同源的网络资源,它是相对于你vue项目中的index.html位置去引入的。
image.png

 
  

在中,因为spark-md5跟worker是同目录下,就不需要使用相对路径引入了。

 
  
使用ES6方式引入

在 src 下新建文件夹 再分别新建 跟 文件

image.png

其实我们查看 (vue-cli5内置webpack5)官方文档就可以看到推荐使用引入方式代替了 worker-loader

image.png

但是在使用importScripts()引入脚本时就不能像第一种用法那样了,不然就会

 
  
 
  

以上四种写法都会导致发生同一种错误如下,可以看到在worker同源策略下时浏览器读不到文件,但是如果这两个文件都放在public下,再使用是正常读取到的。但这不是我们想要的

image.png

在阮一峰教程中可以看到以下评论说明,经验证确实可以成功!

image.png

 
  
 
  

所以后续的代码都会使用第二种引入方法,如遇到问题可直接使用第一种。

为了UI美观,项目中使用了element-ui组件库,可自行选择

代码实战

考虑到代码可能过长,就不一一解释所有代码,但会在代码中进行注解,重点部分会单独拿出来说明

 
  
 
  

首先我们要定义几个变量,考虑到用户需要多选上传,就直接定义上传列表为数组,以及请求最大并发数

 
  

在点击触发文件上传事件后

 
  

我们要注意的是,计算文件hash是需要时间的,所以我们的文件上传状态也需要改变,从而对每个文件对象定义了state属性,关于是用于记录所有需要上传的切片数据(指还未上传的),是正在http请求中的个数,是记录后端处理完的切片个数,也是方便计算文件上传进度的;是记录在所有的上传切片列表中,切片上传失败的个数(后面会详细讲)。

如果有细心的同学可以发现,这里用的是循环,一开始的想法是用,但是发现会出现一个现象:就是只能等一个文件解析完才能进行解析下一个,但是我想要的是,在多文件上传中,直接全部一起解析,哪个文件解析完就开始调上传接口即可,所以这里改用成了。

接下来的重点是对Worker的代码详细说明:

 
  

如果你已经看过阮一峰大佬的教程,相信对worker的怎么已经有了初步的了解,但是这样也已经够用了,在worker中我们可以拿到信息以及需要切片的大小,这时候就需要对每个文件进行切片,我们利用进行切割相同等分,并返回全部切片

在拿到我们需要用到以增量方式对文件进行哈希处理(在spark-md5文档有说明),最终可以拿到整个文件的hash值,并发送消息回给调用worker的主线程。但是在处理文件hash值赋值的时候要注意,不同文件名但内容相同的文件哈希值是一样的,因为spark-md5是根据文件内容去获取文件哈希,从而导致可能出现同一个哈希值,所以我们在上面处理文件的时候就要特殊处理!

 
  

下面两个压缩包处理后的hash值是一样的

image.png

在调用接口上传文件之前我们先对axios进行简单的二次封装,以及封装接口信息

 
  
 
  
浏览器同域名同一时间请求的最大并发数

在处理完上面的代码后,我们就可以开始上传了,但是上传切片之前要普及一个知识点,在Chrome浏览器中同域名同一时间请求的最大并发数为6个,一开始是打算使用for循环或者Promise.all()并发请求几百个接口,感觉这样效率高一些,后来发现文件小没事,大了请求直接不响应了,例如15个切片15个请求浏览器会感觉有点卡顿, 几百上千个切片请求瞬间浏览器卡死或者接口超时自动取消!
在这里插入图片描述

所以我们刚才定义的就派上用场了,同时要注意的是要使用搭配去使用(forEach是没有异步效果以及中断的)

 
  

代码解析:在上传前要判断如果还未上传列表为空 || 部分正在上传的切片请求还在请求,则不做处理(或等待正在上传的切片上传完)。

同时要动态设置最大并发数,例如如果有3个文件同时上传/处理中,则每个文件切片接口最多调 6 / 3 == 2个相同的接口。

设置正在上传的请求列表,并把在未上传切片列表中的相对应数据删除,如果把没请求的全部放进请求列表了,则要把未上传切片列表置空。

开始循环调上传接口,但是如果中途取消了某个文件上传,取消状态则不要做任何处理,及时停止

假设在某个切片在上传过程中,突然被网管拔了网线又或者请求超时等,可能会导致某个上传失败/丢失。那怎么办?我们可以选择继续上传该切片,如果连续三次失败则判定该文件上传失败

所以我们就得以后端是否处理成功并返回为标准,防止出现切片丢失等问题,并把该切片标识为上传完成,文件切片上传成功数+1,并动态设置文件上传进度条。

那我们可以根据 判断文件是否已经把所有切片上传完成,然后调接口通知后端,让后端去合并所有的切片并通知前端是否合并完成,这就代表了该文件完全上传成功!

 
  

以上代码就可以实现前端的大文件切片上传功能了!

到了对于node不太熟悉的同学比较苦恼的地方了(例如我),但通过艰苦的学习后,需要用到的插件有:(node框架,在这里主要用于简单的http请求)、(代替node内置fs,主要用于处理文件的写入、读取、删除等)、(用于处理前端传过来的formData参数等)、(用于自动重启Node服务器及响应代码更改,不然你改一次代码就得重新启动一次)

前置工作

新建一个node-server文件夹,里面再分别以及一个跟,以及一个(用于保存文件),

package.json

 
  

v2-file-upload.js

 
  

做完以上工作后,直接在你的端口运行 ,再运行 就可以成功启动Node服务啦!

在中,主要用express启动一个http服务,并设置了允许跨域,以及设置了对于任何路径上的 HTTP OPTIONS 请求,服务器都会发送一个带有 200 状态码的响应。

成功运行:

image.png

image.png

关于跨域访问,可自行在项目中测试即可。

在基础版中只有两个后端接口, 跟 分别就是上传跟合并接口

/upload 接口
 
  

我们已经定义了,利用解析成可识别的绝对路径,以及我们还要动态新建一个用于缓存文件的所有切片的目录(通过代码)

在上面代码中,我们用 去处理文件数据,要注意是传过来的参数、 是传过来时的文件真实路径及内容,创建一个临时缓存目录,并把真实路径的文件移动到临时缓存目录中,移动完成后再返回上传结果。

/merge 接口
 
  

在上面代码中,当前端调用 接口时,先把所有的传参通过解析为 JSON 对象,然后(注意可能出现文件没有后缀的情况),再我们确定好最终的文件路径,也就是

开始合并切片:通过读取临时目录下的所有文件和子目录,这个方法会返回这些文件和子目录的名称形成一个数组,我们要对这个数组,这样能保证我们最终的目录顺序是正确的。再通过,创建可读流并写入,再写入完成后就删除该切片。最终利用等待所有的切片已写入完成且删除了所有的切片文件,最后再通过递归删除缓存切片目录及其内容,就通知前端合并完该切片! 至此整个文件就上传成功啦!

上传测试:可以看到最终上传的文件会存在下,而开头的临时文件夹是用于存储切片(在合并完后会被删除)

image.png

到这里你已经完全掌握了基础版的大文件上传功能!

附上最终的UI效果:

在这里插入图片描述

关于完整版包括了:切片上传 + 断点上传 + 秒传 + 暂停上传 + 恢复上传等功能,关于切片上传这里就不做过多的解释了(已在基础版讲解比较详细),至于为什么用Vue3只是觉得目前Vue3普及度很高,代码写法并无过多的复杂度(这里不使用hooks写法,需要的可自行封装),那就直接开始吧!

关于秒传原理其实就是:在上传文件之前先询问这个文件服务器端是否已存在,若已存在,则不需要上传了,直接让前端显示“已上传成功!”,这就是秒传(是不是很简单!)

所以我们只要在调上传接口之前调用一次检查文件是否存在接口即可!(代码会在断点上传一起放出)

所谓断点上传(又名恢复上传)就是某个文件已经把部分切片上传到后端且保存,但是在上传过程中出现了一些不可预知问题:例如网络中断、超时、不小心刷新了页面等等,会导致上传中断,但是我们又不想再一次把所有切片又上传一次,只要把未上传的切片上传给后端即可

那我们在上传文件之前就要调接口询问后端:我到底上传了哪些切片给你?赶紧给我返回过来,我要过滤掉这些已经传过的,留下没传过的。

 
  

!这里要注意的是 用了reactive做响应式代理,这是因为在 中,如果在 后面直接修改inTaskArrItem某个属性,Vue3并不能实时更新DOM视图,就会导致uploadFileList在视图中不会变化,就算是用也是类似(最后一个inTaskArrItem不会触发DOM更新),所以这里必须要么使用,要么直接修改, 又或者使用赋值重新引用响应式等其中一种方法去修改。

在上面代码中,我们通过接口去获取该文件是否需要再次上传,以及拿到已上传切片列表,如果不需要二次上传,则直接让其显示上传完成并设置上传状态,如果是已上传部分切片,则需要过滤掉已经上传过的切片,同时要注意,可能存在已经上传完所有切片但未合并的情况,这时候就得调一次合并接口即可。同时要注意处理切片数量,因为这在判断是否已经上传完全部切片的时候用到。

关于上面的循环异步导致响应式视图更新问题,可能会抽空测试总结并做一个分享

说到暂停上传,其实就是把还在请求中的接口直接中断。这时候我们就可以用到的方法去取消接口请求,(原生XMLHttpRequest 使用 方法)

image.png

所以我们的上传接口要重新封装一下:

 
  
 
  

在这段代码中

 
  

解析:这里我们在调uploadFile接口的同时,把一个传递作为第二个形参传递给了,而在封装的内部,获取到的第二个形参也就是判断如果为function,则立即执行onCancel这个函数A,在执行的同时把箭头函数作为传参传给函数A,这样在函数A就可以拿到并赋值给当前切片的字段。

这样就可以在主动取消的时候,调用当前切片内部属性,相当于调用该切片接口的进行取消请求

上面的解释可能需要多操作几遍测试去理解

这样我们就可以定义一个暂停事件:

 
  

在暂停事件中,先判断该文件的上传成功失败状态,如果都不属于这两者,则需要判断是否为主动暂停还是被动中断(某切片连续上传失败3次),同时调用取消还在请求中的所有接口。

既然我们都有了暂停上传了,就得有恢复上传吧,其实原理也很简单:只要把刚才暂停的正在上传中所有切片()放到待上传切片列表中(),再去调用单个文件上传方法()就可以啦。

 
  

看到这里,关于完整版的关键要点跟原理已经基本展示出来了,不把全部代码展示是因为跟基础版有重复之处,且过长以及剩余部分并不难,只要弄懂这几个功能需要的原理即可。

全部源码(Vue2 + Vue3 + Node)我会放在文章末尾~

其实完整版就是多了一个检查文件接口,这里也不难,只要根据前端传过来的文件hash值去找到是否有这个文件,以及检查是否存在该文件的切片缓存目录(如果有则返回该目录下的所有切片哈希)

 
  

附上完整版UI效果图(完整版没有用到组件库,可根据实际情况使用)
在这里插入图片描述

经过学习各大佬的文章再经过每一步都经过不断测试完善,对于代码以及原理算是讲的比较清晰明了,若有不足之处可随时评论。

  • 用户刷新页面/打开页面后,没有实现当前页面自动恢复文件列表的上传(该功能因为是存储文件信息,需要用到浏览器的本地缓存功能,但该功能还是不够完美,所以没去实现,可自行了解)
  • 对于文件哈希值的计算,对于大文件就算使用增量读取,但仍很耗时,可以对文件进行,但注意要保证准确度。
  • 对前端文件合并处理,不一定要调接口,可以把这部分放到后端去处理,可以节省前端工作量

若觉得对你有所帮助,希望可以多多 Star⭐ 支持一下~

github:v2v3-large-file-upload

gitee:v2v3-large-file-upload

  • 上一篇: k2p高恪固件评测
  • 下一篇: oauth原理
  • 版权声明


    相关文章:

  • k2p高恪固件评测2025-06-22 08:01:03
  • jvm测试工具2025-06-22 08:01:03
  • amc8历年真题免费2025-06-22 08:01:03
  • openapi3教程2025-06-22 08:01:03
  • 接口详细设计2025-06-22 08:01:03
  • oauth原理2025-06-22 08:01:03
  • sql编程工具2025-06-22 08:01:03
  • 国内免费dns解析服务器2025-06-22 08:01:03
  • 异步fifo使用2025-06-22 08:01:03
  • 微信小程序源码免费下载2025-06-22 08:01:03