溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

web前端怎么使用koa實現大文件分片上傳

發布時間:2022-08-26 11:27:43 來源:億速云 閱讀:183 作者:iii 欄目:開發技術

本篇內容介紹了“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 中的文件對象,這里我們假設每次有且僅有一個文件,我們也只處理這一個文件。

    然后我們定義 一個 beginSiderange 變量,分別表示每次開始截取文件數據的位置,以及每次截取的片段的大小。

    這樣一來,當我們使用 file.slice(beginSide, beginSide + range) 的時候,我們就取得了這一次需要上傳的對應的文件數據,之后便可以使用 FormData 封裝這個文件數據,然后調用接口發送到服務器了。

    接著,我們使用一個循環不斷重復這一過程,直到 beginSide 超過了文件本身的大小,這時就表示這個文件的每個片段都已經上傳完成了。當然,別忘了每次切完片后,將 beginSide 移動到下一個位置。

    另外,需要注意的是,我們將文件的片添加到表單數據的時候,總共傳入了三個參數。第二個參數沒有什么好說的,是我們的文件片段,關鍵在于第一個和第三個參數。這兩個參數都會作為 Content-Disposition 中的屬性。

    第一個參數,對應的字段名叫做 name ,表示的是這個數據本身對應的名稱,并不區分是什么數據,因為 FormData 不只可以用作文件流的傳輸,也可以用作普通 JSON 數據的傳輸,那么這時候,這個 name 其實就是 JSON 中某個屬性的 key 。

    而第二個參數,對應的字段則是 filename ,這個其實才應該真正地叫做文件名。

    我們可以使用 wireshark 捕獲一下我們發送地請求以驗證這一點。

    web前端怎么使用koa實現大文件分片上傳

    我們再觀察上面構建 FormData 的代碼,可以發現,我們 appendFormData 實例的每個文件片段,使用的 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 方法將在下一個微任務事件的調用時間點直接被執行。

    接著,我們在遍歷文件片段列表的時候,不直接進行讀寫,而是把讀寫操作放到 pthen 回調當中,并且將其封裝在一個 Promsie 對象當中。在這個 Promise 對象中,我們把 resolve 方法的執行放在管道流的 finish 事件中,這表示,這個 then 回調返回的 Promise 實例,將會在一個文件片段寫入完成后被修改狀態。此時,我們只需要將這個 then 回調返回的 Promsie 實例賦值給 p 即可。

    這樣一來,在下個遍歷節點,也就是處理第二個文件片段的時候,取得的 p 的值便是上一個文件片段執行完讀寫操作返回的 Promise 實例,而且第二個片段的執行代碼會在第一個片段對應的 Promise 實例 then 方法被觸發,也就是上一個片段的文件寫入完成之后,再添加到微任務隊列。

    以此類推,每個片段都會在前一個片段寫入完成之后再進行寫入,保證了文件數據先后順序的正確性。

    當所有的文件片段讀寫完成后,我們就拿實現了將完整的文件保存到了服務器。

    不過上面的還有許多可以優化的地方,比如:在合并完文件之后,刪除所有的文件片段,節省磁盤空間;

    使用一個 Map 來保存真實文件名與 MD5 哈希值的映射關系,避免每次都進行 MD5 運算等等。但這里只是給出了簡單的實習,具體的優化還請根據實際需求進行調整。

    “web前端怎么使用koa實現大文件分片上傳”的內容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業相關的知識可以關注億速云網站,小編將為大家輸出更多高質量的實用文章!

    向AI問一下細節

    免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

    AI

    亚洲午夜精品一区二区_中文无码日韩欧免_久久香蕉精品视频_欧美主播一区二区三区美女