本篇內容介紹了“web前端怎么使用koa實現大文件分片上傳”的有關知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領大家學習一下如何處理這些情況吧!希望大家仔細閱讀,能夠學有所成!
一個文件資源服務器,很多時候需要保存的不只是圖片,文本之類的體積相對較小的文件,有時候,也會需要保存音視頻之類的大文件。在上傳這些大文件的時候,我們不可能一次性將這些文件數據全部發送,網絡帶寬很多時候不允許我們這么做,而且這樣也極度浪費網絡資源。
因此,對于這些大文件的上傳,往往會考慮用到分片傳輸。
分片傳輸,顧名思義,也就是將文件拆分成若干個文件片段,然后一個片段一個片段的上傳,服務器也一個片段一個片段的接收,最后再合并成為完整的文件。
下面我們來一起簡單地實現以下如何進行大文件分片傳輸。
首先,我們要知道一點:文件信息的 File 對象繼承自 Blob
類,也就是說, File
對象上也存在 slice
方法,用于截取指定區間的 Buffer
數組。
通過這個方法,我們就可以在取得用戶需要上傳的文件流的時候,將其拆分成多個文件來上傳:
<script setup lang='ts'> import { ref } from "vue" import { uploadLargeFile } from "@/api" const fileInput = ref<HTMLInputElement>() const onSubmit = () => { // 獲取文件對象 const file = onlyFile.value?.file; if (!file) { return } const fileSize = file.size; // 文件的完整大小 const range = 100 * 1024; // 每個區間的大小 let beginSide = 0; // 開始截取文件的位置 // 循環分片上傳文件 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>
我們先定義一個 onSubmit
方法來處理我們需要上傳的文件。
在 onSubmit
中,我們先取得 ref
中的文件對象,這里我們假設每次有且僅有一個文件,我們也只處理這一個文件。
然后我們定義 一個 beginSide
和 range
變量,分別表示每次開始截取文件數據的位置,以及每次截取的片段的大小。
這樣一來,當我們使用 file.slice(beginSide, beginSide + range)
的時候,我們就取得了這一次需要上傳的對應的文件數據,之后便可以使用 FormData
封裝這個文件數據,然后調用接口發送到服務器了。
接著,我們使用一個循環不斷重復這一過程,直到 beginSide
超過了文件本身的大小,這時就表示這個文件的每個片段都已經上傳完成了。當然,別忘了每次切完片后,將 beginSide
移動到下一個位置。
另外,需要注意的是,我們將文件的片添加到表單數據的時候,總共傳入了三個參數。第二個參數沒有什么好說的,是我們的文件片段,關鍵在于第一個和第三個參數。這兩個參數都會作為 Content-Disposition
中的屬性。
第一個參數,對應的字段名叫做 name
,表示的是這個數據本身對應的名稱,并不區分是什么數據,因為 FormData
不只可以用作文件流的傳輸,也可以用作普通 JSON
數據的傳輸,那么這時候,這個 name
其實就是 JSON
中某個屬性的 key
。
而第二個參數,對應的字段則是 filename
,這個其實才應該真正地叫做文件名。
我們可以使用 wireshark
捕獲一下我們發送地請求以驗證這一點。
我們再觀察上面構建 FormData
的代碼,可以發現,我們 append
進 FormData
實例的每個文件片段,使用的 name
都是固定為這個文件的真實名稱,因此,同一個文件的每個片,都會有相同的 name
,這樣一來,服務器就能區分哪個片是屬于哪個文件的。
而 filename
,使用 beginSide
除以 range
作為其值,根據上下文語意可以推出,每個片的 filename
將會是這個片的 序號 ,這是為了在后面服務端合并文件片段的時候,作為前后順序的依據。
當然,上面的代碼還有一點問題。
在循環中,我們確實是將文件切成若干個片單獨發送,但是,我們知道, http
請求是異步的,它不會阻塞主線程。所以,當我們發送了一個請求之后,并不會等這個請求收到響應再繼續發送下一個請求。因此,我們只是做到了將文件拆分成多個片一次性發送而已,這并不是我們想要的。
想要解決這個問題也很簡單,只需要將 onSubmit
方法修改為一個異步方法,使用 await
等待每個 http
請求完成即可:
// 省略一些代碼 const onSubmit = async () => { // ...... while(beginSide < fileSize) { // ...... await uploadLargeFile(formData) } } // ......
這樣一來,每個片都會等到上一個片發送完成才發送,可以在網絡控制臺的時間線中看到這一點:
這里我們使用的 koa-body
來 處理上傳的文件數據:
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() } )
我們的策略是先將同一個 name
的文件片段收集到以這個 name
進行 MD5
哈希轉換后對應的文件夾名稱的文件夾當中,但使用 koa-body
提供的配置項無法做到這么細致的工作,所以,我們需要使用自定義 onFileBegin
,即在文件保存之前,將我們期望的工作完成。
首先,我們拼接出我們期望的路徑,并判斷這個路徑對應的文件夾是否已經存在,如果不存在,那么我們先創建這個文件夾。然后,我們需要修改 koa-body
傳給我們的 file
對象。因為對象類型是引用類型,指向的是同一個地址空間,所以我們修改了這個 file
對象的屬性, koa-body
最后獲得的 file
對象也就被修改了,因此, koa-body
就能夠根據我們修改的 file
對象去進行后續保存文件的操作。
這里我們因為要將保存的文件指定為我們期望的路徑,所以需要修改 filepath
這個屬性。
而在上文中我們提到,前端在 FormData
中傳入了第三個參數(文件片段的序號),這個參數,我們可以通過 file.originalFilename
訪問。這里,我們就直接使用這個序號字段作為文件片段的名稱,也就是說,每個片段最終會保存到 ${tempDir}/${hashDir}/${序號} 這個文件。
由于每個文件片段沒有實際意義以及用處,所以我們不需要指定后綴名。
在我們合并文件之前,我們需要知道文件片段是否已經全部上傳完成了,這里我們需要修改一下前端部分的 onSubmit
方法,以發送給后端這個信號:
// 省略一些代碼 const onSubmit = async () => { // ...... while(beginSide < fileSize) { const formData = new FormData() formData.append( file.name, file.slice(beginSide, beginSide + range), (beginSide / range).toString() ) beginSide += range // 滿足這個條件表示文件片段已經全部發送完成,此時在表單中帶入結束信息 if(beginSide >= fileSize) { formData.append("over", file.name) } await uploadLargeFile(formData) } } // ......
為圖方便,我們直接在一個接口中做傳輸結束的判斷。判斷的依據是:當 beiginSide
大于等于 fileSize
的時候,就放入一個 over
字段,并以這個文件的真實名稱作為其屬性值。
這樣,后端代碼就可以以是否存在 over
這個字段作為文件片段是否已經全部發送完成的標志:
router.post( "/upload/largeFile", KoaBody({ // 省略一些配置 }), async (ctx, next) => { if (ctx.request.body.over) { // 如果 over 存在值,那么表示文件片段已經全部上傳完成了 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() } )
我們先取得這個文件真實名字的 hash
,這個也是我們之前用于存放對應文件片段使用的文件夾的名稱。
接著我們獲取該文件夾下的文件列表,這會是一個字符串數組(并且由于我們前期的設計邏輯,我們不需要在這里考慮文件夾的嵌套)。
然后我們遍歷這個數組,去拿到每個文件片段的路徑,以此來創建一個讀入流,再以存放合并后的文件的路徑創建一個寫入流(注意,此時需要帶上擴展名,并且,需要設置 flags
為 'a'
,表示追加寫入),最后以管道流的方式進行傳輸。
但我們知道,這些使用到的流的操作都是異步回調的??墒?,我們保存的文件片段彼此之間是有先后順序的,也就是說,我們得保證在前面一個片段寫入完成之后再寫入下一個片段,否則文件的數據就錯誤了。
要實現這一點,需要使用到 Promise
這一api。
首先我們定義了一個 fulfilled
狀態的 Promise
變量 p
,也就是說,這個 p
變量的 then
方法將在下一個微任務事件的調用時間點直接被執行。
接著,我們在遍歷文件片段列表的時候,不直接進行讀寫,而是把讀寫操作放到 p
的 then
回調當中,并且將其封裝在一個 Promsie
對象當中。在這個 Promise
對象中,我們把 resolve
方法的執行放在管道流的 finish
事件中,這表示,這個 then
回調返回的 Promise
實例,將會在一個文件片段寫入完成后被修改狀態。此時,我們只需要將這個 then
回調返回的 Promsie
實例賦值給 p
即可。
這樣一來,在下個遍歷節點,也就是處理第二個文件片段的時候,取得的 p
的值便是上一個文件片段執行完讀寫操作返回的 Promise
實例,而且第二個片段的執行代碼會在第一個片段對應的 Promise
實例 then
方法被觸發,也就是上一個片段的文件寫入完成之后,再添加到微任務隊列。
以此類推,每個片段都會在前一個片段寫入完成之后再進行寫入,保證了文件數據先后順序的正確性。
當所有的文件片段讀寫完成后,我們就拿實現了將完整的文件保存到了服務器。
不過上面的還有許多可以優化的地方,比如:在合并完文件之后,刪除所有的文件片段,節省磁盤空間;
使用一個 Map 來保存真實文件名與 MD5 哈希值的映射關系,避免每次都進行 MD5 運算等等。但這里只是給出了簡單的實習,具體的優化還請根據實際需求進行調整。
“web前端怎么使用koa實現大文件分片上傳”的內容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業相關的知識可以關注億速云網站,小編將為大家輸出更多高質量的實用文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。