帶你一起利用Node剖析大文件上傳

好久沒(méi)有花時(shí)間來(lái)寫(xiě)一寫(xiě)東西啦,寫(xiě)東西這種事情對(duì)我來(lái)說(shuō)是一種比較喜歡的事情,但又因生活瑣碎和缺少穩(wěn)定的動(dòng)力,要么是把這些拋諸腦后,要么就是停留在幻想的階段。諸如“嗯!我以后得寫(xiě)一寫(xiě)這個(gè)。哎!我以后得寫(xiě)一寫(xiě)那個(gè)。”

曾經(jīng)也把百余篇文章寫(xiě)在自己購(gòu)買的服務(wù)器做的網(wǎng)站里,因一些原因沒(méi)有去續(xù)費(fèi)導(dǎo)致這些文章也隨之銷聲匿跡了。所以呢,現(xiàn)在就選擇一個(gè)大平臺(tái)來(lái)寫(xiě),這樣也不至于內(nèi)容就很容易就毀掉了。

這篇文章想寫(xiě)的主要原因是我覺(jué)得網(wǎng)上難有把大文件上傳講的比較通俗易懂的文章,大多都是這抄來(lái)那抄去的。所以我就想整一篇獨(dú)一無(wú)二的,雖然我不會(huì)去把所有的代碼和內(nèi)容都寫(xiě)進(jìn)來(lái),只會(huì)將關(guān)鍵部分展現(xiàn)出來(lái),但我相信肯定能或多或少對(duì)部分看官有所啟發(fā)。另外,我也相信,如果有的童鞋能夠在找開(kāi)發(fā)工作中將面試官引入大文件上傳這一塊,工資應(yīng)該也可以漲個(gè)千把塊。

好了,廢話不多說(shuō),回到我們文章的主題吧!

相信在生活中,大家都會(huì)經(jīng)常使用網(wǎng)盤去上傳一些自己需要保存的文件,那么這里面肯定就會(huì)有一些比較大的文件,比如生活中值得紀(jì)念的視頻,或者覺(jué)得非常好玩的游戲等。對(duì)于視頻和游戲而言,基本上屬于比較大的文件了。而這種大文件上傳是非常耗時(shí)的,可能晴天霹靂一個(gè)雷就把家里的電給搞停了,沒(méi)有電,沒(méi)有網(wǎng),你用的這個(gè)臺(tái)式機(jī)很不幸就給關(guān)機(jī)啦,那這個(gè)文件辛苦的上傳了辣么久,不就白費(fèi)了嗎?。窟@時(shí)候你千萬(wàn)不能氣急敗壞,傷身體不說(shuō),對(duì)大廠的程序員那也是不夠信任。等到電來(lái)了,網(wǎng)絡(luò)有了,再按一下開(kāi)機(jī)鍵,我們可以發(fā)現(xiàn)剛剛上傳的大文件可以繼續(xù)進(jìn)行上傳,絲毫不影響,就是如此的絲滑。

你認(rèn)為這就很絲滑了么?非也,有一天你看到習(xí)大大的某個(gè)采訪視頻后,備受感染,深受馬克思主義的洗禮,決定必須把這個(gè)視頻給保存到網(wǎng)盤里去,在今后的日子里,一定要反復(fù)看,反復(fù)學(xué)。當(dāng)你把這個(gè)視頻文件上傳到網(wǎng)盤的時(shí)候,會(huì)發(fā)現(xiàn)這么大的一個(gè)文件,嗖的一下就上傳完畢了,你可能都覺(jué)得有點(diǎn)不可思議,是不是搞錯(cuò)了?不放心的你在網(wǎng)盤目錄里找到了習(xí)大大的這個(gè)視頻,神奇的發(fā)現(xiàn)居然沒(méi)有任何問(wèn)題,哇塞,真的是太神奇了吧。這個(gè)網(wǎng)盤太牛皮了,居然可以秒傳!那為什么能夠秒傳讓你倍感絲滑呢,其實(shí)不僅僅是你一個(gè)人備受習(xí)大大的熏陶和感染,而是因?yàn)樵缭谀阒熬陀衅渌耐惺艿搅肆?xí)大大的魅力,把這個(gè)文件上傳到網(wǎng)盤里去啦。網(wǎng)盤一看,噢喲,你這兩個(gè)是志同道合的人嘛,上傳一樣的視頻,那你就不用再上傳一遍啦,已經(jīng)上傳好啦!

在以上實(shí)際生活中我們遇到的問(wèn)題利用專業(yè)術(shù)語(yǔ)來(lái)說(shuō)就是大文件的分片上傳、斷點(diǎn)續(xù)傳、文件秒傳。

把大象放進(jìn)冰箱里只需要三步,實(shí)現(xiàn)這些功能其實(shí)也只需要三步,第一步就是把所要上傳的文件進(jìn)行分片,第二步把這些分片的文件上傳到服務(wù)器,第三步把所有分片的文件進(jìn)行合并還原成最初上傳的模樣。接下來(lái)咱們就一步一步的來(lái)實(shí)現(xiàn)。

一、將上傳的文件進(jìn)行分片

分片就是把一個(gè)大的文件分成若干塊,然后一塊一塊的傳輸給服務(wù)器。

文件分片

首先我們利用一些UI框架快速的搭建一個(gè)上傳文件的頁(yè)面,如下圖所示,我這邊選用的是elementPlus框架快速生成的文件上傳界面。

element-plus文件上傳界面

為了覆蓋element-plus默認(rèn)的XHR請(qǐng)求行為,我們利用http-request鉤子函數(shù)進(jìn)行覆蓋,下圖是其官網(wǎng)的相應(yīng)解釋。如果利用其它的UI框架,也需參考對(duì)應(yīng)框架的官方文檔。

http-request鉤子函數(shù)

這個(gè)鉤子函數(shù)的第一個(gè)參數(shù)就是存放的一些請(qǐng)求信息,我們可以在控制臺(tái)打印出來(lái)瞅一瞅這是啥。

上傳一個(gè)文件后控制臺(tái)的打印信息

我們發(fā)現(xiàn)這個(gè)數(shù)據(jù)其實(shí)是element-plus封裝好了的數(shù)據(jù)信息,有它所需的配置參數(shù),比如action、data、headers、method等,同時(shí)其中也包括我們上傳的文件信息,比如可以知道文件的name、size、type等信息,我們繼續(xù)展開(kāi)文件信息的原型鏈,可以發(fā)現(xiàn)其中有一個(gè)slice方法,看到了slice方法,是不是突然眼前一亮嘞。既然眼前一亮了,那就開(kāi)干寫(xiě)代碼吧!

將上傳的文件分片

二、將分片后的文件逐個(gè)上傳給服務(wù)器

在第一步的時(shí)候我們已經(jīng)將文件進(jìn)行切片存放在chunks數(shù)組當(dāng)中,那么上傳給服務(wù)器則只需遍歷該數(shù)組,發(fā)送請(qǐng)求即可。

我們知道,上傳文件可以采用Base64或者FormData的方式,由于Base64的局限性,一般只上傳圖片文件才使用,我們本次是上傳大文件,所以必定選擇的是FormData。那么各位如果選擇的是原生js或者其他的框架的話,可能需要指定一下文件上傳時(shí)候的Content-Type為multipart/form-data。

將分片文件上傳給服務(wù)器

emm,看起來(lái)好像并不復(fù)雜嘛,so easy?有沒(méi)有覺(jué)得哪里有問(wèn)題呢?咱們思考一下,我們把文件是分片上傳給了后臺(tái)服務(wù)器,但是上傳給服務(wù)器后這些文件叫啥名字呢?按時(shí)間戳來(lái)取名字么?還是按一些后臺(tái)上傳模塊內(nèi)部的機(jī)制自動(dòng)生成一些名字?如果是自動(dòng)生成名字的話,這些名字的規(guī)律是不是可控的呢?

除了名字的問(wèn)題,還要考慮到斷點(diǎn)上傳的功能,生活中,我們網(wǎng)斷了,文件下一次上傳可以直接定位到之前上傳了的進(jìn)度,這就是斷點(diǎn)續(xù)傳,甚至可以秒傳。斷點(diǎn)續(xù)傳就是只上傳了部分切片內(nèi)容,下一次再上傳的時(shí)候,已經(jīng)上傳了的切片內(nèi)容是不需要重新再上傳的,所以問(wèn)題就來(lái)了,我們要咋樣才能知道這個(gè)文件有沒(méi)有上傳呢?

1、利用spark-md5來(lái)計(jì)算文件的HASH值

spark-md5簡(jiǎn)單理解就是可以根據(jù)文件的內(nèi)容來(lái)計(jì)算出特定的值,也就是說(shuō)只要文件內(nèi)容是一樣的,那么最后生成的這個(gè)特定的值也是一樣的,這個(gè)特定的值也是唯一的。比如說(shuō),電腦里有一個(gè)文件為“我的照片.jpg”,有一天,你覺(jué)得這個(gè)照片的名字不符合你的氣質(zhì),于是乎,你將其改為“最帥的人.jpg”,但是無(wú)論你把這張圖片的名字怎么改,里面的內(nèi)容是不變化的,也就是它們生成的HASH最后都是一樣的。

顯然,對(duì)于上面我們提出的問(wèn)題也就可以解決了。你不是要起名麥,那簡(jiǎn)單,在上傳的時(shí)候,通過(guò)spark-md5來(lái)計(jì)算出文件的HASH值,分片的時(shí)候就按照"HASH_分片的序號(hào)"來(lái)命名。

引入spark-md5到項(xiàng)目中來(lái)

接下來(lái),我們封裝一個(gè)函數(shù),利用spark-md5來(lái)生成文件的HASH,最后返回一些可能需要的信息,比如HASH值,文件的后綴名,利用HASH命名的文件名以及文件的buffer等。

封裝的函數(shù)

在上述封裝的函數(shù)中,一開(kāi)始是通過(guò)FileReader將文件轉(zhuǎn)換為buffer對(duì)象,然后創(chuàng)建一個(gè)spark-md5的buffer緩沖區(qū),最后得到文件的HASH值。文件的后綴名通過(guò)正則表達(dá)式來(lái)獲取到,最后的文件名自然也就是將HASH值與文件后綴名做拼接啦!

2、實(shí)現(xiàn)文件上傳的接口

服務(wù)器的搭建,我采用的是Koa,如果直接只利用koa-router寫(xiě)個(gè)路由的話,想直接快速拿到前端上傳FormData格式的數(shù)據(jù)是比較困難的。又要解析數(shù)據(jù),又是文件上傳,我第一時(shí)間想到的是multer。

廢話不多說(shuō),咱們執(zhí)行npm install @koa/multer multer --save命令,const multer = require('@koa/multer')引入咱們的multer,初始化咱們的multer資源。

因?yàn)槲募蟼骱笤蹅兪且獙⑵浞旁谝粋€(gè)目錄下,于是我們?cè)O(shè)想,如果是上傳大文件的話,咱們就把它存放在public/uploads文件夾下,如果是其他的文件,咱們就根據(jù)當(dāng)前的時(shí)間再生成一個(gè)文件夾,將這些文件放在這個(gè)文件夾里。

文件存放路徑的函數(shù)

然后我們通過(guò)multer.diskStorage來(lái)配置multer,利用destination來(lái)生成存放目錄。

配置multer

細(xì)心的同學(xué)會(huì)發(fā)現(xiàn),我這里的multer配置項(xiàng)后面還多了一個(gè)fileFilter,但是我沒(méi)有申明這個(gè)函數(shù)呀!

因?yàn)槲野l(fā)現(xiàn)如果利用fileFilter來(lái)實(shí)現(xiàn)文件上傳的篩選的話,是拿不到前端上傳過(guò)來(lái)的FormData數(shù)據(jù)的,同樣在diskStorage中的filename也是拿不到想要的FormData數(shù)據(jù)。


很遺憾,沒(méi)拿到FormData數(shù)據(jù)

不過(guò)我發(fā)現(xiàn)如果用PostMan、ApiPost相關(guān)的API調(diào)試工具發(fā)送的話,是能夠拿到數(shù)據(jù)的。我檢查了一下前端的Content-Type,也沒(méi)發(fā)現(xiàn)有有哪里不對(duì)。

API調(diào)試工具
前端發(fā)送的API請(qǐng)求

于是乎,我看了一些文檔,但還是未能找到有效解決方法,當(dāng)然了,如果有小伙伴能夠解決的話,歡迎隨時(shí)進(jìn)行指導(dǎo)。

所以,我準(zhǔn)備采用multer來(lái)實(shí)現(xiàn)大文件上傳的思路就終止了,是不是感覺(jué)很悲傷。

人不能在一顆樹(shù)上吊死,咱們也不能就非要使用multer來(lái)實(shí)現(xiàn),于是乎,我考慮了再三,從multiparty和koa-body中選擇了koa-body。

這期間有一個(gè)小插曲,主要是我還不死心,我還是想用一用multer,我想用koa-body來(lái)幫助一下multer,結(jié)果呢,不僅發(fā)現(xiàn)幫不了,而且還會(huì)報(bào)錯(cuò)。我想,這應(yīng)該是multer和koa-body之間去處理FormData數(shù)據(jù)時(shí)所造成的沖突而產(chǎn)生的報(bào)錯(cuò)。

既然選擇了koa-body,那么之前用的koa-bodyparser我也給去掉了,再初始化koa-body資源。

初始化koa-body資源

然后我寫(xiě)了一個(gè)parseFileName的公共函數(shù),目的是根據(jù)傳遞過(guò)來(lái)的切片文件名,返回對(duì)應(yīng)的HASH值和切片索引。

解析切片文件名

3、實(shí)現(xiàn)文件的斷點(diǎn)續(xù)傳和秒傳

文件的斷點(diǎn)續(xù)傳和秒傳其實(shí)就是已經(jīng)知道分片文件上傳過(guò)了之后,不讓它再重新傳了。比如下圖中,我們計(jì)算得到一個(gè)大文件的HASH值,然后將其切片為5份,判斷服務(wù)器中是否存在某個(gè)切片的時(shí)候,可以直接查找文件所存儲(chǔ)的路徑,判斷是否有該HASH所對(duì)應(yīng)的文件切片名存在,如果存在,那么就說(shuō)明這個(gè)切片已經(jīng)上傳過(guò),是不用再進(jìn)行上傳的,反之,則進(jìn)行上傳。

大文件上傳的過(guò)程
切片文件上傳接口

三、切片文件的合并

咱們已經(jīng)把大象放進(jìn)冰箱啦,就差關(guān)閉冰箱門啦。我精心畫(huà)了一個(gè)文件層級(jí)圖,相信這樣更容易理解我處理文件的邏輯。首先,我們將文件存放在public/upload目錄下,用當(dāng)前文件的HASH值創(chuàng)建了一個(gè)臨時(shí)文件夾,將該文件的所有切片都放在這個(gè)HASH命名的文件夾下。然后當(dāng)我們文件合并輸出放在upload文件夾下,成功合并后刪除用HASH命名的文件夾及其所有內(nèi)容。

文件層級(jí)示意圖
聲明一些所需的變量
判斷有沒(méi)有該文件所對(duì)應(yīng)HASH值的文件夾,沒(méi)有說(shuō)明肯定沒(méi)上傳過(guò)該文件
獲取該HASH文件夾下的所有文件,如果文件的個(gè)數(shù)小于切片應(yīng)當(dāng)擁有的數(shù)量,那么就拋出文件不完整的錯(cuò)誤。反之則將這些文件排序,并獲取該文件的后綴名
文件合并的函數(shù),fileList是需要合并的切片數(shù)組,fileWriteStream是傳入一個(gè)文件的寫(xiě)入流[因?yàn)槲覀冞@是合并一堆切片文件,所以寫(xiě)入流只能是唯一的],mergeDir是文件合并后存放的地址,HASH即文件的HASH值,suffix為文件的后綴名
合并完文件后,刪除所有的切片文件和HASH文件夾,并向前端返回文件名和文件訪問(wèn)的地址

大象放進(jìn)冰箱全部都搞定啦,我們一起來(lái)看看結(jié)果如何!

找準(zhǔn)一個(gè)文件上傳
瀏覽器一瞬間發(fā)送了很多個(gè)切片上傳請(qǐng)求
服務(wù)器里按我們的程序創(chuàng)建了HASH文件夾,并將21個(gè)切片文件存放在內(nèi)
調(diào)取一下合并文件的API


最后文件合并成功啦!

四、對(duì)于大文件上傳的優(yōu)化建議【主要是太懶啦,而且感覺(jué)后續(xù)內(nèi)容都不算很復(fù)雜】

1、上傳文件的時(shí)候可以通過(guò)xhr的onProgress來(lái)顯示上傳的進(jìn)度。

2、網(wǎng)絡(luò)請(qǐng)求的并發(fā)控制。主要原因是大文件HASH計(jì)算后,計(jì)算HASH沒(méi)卡,結(jié)果一下子那么多請(qǐng)求建立很可能把瀏覽器給干卡死掉了。解決思路其實(shí)也不難,就是我們把異步請(qǐng)求放在一個(gè)隊(duì)列里,比如并發(fā)數(shù)是5,就先同時(shí)發(fā)起5個(gè)請(qǐng)求,然后有請(qǐng)求結(jié)束了,再發(fā)起下一個(gè)請(qǐng)求即可。

3、服務(wù)器碎片文件的定時(shí)清理。主要是如果很多人傳文件傳了一半就離開(kāi)了,長(zhǎng)時(shí)間也沒(méi)有進(jìn)行再傳,那么這些切片可以認(rèn)為是沒(méi)有意義的切片,我們可以給它清理掉。

好啦,本篇文章到此結(jié)束啦!期待我的下一篇文章【我很期待】,喜歡的話就給我點(diǎn)個(gè)贊吧!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請(qǐng)通過(guò)簡(jiǎn)信或評(píng)論聯(lián)系作者。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容