您好,登錄后才能下訂單哦!
本篇內(nèi)容介紹了“web前端怎么使用koa實(shí)現(xiàn)大文件分片上傳”的有關(guān)知識(shí),在實(shí)際案例的操作過(guò)程中,不少人都會(huì)遇到這樣的困境,接下來(lái)就讓小編帶領(lǐng)大家學(xué)習(xí)一下如何處理這些情況吧!希望大家仔細(xì)閱讀,能夠?qū)W有所成!
一個(gè)文件資源服務(wù)器,很多時(shí)候需要保存的不只是圖片,文本之類的體積相對(duì)較小的文件,有時(shí)候,也會(huì)需要保存音視頻之類的大文件。在上傳這些大文件的時(shí)候,我們不可能一次性將這些文件數(shù)據(jù)全部發(fā)送,網(wǎng)絡(luò)帶寬很多時(shí)候不允許我們這么做,而且這樣也極度浪費(fèi)網(wǎng)絡(luò)資源。
因此,對(duì)于這些大文件的上傳,往往會(huì)考慮用到分片傳輸。
分片傳輸,顧名思義,也就是將文件拆分成若干個(gè)文件片段,然后一個(gè)片段一個(gè)片段的上傳,服務(wù)器也一個(gè)片段一個(gè)片段的接收,最后再合并成為完整的文件。
下面我們來(lái)一起簡(jiǎn)單地實(shí)現(xiàn)以下如何進(jìn)行大文件分片傳輸。
首先,我們要知道一點(diǎn):文件信息的 File 對(duì)象繼承自 Blob
類,也就是說(shuō), File
對(duì)象上也存在 slice
方法,用于截取指定區(qū)間的 Buffer
數(shù)組。
通過(guò)這個(gè)方法,我們就可以在取得用戶需要上傳的文件流的時(shí)候,將其拆分成多個(gè)文件來(lái)上傳:
<script setup lang='ts'> import { ref } from "vue" import { uploadLargeFile } from "@/api" const fileInput = ref<HTMLInputElement>() const onSubmit = () => { // 獲取文件對(duì)象 const file = onlyFile.value?.file; if (!file) { return } const fileSize = file.size; // 文件的完整大小 const range = 100 * 1024; // 每個(gè)區(qū)間的大小 let beginSide = 0; // 開(kāi)始截取文件的位置 // 循環(huán)分片上傳文件 while (beginSide < fileSize) { const formData = new FormData() formData.append( file.name, file.slice(beginSide, beginSide + range), (beginSide / range).toString() ) beginSide += range uploadLargeFile(formData) } } </script> <template> <input ref="fileInput" type="file" placeholder="選擇你的文件" > <button @click="onSubmit">提交</button> </template>
我們先定義一個(gè) onSubmit
方法來(lái)處理我們需要上傳的文件。
在 onSubmit
中,我們先取得 ref
中的文件對(duì)象,這里我們假設(shè)每次有且僅有一個(gè)文件,我們也只處理這一個(gè)文件。
然后我們定義 一個(gè) beginSide
和 range
變量,分別表示每次開(kāi)始截取文件數(shù)據(jù)的位置,以及每次截取的片段的大小。
這樣一來(lái),當(dāng)我們使用 file.slice(beginSide, beginSide + range)
的時(shí)候,我們就取得了這一次需要上傳的對(duì)應(yīng)的文件數(shù)據(jù),之后便可以使用 FormData
封裝這個(gè)文件數(shù)據(jù),然后調(diào)用接口發(fā)送到服務(wù)器了。
接著,我們使用一個(gè)循環(huán)不斷重復(fù)這一過(guò)程,直到 beginSide
超過(guò)了文件本身的大小,這時(shí)就表示這個(gè)文件的每個(gè)片段都已經(jīng)上傳完成了。當(dāng)然,別忘了每次切完片后,將 beginSide
移動(dòng)到下一個(gè)位置。
另外,需要注意的是,我們將文件的片添加到表單數(shù)據(jù)的時(shí)候,總共傳入了三個(gè)參數(shù)。第二個(gè)參數(shù)沒(méi)有什么好說(shuō)的,是我們的文件片段,關(guān)鍵在于第一個(gè)和第三個(gè)參數(shù)。這兩個(gè)參數(shù)都會(huì)作為 Content-Disposition
中的屬性。
第一個(gè)參數(shù),對(duì)應(yīng)的字段名叫做 name
,表示的是這個(gè)數(shù)據(jù)本身對(duì)應(yīng)的名稱,并不區(qū)分是什么數(shù)據(jù),因?yàn)?FormData
不只可以用作文件流的傳輸,也可以用作普通 JSON
數(shù)據(jù)的傳輸,那么這時(shí)候,這個(gè) name
其實(shí)就是 JSON
中某個(gè)屬性的 key
。
而第二個(gè)參數(shù),對(duì)應(yīng)的字段則是 filename
,這個(gè)其實(shí)才應(yīng)該真正地叫做文件名。
我們可以使用 wireshark
捕獲一下我們發(fā)送地請(qǐng)求以驗(yàn)證這一點(diǎn)。
我們?cè)儆^察上面構(gòu)建 FormData
的代碼,可以發(fā)現(xiàn),我們 append
進(jìn) FormData
實(shí)例的每個(gè)文件片段,使用的 name
都是固定為這個(gè)文件的真實(shí)名稱,因此,同一個(gè)文件的每個(gè)片,都會(huì)有相同的 name
,這樣一來(lái),服務(wù)器就能區(qū)分哪個(gè)片是屬于哪個(gè)文件的。
而 filename
,使用 beginSide
除以 range
作為其值,根據(jù)上下文語(yǔ)意可以推出,每個(gè)片的 filename
將會(huì)是這個(gè)片的 序號(hào) ,這是為了在后面服務(wù)端合并文件片段的時(shí)候,作為前后順序的依據(jù)。
當(dāng)然,上面的代碼還有一點(diǎn)問(wèn)題。
在循環(huán)中,我們確實(shí)是將文件切成若干個(gè)片單獨(dú)發(fā)送,但是,我們知道, http
請(qǐng)求是異步的,它不會(huì)阻塞主線程。所以,當(dāng)我們發(fā)送了一個(gè)請(qǐng)求之后,并不會(huì)等這個(gè)請(qǐng)求收到響應(yīng)再繼續(xù)發(fā)送下一個(gè)請(qǐng)求。因此,我們只是做到了將文件拆分成多個(gè)片一次性發(fā)送而已,這并不是我們想要的。
想要解決這個(gè)問(wèn)題也很簡(jiǎn)單,只需要將 onSubmit
方法修改為一個(gè)異步方法,使用 await
等待每個(gè) http
請(qǐng)求完成即可:
// 省略一些代碼 const onSubmit = async () => { // ...... while(beginSide < fileSize) { // ...... await uploadLargeFile(formData) } } // ......
這樣一來(lái),每個(gè)片都會(huì)等到上一個(gè)片發(fā)送完成才發(fā)送,可以在網(wǎng)絡(luò)控制臺(tái)的時(shí)間線中看到這一點(diǎn):
這里我們使用的 koa-body
來(lái) 處理上傳的文件數(shù)據(jù):
import Router = require("@koa/router") import KoaBody = require("koa-body") import { resolve } from 'path' import { publicPath } from "../common"; import { existsSync, mkdirSync } from "fs" import { MD5 } from "crypto-js" const router = new Router() const savePath = resolve(publicPath, 'assets') const tempDirPath = resolve(publicPath, "assets", "temp") router.post( "/upload/largeFile", KoaBody({ multipart: true, formidable: { maxFileSize: 1024 * 1024 * 2, onFileBegin(name, file) { const hashDir = MD5(name).toString() const dirPath = resolve(tempDirPath, hashDir) if (!existsSync(dirPath)) { mkdirSync(dirPath, { recursive: true }) } if (file.originalFilename) { file.filepath = resolve(dirPath, file.originalFilename) } } } }), async (ctx, next) => { ctx.response.body = "done"; next() } )
我們的策略是先將同一個(gè) name
的文件片段收集到以這個(gè) name
進(jìn)行 MD5
哈希轉(zhuǎn)換后對(duì)應(yīng)的文件夾名稱的文件夾當(dāng)中,但使用 koa-body
提供的配置項(xiàng)無(wú)法做到這么細(xì)致的工作,所以,我們需要使用自定義 onFileBegin
,即在文件保存之前,將我們期望的工作完成。
首先,我們拼接出我們期望的路徑,并判斷這個(gè)路徑對(duì)應(yīng)的文件夾是否已經(jīng)存在,如果不存在,那么我們先創(chuàng)建這個(gè)文件夾。然后,我們需要修改 koa-body
傳給我們的 file
對(duì)象。因?yàn)閷?duì)象類型是引用類型,指向的是同一個(gè)地址空間,所以我們修改了這個(gè) file
對(duì)象的屬性, koa-body
最后獲得的 file
對(duì)象也就被修改了,因此, koa-body
就能夠根據(jù)我們修改的 file
對(duì)象去進(jìn)行后續(xù)保存文件的操作。
這里我們因?yàn)橐獙⒈4娴奈募付槲覀兤谕穆窂?,所以需要修?filepath
這個(gè)屬性。
而在上文中我們提到,前端在 FormData
中傳入了第三個(gè)參數(shù)(文件片段的序號(hào)),這個(gè)參數(shù),我們可以通過(guò) file.originalFilename
訪問(wèn)。這里,我們就直接使用這個(gè)序號(hào)字段作為文件片段的名稱,也就是說(shuō),每個(gè)片段最終會(huì)保存到 ${tempDir}/${hashDir}/${序號(hào)} 這個(gè)文件。
由于每個(gè)文件片段沒(méi)有實(shí)際意義以及用處,所以我們不需要指定后綴名。
在我們合并文件之前,我們需要知道文件片段是否已經(jīng)全部上傳完成了,這里我們需要修改一下前端部分的 onSubmit
方法,以發(fā)送給后端這個(gè)信號(hào):
// 省略一些代碼 const onSubmit = async () => { // ...... while(beginSide < fileSize) { const formData = new FormData() formData.append( file.name, file.slice(beginSide, beginSide + range), (beginSide / range).toString() ) beginSide += range // 滿足這個(gè)條件表示文件片段已經(jīng)全部發(fā)送完成,此時(shí)在表單中帶入結(jié)束信息 if(beginSide >= fileSize) { formData.append("over", file.name) } await uploadLargeFile(formData) } } // ......
為圖方便,我們直接在一個(gè)接口中做傳輸結(jié)束的判斷。判斷的依據(jù)是:當(dāng) beiginSide
大于等于 fileSize
的時(shí)候,就放入一個(gè) over
字段,并以這個(gè)文件的真實(shí)名稱作為其屬性值。
這樣,后端代碼就可以以是否存在 over
這個(gè)字段作為文件片段是否已經(jīng)全部發(fā)送完成的標(biāo)志:
router.post( "/upload/largeFile", KoaBody({ // 省略一些配置 }), async (ctx, next) => { if (ctx.request.body.over) { // 如果 over 存在值,那么表示文件片段已經(jīng)全部上傳完成了 const _fileName = ctx.request.body.over; const ext = _fileName.split("\.")[1] const hashedDir = MD5(_fileName).toString() const dirPath = resolve(tempDirPath, hashedDir) const fileList = readdirSync(dirPath); let p = Promise.resolve(void 0) fileList.forEach(fragmentFileName => { p = p.then(() => new Promise((r) => { const ws = createWriteStream(resolve(savePath, `${hashedDir}.${ext}`), { flags: "a" }) const rs = createReadStream(resolve(dirPath, fragmentFileName)) rs.pipe(ws).on("finish", () => { ws.close() rs.close(); r(void 0) }) }) ) }) await p } ctx.response.body = "done"; next() } )
我們先取得這個(gè)文件真實(shí)名字的 hash
,這個(gè)也是我們之前用于存放對(duì)應(yīng)文件片段使用的文件夾的名稱。
接著我們獲取該文件夾下的文件列表,這會(huì)是一個(gè)字符串?dāng)?shù)組(并且由于我們前期的設(shè)計(jì)邏輯,我們不需要在這里考慮文件夾的嵌套)。
然后我們遍歷這個(gè)數(shù)組,去拿到每個(gè)文件片段的路徑,以此來(lái)創(chuàng)建一個(gè)讀入流,再以存放合并后的文件的路徑創(chuàng)建一個(gè)寫(xiě)入流(注意,此時(shí)需要帶上擴(kuò)展名,并且,需要設(shè)置 flags
為 'a'
,表示追加寫(xiě)入),最后以管道流的方式進(jìn)行傳輸。
但我們知道,這些使用到的流的操作都是異步回調(diào)的。可是,我們保存的文件片段彼此之間是有先后順序的,也就是說(shuō),我們得保證在前面一個(gè)片段寫(xiě)入完成之后再寫(xiě)入下一個(gè)片段,否則文件的數(shù)據(jù)就錯(cuò)誤了。
要實(shí)現(xiàn)這一點(diǎn),需要使用到 Promise
這一api。
首先我們定義了一個(gè) fulfilled
狀態(tài)的 Promise
變量 p
,也就是說(shuō),這個(gè) p
變量的 then
方法將在下一個(gè)微任務(wù)事件的調(diào)用時(shí)間點(diǎn)直接被執(zhí)行。
接著,我們?cè)诒闅v文件片段列表的時(shí)候,不直接進(jìn)行讀寫(xiě),而是把讀寫(xiě)操作放到 p
的 then
回調(diào)當(dāng)中,并且將其封裝在一個(gè) Promsie
對(duì)象當(dāng)中。在這個(gè) Promise
對(duì)象中,我們把 resolve
方法的執(zhí)行放在管道流的 finish
事件中,這表示,這個(gè) then
回調(diào)返回的 Promise
實(shí)例,將會(huì)在一個(gè)文件片段寫(xiě)入完成后被修改狀態(tài)。此時(shí),我們只需要將這個(gè) then
回調(diào)返回的 Promsie
實(shí)例賦值給 p
即可。
這樣一來(lái),在下個(gè)遍歷節(jié)點(diǎn),也就是處理第二個(gè)文件片段的時(shí)候,取得的 p
的值便是上一個(gè)文件片段執(zhí)行完讀寫(xiě)操作返回的 Promise
實(shí)例,而且第二個(gè)片段的執(zhí)行代碼會(huì)在第一個(gè)片段對(duì)應(yīng)的 Promise
實(shí)例 then
方法被觸發(fā),也就是上一個(gè)片段的文件寫(xiě)入完成之后,再添加到微任務(wù)隊(duì)列。
以此類推,每個(gè)片段都會(huì)在前一個(gè)片段寫(xiě)入完成之后再進(jìn)行寫(xiě)入,保證了文件數(shù)據(jù)先后順序的正確性。
當(dāng)所有的文件片段讀寫(xiě)完成后,我們就拿實(shí)現(xiàn)了將完整的文件保存到了服務(wù)器。
不過(guò)上面的還有許多可以優(yōu)化的地方,比如:在合并完文件之后,刪除所有的文件片段,節(jié)省磁盤(pán)空間;
使用一個(gè) Map 來(lái)保存真實(shí)文件名與 MD5 哈希值的映射關(guān)系,避免每次都進(jìn)行 MD5 運(yùn)算等等。但這里只是給出了簡(jiǎn)單的實(shí)習(xí),具體的優(yōu)化還請(qǐng)根據(jù)實(shí)際需求進(jìn)行調(diào)整。
“web前端怎么使用koa實(shí)現(xiàn)大文件分片上傳”的內(nèi)容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業(yè)相關(guān)的知識(shí)可以關(guān)注億速云網(wǎng)站,小編將為大家輸出更多高質(zhì)量的實(shí)用文章!
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。