带你一起利用Node剖析大文件上传

好久没有花时间来写一写东西啦,写东西这种事情对我来说是一种比较喜欢的事情,但又因生活琐碎和缺少稳定的动力,要么是把这些抛诸脑后,要么就是停留在幻想的阶段。诸如“嗯!我以后得写一写这个。哎!我以后得写一写那个。”

曾经也把百余篇文章写在自己购买的服务器做的网站里,因一些原因没有去续费导致这些文章也随之销声匿迹了。所以呢,现在就选择一个大平台来写,这样也不至于内容就很容易就毁掉了。

这篇文章想写的主要原因是我觉得网上难有把大文件上传讲的比较通俗易懂的文章,大多都是这抄来那抄去的。所以我就想整一篇独一无二的,虽然我不会去把所有的代码和内容都写进来,只会将关键部分展现出来,但我相信肯定能或多或少对部分看官有所启发。另外,我也相信,如果有的童鞋能够在找开发工作中将面试官引入大文件上传这一块,工资应该也可以涨个千把块。

好了,废话不多说,回到我们文章的主题吧!

相信在生活中,大家都会经常使用网盘去上传一些自己需要保存的文件,那么这里面肯定就会有一些比较大的文件,比如生活中值得纪念的视频,或者觉得非常好玩的游戏等。对于视频和游戏而言,基本上属于比较大的文件了。而这种大文件上传是非常耗时的,可能晴天霹雳一个雷就把家里的电给搞停了,没有电,没有网,你用的这个台式机很不幸就给关机啦,那这个文件辛苦的上传了辣么久,不就白费了吗!?这时候你千万不能气急败坏,伤身体不说,对大厂的程序员那也是不够信任。等到电来了,网络有了,再按一下开机键,我们可以发现刚刚上传的大文件可以继续进行上传,丝毫不影响,就是如此的丝滑。

你认为这就很丝滑了么?非也,有一天你看到习大大的某个采访视频后,备受感染,深受马克思主义的洗礼,决定必须把这个视频给保存到网盘里去,在今后的日子里,一定要反复看,反复学。当你把这个视频文件上传到网盘的时候,会发现这么大的一个文件,嗖的一下就上传完毕了,你可能都觉得有点不可思议,是不是搞错了?不放心的你在网盘目录里找到了习大大的这个视频,神奇的发现居然没有任何问题,哇塞,真的是太神奇了吧。这个网盘太牛皮了,居然可以秒传!那为什么能够秒传让你倍感丝滑呢,其实不仅仅是你一个人备受习大大的熏陶和感染,而是因为早在你之前就有其他的童鞋感受到了习大大的魅力,把这个文件上传到网盘里去啦。网盘一看,噢哟,你这两个是志同道合的人嘛,上传一样的视频,那你就不用再上传一遍啦,已经上传好啦!

在以上实际生活中我们遇到的问题利用专业术语来说就是大文件的分片上传、断点续传、文件秒传。

把大象放进冰箱里只需要三步,实现这些功能其实也只需要三步,第一步就是把所要上传的文件进行分片,第二步把这些分片的文件上传到服务器,第三步把所有分片的文件进行合并还原成最初上传的模样。接下来咱们就一步一步的来实现。

一、将上传的文件进行分片

分片就是把一个大的文件分成若干块,然后一块一块的传输给服务器。

文件分片

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

element-plus文件上传界面

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

http-request钩子函数

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

上传一个文件后控制台的打印信息

我们发现这个数据其实是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到项目中来

接下来,我们封装一个函数,利用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

细心的同学会发现,我这里的multer配置项后面还多了一个fileFilter,但是我没有申明这个函数呀!

因为我发现如果利用fileFilter来实现文件上传的筛选的话,是拿不到前端上传过来的FormData数据的,同样在diskStorage中的filename也是拿不到想要的FormData数据。


很遗憾,没拿到FormData数据

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

API调试工具
前端发送的API请求

于是乎,我看了一些文档,但还是未能找到有效解决方法,当然了,如果有小伙伴能够解决的话,欢迎随时进行指导。

所以,我准备采用multer来实现大文件上传的思路就终止了,是不是感觉很悲伤。

人不能在一颗树上吊死,咱们也不能就非要使用multer来实现,于是乎,我考虑了再三,从multiparty和koa-body中选择了koa-body。

这期间有一个小插曲,主要是我还不死心,我还是想用一用multer,我想用koa-body来帮助一下multer,结果呢,不仅发现帮不了,而且还会报错。我想,这应该是multer和koa-body之间去处理FormData数据时所造成的冲突而产生的报错。

既然选择了koa-body,那么之前用的koa-bodyparser我也给去掉了,再初始化koa-body资源。

初始化koa-body资源

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

解析切片文件名

3、实现文件的断点续传和秒传

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

大文件上传的过程
切片文件上传接口

三、切片文件的合并

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

文件层级示意图
声明一些所需的变量
判断有没有该文件所对应HASH值的文件夹,没有说明肯定没上传过该文件
获取该HASH文件夹下的所有文件,如果文件的个数小于切片应当拥有的数量,那么就抛出文件不完整的错误。反之则将这些文件排序,并获取该文件的后缀名
文件合并的函数,fileList是需要合并的切片数组,fileWriteStream是传入一个文件的写入流[因为我们这是合并一堆切片文件,所以写入流只能是唯一的],mergeDir是文件合并后存放的地址,HASH即文件的HASH值,suffix为文件的后缀名
合并完文件后,删除所有的切片文件和HASH文件夹,并向前端返回文件名和文件访问的地址

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

找准一个文件上传
浏览器一瞬间发送了很多个切片上传请求
服务器里按我们的程序创建了HASH文件夹,并将21个切片文件存放在内
调取一下合并文件的API


最后文件合并成功啦!

四、对于大文件上传的优化建议【主要是太懒啦,而且感觉后续内容都不算很复杂】

1、上传文件的时候可以通过xhr的onProgress来显示上传的进度。

2、网络请求的并发控制。主要原因是大文件HASH计算后,计算HASH没卡,结果一下子那么多请求建立很可能把浏览器给干卡死掉了。解决思路其实也不难,就是我们把异步请求放在一个队列里,比如并发数是5,就先同时发起5个请求,然后有请求结束了,再发起下一个请求即可。

3、服务器碎片文件的定时清理。主要是如果很多人传文件传了一半就离开了,长时间也没有进行再传,那么这些切片可以认为是没有意义的切片,我们可以给它清理掉。

好啦,本篇文章到此结束啦!期待我的下一篇文章【我很期待】,喜欢的话就给我点个赞吧!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345

推荐阅读更多精彩内容