最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

前端使用koa實現(xiàn)大文件分片上傳

 更新時間:2022年08月26日 10:30:24   作者:樹下無光  
這篇文章主要為大家介紹了前端使用koa實現(xiàn)大文件分片上傳示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪

引言

一個文件資源服務(wù)器,很多時候需要保存的不只是圖片,文本之類的體積相對較小的文件,有時候,也會需要保存音視頻之類的大文件。在上傳這些大文件的時候,我們不可能一次性將這些文件數(shù)據(jù)全部發(fā)送,網(wǎng)絡(luò)帶寬很多時候不允許我們這么做,而且這樣也極度浪費網(wǎng)絡(luò)資源。

因此,對于這些大文件的上傳,往往會考慮用到分片傳輸。

分片傳輸,顧名思義,也就是將文件拆分成若干個文件片段,然后一個片段一個片段的上傳,服務(wù)器也一個片段一個片段的接收,最后再合并成為完整的文件。

下面我們來一起簡單地實現(xiàn)以下如何進(jìn)行大文件分片傳輸。

前端

拆分上傳的文件流

首先,我們要知道一點:文件信息的 File 對象繼承自 Blob 類,也就是說, File 對象上也存在 slice 方法,用于截取指定區(qū)間的 Buffer 數(shù)組。

通過這個方法,我們就可以在取得用戶需要上傳的文件流的時候,將其拆分成多個文件來上傳:

<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; // 每個區(qū)間的大小
  let beginSide = 0; // 開始截取文件的位置
  // 循環(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>

我們先定義一個 onSubmit 方法來處理我們需要上傳的文件。

onSubmit 中,我們先取得 ref 中的文件對象,這里我們假設(shè)每次有且僅有一個文件,我們也只處理這一個文件。

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

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

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

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

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

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

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

我們再觀察上面構(gòu)建 FormData 的代碼,可以發(fā)現(xiàn),我們 append 進(jìn) FormData 實例的每個文件片段,使用的 name 都是固定為這個文件的真實名稱,因此,同一個文件的每個片,都會有相同的 name ,這樣一來,服務(wù)器就能區(qū)分哪個片是屬于哪個文件的。

filename ,使用 beginSide 除以 range 作為其值,根據(jù)上下文語意可以推出,每個片的 filename 將會是這個片的 序號 ,這是為了在后面服務(wù)端合并文件片段的時候,作為前后順序的依據(jù)。

當(dāng)然,上面的代碼還有一點問題。

在循環(huán)中,我們確實是將文件切成若干個片單獨發(fā)送,但是,我們知道, http 請求是異步的,它不會阻塞主線程。所以,當(dāng)我們發(fā)送了一個請求之后,并不會等這個請求收到響應(yīng)再繼續(xù)發(fā)送下一個請求。因此,我們只是做到了將文件拆分成多個片一次性發(fā)送而已,這并不是我們想要的。

想要解決這個問題也很簡單,只需要將 onSubmit 方法修改為一個異步方法,使用 await 等待每個 http 請求完成即可:

// 省略一些代碼
const onSubmit = async () => {
  // ......
  while(beginSide < fileSize) {
    // ......
    await uploadLargeFile(formData)
  }
}
// ......

這樣一來,每個片都會等到上一個片發(fā)送完成才發(fā)送,可以在網(wǎng)絡(luò)控制臺的時間線中看到這一點:

后端

接收文件片段

這里我們使用的 koa-body 來 處理上傳的文件數(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()
  }
)

我們的策略是先將同一個 name 的文件片段收集到以這個 name 進(jìn)行 MD5 哈希轉(zhuǎn)換后對應(yīng)的文件夾名稱的文件夾當(dāng)中,但使用 koa-body 提供的配置項無法做到這么細(xì)致的工作,所以,我們需要使用自定義 onFileBegin ,即在文件保存之前,將我們期望的工作完成。

首先,我們拼接出我們期望的路徑,并判斷這個路徑對應(yīng)的文件夾是否已經(jīng)存在,如果不存在,那么我們先創(chuàng)建這個文件夾。然后,我們需要修改 koa-body 傳給我們的 file 對象。因為對象類型是引用類型,指向的是同一個地址空間,所以我們修改了這個 file 對象的屬性, koa-body 最后獲得的 file 對象也就被修改了,因此, koa-body 就能夠根據(jù)我們修改的 file 對象去進(jìn)行后續(xù)保存文件的操作。

這里我們因為要將保存的文件指定為我們期望的路徑,所以需要修改 filepath 這個屬性。

而在上文中我們提到,前端在 FormData 中傳入了第三個參數(shù)(文件片段的序號),這個參數(shù),我們可以通過 file.originalFilename 訪問。這里,我們就直接使用這個序號字段作為文件片段的名稱,也就是說,每個片段最終會保存到 ${tempDir}/${hashDir}/${序號} 這個文件。

由于每個文件片段沒有實際意義以及用處,所以我們不需要指定后綴名。

合并文件片段

在我們合并文件之前,我們需要知道文件片段是否已經(jīng)全部上傳完成了,這里我們需要修改一下前端部分的 onSubmit 方法,以發(fā)送給后端這個信號:

// 省略一些代碼
const onSubmit = async () => {
  // ......
  while(beginSide < fileSize) {
    const formData = new FormData()
    formData.append(
      file.name, 
      file.slice(beginSide, beginSide + range), 
      (beginSide / range).toString()
    )
    beginSide += range
    // 滿足這個條件表示文件片段已經(jīng)全部發(fā)送完成,此時在表單中帶入結(jié)束信息
    if(beginSide >= fileSize) {
      formData.append("over", file.name)
    }
    await uploadLargeFile(formData)
  }
}
// ......

為圖方便,我們直接在一個接口中做傳輸結(jié)束的判斷。判斷的依據(jù)是:當(dāng) beiginSide 大于等于 fileSize 的時候,就放入一個 over 字段,并以這個文件的真實名稱作為其屬性值。

這樣,后端代碼就可以以是否存在 over 這個字段作為文件片段是否已經(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()
  }
)

我們先取得這個文件真實名字的 hash ,這個也是我們之前用于存放對應(yīng)文件片段使用的文件夾的名稱。

接著我們獲取該文件夾下的文件列表,這會是一個字符串?dāng)?shù)組(并且由于我們前期的設(shè)計邏輯,我們不需要在這里考慮文件夾的嵌套)。

然后我們遍歷這個數(shù)組,去拿到每個文件片段的路徑,以此來創(chuàng)建一個讀入流,再以存放合并后的文件的路徑創(chuàng)建一個寫入流(注意,此時需要帶上擴(kuò)展名,并且,需要設(shè)置 flags'a' ,表示追加寫入),最后以管道流的方式進(jìn)行傳輸。

但我們知道,這些使用到的流的操作都是異步回調(diào)的??墒?,我們保存的文件片段彼此之間是有先后順序的,也就是說,我們得保證在前面一個片段寫入完成之后再寫入下一個片段,否則文件的數(shù)據(jù)就錯誤了。

要實現(xiàn)這一點,需要使用到 Promise 這一api。

首先我們定義了一個 fulfilled 狀態(tài)的 Promise 變量 p ,也就是說,這個 p 變量的 then 方法將在下一個微任務(wù)事件的調(diào)用時間點直接被執(zhí)行。

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

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

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

當(dāng)所有的文件片段讀寫完成后,我們就拿實現(xiàn)了將完整的文件保存到了服務(wù)器。

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

使用一個 Map 來保存真實文件名與 MD5 哈希值的映射關(guān)系,避免每次都進(jìn)行 MD5 運算等等。但這里只是給出了簡單的實習(xí),具體的優(yōu)化還請根據(jù)實際需求進(jìn)行調(diào)整。

總結(jié)

  • 使用 slice 方法可以截取 file 對象的片段,分次發(fā)送文件片段;
  • 使用 koa-body 保存每個文件片段到一個指定的暫存文件夾,在文件片段全部發(fā)送完成之后,將片段合并。

以上就是前端使用koa實現(xiàn)大文件分片上傳的詳細(xì)內(nèi)容,更多關(guān)于koa大文件分片上傳的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

最新評論

门源| 江口县| 益阳市| 三原县| 建昌县| 灌阳县| 塔河县| 张掖市| 班玛县| 特克斯县| 华阴市| 汕头市| 宁南县| 嘉鱼县| 昌宁县| 玉田县| 安西县| 维西| 田林县| 尚义县| 扬中市| 康平县| 旅游| 余庆县| 辛集市| 重庆市| 长汀县| 会同县| 郓城县| 乌兰察布市| 德阳市| 迁西县| 新巴尔虎右旗| 益阳市| 乌审旗| 亚东县| 西乌珠穆沁旗| 方城县| 白朗县| 比如县| 秦安县|