带你一起利用Node剖析大文件上传
好久没有花时间来写一写东西啦,写东西这种事情对我来说是一种比较喜欢的事情,但又因生活琐碎和缺少稳定的动力,要么是把这些抛诸脑后,要么就是停留在幻想的阶段。诸如“嗯!我以后得写一写这个。哎!我以后得写一写那个。”
曾经也把百余篇文章写在自己购买的服务器做的网站里,因一些原因没有去续费导致这些文章也随之销声匿迹了。所以呢,现在就选择一个大平台来写,这样也不至于内容就很容易就毁掉了。
这篇文章想写的主要原因是我觉得网上难有把大文件上传讲的比较通俗易懂的文章,大多都是这抄来那抄去的。所以我就想整一篇独一无二的,虽然我不会去把所有的代码和内容都写进来,只会将关键部分展现出来,但我相信肯定能或多或少对部分看官有所启发。另外,我也相信,如果有的童鞋能够在找开发工作中将面试官引入大文件上传这一块,工资应该也可以涨个千把块。
好了,废话不多说,回到我们文章的主题吧!
相信在生活中,大家都会经常使用网盘去上传一些自己需要保存的文件,那么这里面肯定就会有一些比较大的文件,比如生活中值得纪念的视频,或者觉得非常好玩的游戏等。对于视频和游戏而言,基本上属于比较大的文件了。而这种大文件上传是非常耗时的,可能晴天霹雳一个雷就把家里的电给搞停了,没有电,没有网,你用的这个台式机很不幸就给关机啦,那这个文件辛苦的上传了辣么久,不就白费了吗!?这时候你千万不能气急败坏,伤身体不说,对大厂的程序员那也是不够信任。等到电来了,网络有了,再按一下开机键,我们可以发现刚刚上传的大文件可以继续进行上传,丝毫不影响,就是如此的丝滑。
你认为这就很丝滑了么?非也,有一天你看到习大大的某个采访视频后,备受感染,深受马克思主义的洗礼,决定必须把这个视频给保存到网盘里去,在今后的日子里,一定要反复看,反复学。当你把这个视频文件上传到网盘的时候,会发现这么大的一个文件,嗖的一下就上传完毕了,你可能都觉得有点不可思议,是不是搞错了?不放心的你在网盘目录里找到了习大大的这个视频,神奇的发现居然没有任何问题,哇塞,真的是太神奇了吧。这个网盘太牛皮了,居然可以秒传!那为什么能够秒传让你倍感丝滑呢,其实不仅仅是你一个人备受习大大的熏陶和感染,而是因为早在你之前就有其他的童鞋感受到了习大大的魅力,把这个文件上传到网盘里去啦。网盘一看,噢哟,你这两个是志同道合的人嘛,上传一样的视频,那你就不用再上传一遍啦,已经上传好啦!
在以上实际生活中我们遇到的问题利用专业术语来说就是大文件的分片上传、断点续传、文件秒传。
把大象放进冰箱里只需要三步,实现这些功能其实也只需要三步,第一步就是把所要上传的文件进行分片,第二步把这些分片的文件上传到服务器,第三步把所有分片的文件进行合并还原成最初上传的模样。接下来咱们就一步一步的来实现。
一、将上传的文件进行分片
分片就是把一个大的文件分成若干块,然后一块一块的传输给服务器。

首先我们利用一些UI框架快速的搭建一个上传文件的页面,如下图所示,我这边选用的是elementPlus框架快速生成的文件上传界面。

为了覆盖element-plus默认的XHR请求行为,我们利用http-request钩子函数进行覆盖,下图是其官网的相应解释。如果利用其它的UI框架,也需参考对应框架的官方文档。

这个钩子函数的第一个参数就是存放的一些请求信息,我们可以在控制台打印出来瞅一瞅这是啥。

我们发现这个数据其实是element-plus封装好了的数据信息,有它所需的配置参数,比如action、data、headers、method等,同时其中也包括我们上传的文件信息,比如可以知道文件的name、size、type等信息,我们继续展开文件信息的原型链,可以发现其中有一个slice方法,看到了slice方法,是不是突然眼前一亮嘞。既然眼前一亮了,那就开干写代码吧!

二、将分片后的文件逐个上传给服务器
在第一步的时候我们已经将文件进行切片存放在chunks数组当中,那么上传给服务器则只需遍历该数组,发送请求即可。
我们知道,上传文件可以采用Base64或者FormData的方式,由于Base64的局限性,一般只上传图片文件才使用,我们本次是上传大文件,所以必定选择的是FormData。那么各位如果选择的是原生js或者其他的框架的话,可能需要指定一下文件上传时候的Content-Type为multipart/form-data。

emm,看起来好像并不复杂嘛,so easy?有没有觉得哪里有问题呢?咱们思考一下,我们把文件是分片上传给了后台服务器,但是上传给服务器后这些文件叫啥名字呢?按时间戳来取名字么?还是按一些后台上传模块内部的机制自动生成一些名字?如果是自动生成名字的话,这些名字的规律是不是可控的呢?
除了名字的问题,还要考虑到断点上传的功能,生活中,我们网断了,文件下一次上传可以直接定位到之前上传了的进度,这就是断点续传,甚至可以秒传。断点续传就是只上传了部分切片内容,下一次再上传的时候,已经上传了的切片内容是不需要重新再上传的,所以问题就来了,我们要咋样才能知道这个文件有没有上传呢?
1、利用spark-md5来计算文件的HASH值
spark-md5简单理解就是可以根据文件的内容来计算出特定的值,也就是说只要文件内容是一样的,那么最后生成的这个特定的值也是一样的,这个特定的值也是唯一的。比如说,电脑里有一个文件为“我的照片.jpg”,有一天,你觉得这个照片的名字不符合你的气质,于是乎,你将其改为“最帅的人.jpg”,但是无论你把这张图片的名字怎么改,里面的内容是不变化的,也就是它们生成的HASH最后都是一样的。
显然,对于上面我们提出的问题也就可以解决了。你不是要起名麦,那简单,在上传的时候,通过spark-md5来计算出文件的HASH值,分片的时候就按照"HASH_分片的序号"来命名。

接下来,我们封装一个函数,利用spark-md5来生成文件的HASH,最后返回一些可能需要的信息,比如HASH值,文件的后缀名,利用HASH命名的文件名以及文件的buffer等。

在上述封装的函数中,一开始是通过FileReader将文件转换为buffer对象,然后创建一个spark-md5的buffer缓冲区,最后得到文件的HASH值。文件的后缀名通过正则表达式来获取到,最后的文件名自然也就是将HASH值与文件后缀名做拼接啦!
2、实现文件上传的接口
服务器的搭建,我采用的是Koa,如果直接只利用koa-router写个路由的话,想直接快速拿到前端上传FormData格式的数据是比较困难的。又要解析数据,又是文件上传,我第一时间想到的是multer。
废话不多说,咱们执行npm install @koa/multer multer --save命令,const multer = require('@koa/multer')引入咱们的multer,初始化咱们的multer资源。
因为文件上传后咱们是要将其放在一个目录下,于是我们设想,如果是上传大文件的话,咱们就把它存放在public/uploads文件夹下,如果是其他的文件,咱们就根据当前的时间再生成一个文件夹,将这些文件放在这个文件夹里。

然后我们通过multer.diskStorage来配置multer,利用destination来生成存放目录。

细心的同学会发现,我这里的multer配置项后面还多了一个fileFilter,但是我没有申明这个函数呀!
因为我发现如果利用fileFilter来实现文件上传的筛选的话,是拿不到前端上传过来的FormData数据的,同样在diskStorage中的filename也是拿不到想要的FormData数据。

不过我发现如果用PostMan、ApiPost相关的API调试工具发送的话,是能够拿到数据的。我检查了一下前端的Content-Type,也没发现有有哪里不对。


于是乎,我看了一些文档,但还是未能找到有效解决方法,当然了,如果有小伙伴能够解决的话,欢迎随时进行指导。
所以,我准备采用multer来实现大文件上传的思路就终止了,是不是感觉很悲伤。
人不能在一颗树上吊死,咱们也不能就非要使用multer来实现,于是乎,我考虑了再三,从multiparty和koa-body中选择了koa-body。
这期间有一个小插曲,主要是我还不死心,我还是想用一用multer,我想用koa-body来帮助一下multer,结果呢,不仅发现帮不了,而且还会报错。我想,这应该是multer和koa-body之间去处理FormData数据时所造成的冲突而产生的报错。
既然选择了koa-body,那么之前用的koa-bodyparser我也给去掉了,再初始化koa-body资源。

然后我写了一个parseFileName的公共函数,目的是根据传递过来的切片文件名,返回对应的HASH值和切片索引。

3、实现文件的断点续传和秒传
文件的断点续传和秒传其实就是已经知道分片文件上传过了之后,不让它再重新传了。比如下图中,我们计算得到一个大文件的HASH值,然后将其切片为5份,判断服务器中是否存在某个切片的时候,可以直接查找文件所存储的路径,判断是否有该HASH所对应的文件切片名存在,如果存在,那么就说明这个切片已经上传过,是不用再进行上传的,反之,则进行上传。


三、切片文件的合并
咱们已经把大象放进冰箱啦,就差关闭冰箱门啦。我精心画了一个文件层级图,相信这样更容易理解我处理文件的逻辑。首先,我们将文件存放在public/upload目录下,用当前文件的HASH值创建了一个临时文件夹,将该文件的所有切片都放在这个HASH命名的文件夹下。然后当我们文件合并输出放在upload文件夹下,成功合并后删除用HASH命名的文件夹及其所有内容。






大象放进冰箱全部都搞定啦,我们一起来看看结果如何!





四、对于大文件上传的优化建议【主要是太懒啦,而且感觉后续内容都不算很复杂】
1、上传文件的时候可以通过xhr的onProgress来显示上传的进度。
2、网络请求的并发控制。主要原因是大文件HASH计算后,计算HASH没卡,结果一下子那么多请求建立很可能把浏览器给干卡死掉了。解决思路其实也不难,就是我们把异步请求放在一个队列里,比如并发数是5,就先同时发起5个请求,然后有请求结束了,再发起下一个请求即可。
3、服务器碎片文件的定时清理。主要是如果很多人传文件传了一半就离开了,长时间也没有进行再传,那么这些切片可以认为是没有意义的切片,我们可以给它清理掉。
好啦,本篇文章到此结束啦!期待我的下一篇文章【我很期待】,喜欢的话就给我点个赞吧!