切換語言為:簡體
輕量級中介軟體實現大檔案上傳:檔案切片,附完整程式碼

輕量級中介軟體實現大檔案上傳:檔案切片,附完整程式碼

  • 爱糖宝
  • 2024-06-08
  • 2147
  • 0
  • 0

檔案上傳的方案

  1. 大檔案上傳:將大檔案切分成較小的片段(通常稱為分片或塊),然後逐個上傳這些分片。這種方法可以提高上傳的穩定性,因為如果某個分片上傳失敗,只需要重新上傳該分片而不需要重新上傳整個檔案。同時,分片上傳還可以利用多個網路連線並行上傳多個分片,提高上傳速度。

  2. 斷點續傳:在上傳過程中,如果網路中斷或上傳被中止,斷點續傳技術可以記錄已成功上傳的分片資訊,以便在恢復上傳時繼續上傳未完成的部分,而不需要重新上傳整個檔案。這種技術可以大大減少上傳失敗的影響,並節省時間和頻寬。

安裝依賴:

  1. express:啟動後端服務,並且提供介面

  2. multer:讀取檔案,儲存

  3. cors:解決跨域

npm i express
npm i multer
npm i cors

multer中介軟體

  • Multer 是一個node.js中介軟體,用於處理 multipart/form-data型別的表單資料,主要用於上傳檔案。

  • enctype = "multipart/form-data"

  • Multer 不會處理任何非 multipart/form-data 型別的表單資料。

  • 不要將 Multer 作為全域性中介軟體使用,因為惡意使用者可以上傳檔案到一個沒有預料到的路由,應該只在需要處理上傳檔案的路由上使用。

初始化multer

Multer是一個函式,接受一個 options 配置物件,其中最基本的是storage屬性,這將告訴 Multer 將上傳檔案儲存在哪。如果省略 options 物件,這些檔案將儲存在記憶體中,永遠不會寫入磁碟,options 配置如下:

屬性值 描述
dest 或者 storage 在哪裏儲存檔案
limits 限制上傳資料的大小
fileFilter 檔案過濾器,控制哪些檔案可以被接受
preservePath 儲存包含檔名的完整檔案路徑
  • dest:指定上傳檔案的儲存路徑。檔名預設為隨機字元。如果想自定義檔名稱,使用storage自定義儲存引擎屬性,屬性值用multer.diskStorage磁碟儲存引擎來配置。

let upload = multer({dest:"attachment/"});
  • storage:自定義儲存引擎,可以是磁碟儲存引擎,也可以是記憶體儲存引擎。

multer.diskStorage()磁碟儲存引擎,磁碟儲存引擎可以控制檔案的儲存。它是一個函式,函式接受一個 options 配置物件,配置物件有兩個屬性,屬性值都是函式。destination用於指定檔案儲存的路徑;filename用於指定檔案的儲存名稱。

import multer from 'multer' // 上傳檔案處理
// 自定義磁碟儲存引擎
const storage = multer.diskStorage({
    // 儲存檔案的目錄
    destination(req, file, callback) {
        console.log(file,"destination");//列印結果如下圖
        callback(null, 'uploads/'); // 第一個引數是error錯誤物件,不需要設定為null,第二個引數是每個上傳檔案儲存的目錄
    },
    // 儲存檔案的名稱
    filename(req, file, callback) {
        console.log(req.body,"canshu"); // 接受前端傳的引數,multipart/form-data 型別
        callback(null, `${req.body.index}-${req.body.filename}`); // 檔案的名稱
    }
});
// 初始化multer中介軟體,multer是一個函式,接受的引數是一個配置物件
const uploadContainer = multer({
    storage  // 自定義儲存
});

輕量級中介軟體實現大檔案上傳:檔案切片,附完整程式碼

輕量級中介軟體實現大檔案上傳:檔案切片,附完整程式碼

multer.memoryStorage()記憶體儲存引擎

  • 記憶體儲存引擎將檔案儲存在記憶體中的 Buffer 物件,它沒有任何選項。

  • 當使用記憶體儲存引擎,檔案資訊將包含一個 buffer 欄位,裡面包含了整個檔案資料。

  • 當使用記憶體儲存,上傳非常大的檔案,或者非常多的小檔案,會導致應用程式記憶體溢位。

let storage = multer.memoryStorage()
let upload = multer({storage})
  • limits:用來指定一些資料大小的限制,設定 limits 可以幫助保護站點抵禦拒絕服務 (DoS) 攻擊。是一個物件,包含如下屬性:

屬性 值型別 預設值 描述
files Number 無限 上傳時,檔案的最大數量
fileSize Number 無限 上傳時,每一個檔案最大長度 (單位:bytes)
fields Number 無限 上傳時,可以提交非檔案的欄位的數量
fieldNameSize Number 100 bytes 上傳時,每一個欄位名字的最大長度
fieldSize Number 1048576 bytes,即1MB 上傳時,每一個欄位名的屬性值的最大長度
parts Number 無限 上傳時,傳輸的最大數量(fields + files)
headerPairs Number 2000 在上傳的表單型別資料中,鍵值對最大組數
const multer=require("multer");
let upload=multer({
      limits:{
        files:2, //最多上傳2個檔案
        fileSize:5120 //設定單個檔案最大為 5kb
   }
});
  • fileFilter屬性值是一個函式,用來控制什麼檔案可以上傳以及什麼檔案應該跳過

let storage = multer.memoryStorage()
var upload = multer({
    fileFilter(req, file, cb) {
        // 透過呼叫cb,用boolean值來指示是否應接受該檔案
        // 拒絕這個檔案,使用false,像這樣:
         cb(null, false)
        // 接受這個檔案,使用true,像這樣:
         cb(null, true)
        // 如果有問題,可以總是這樣傳送一個錯誤:
      cb(new Error('I don\'t have a clue!'))
}})

multer方法

multer(options).single(fieldname):上傳單個檔案,比如一次只上傳一張圖片。fieldname是前端傳參的上傳檔案的欄位名稱。然後它會自動將檔案儲存到設定的路徑。

輕量級中介軟體實現大檔案上傳:檔案切片,附完整程式碼

multer(options).array(fieldname[,maxCount]):適用於同一個欄位,一次上傳多個檔案的情況,例如使用者選擇多張圖片傳送,接受一個以 fieldname 命名的檔案陣列,fieldname是前端傳參的上傳檔案的欄位名稱。maxCount可選引數,可以指定限制上傳的最大數量。這些檔案的資訊儲存在 req.files

//一次最多上傳3個檔案
let upload=multer({dest:"attachment/"}).array("photo",3);
// 前端傳參格式
let formdata = new FromData();
const fileList = [fileObj1,fileObj2,fileObj3];
formdata.append('photo',fileList)

上傳的資料格式如下:

輕量級中介軟體實現大檔案上傳:檔案切片,附完整程式碼

multer(options).fields(fields):適用於上傳多個欄位的情況。接受指定 fields 的混合檔案。這些檔案的資訊儲存在 req.files。fields 是一個物件陣列,具有 name 和可選的 maxCount 屬性。

let fieldsList=[
    {name:"photo1"},
    {name:"photo2",maxCount:2}
]
let upload=multer({dest:"attachment/"}).fields(fieldsList);
// 前端傳參格式
let formdata = new FromData();
const fileList = [fileObj2,fileObj3];
formdata.append('photo1',fileObj1)
formdata.append('photo2',fileList)

上傳的資料格式如下:

輕量級中介軟體實現大檔案上傳:檔案切片,附完整程式碼

multer(options).none():接收只有文字域的表單,如果上傳任何檔案,會返回 “LIMIT_UNEXPECTED_FILE” 錯誤。

let upload=multer({dest:"attachment/"}).none();

multer(options).any():接收一切上傳的檔案。

let upload=multer({dest:"attachment/"}).any();

錯誤處理機制

當遇到一個錯誤,multer 將會把錯誤傳送給 express。如果想捕捉 Multer 錯誤,可以使用 multer 物件下的 MulterError 類 (即 err instanceof multer.MulterError)。

var multer = require("multer")
var upload = multer().single("photo")
upload(req, res, function (err) {
	if (err instanceof multer.MulterError) {
	  // 捕捉 Multer 錯誤
	} else if (err) {
	  // 捕捉 express 錯誤
	} else {
	  // 上傳成功
	}
})

前端

上傳部分的邏輯可以分為三部分:

  • 對每個切片進行包裝:首先將資料放進 formdata 中,然後發起請求

  • 使用 Promise.all 傳送請求

  • 發起 merge 請求讓後端進行切片的合併

前端程式碼:

<script setup>
import { ref } from "vue";
import axios from "axios";
import { ElMessage } from "element-plus";
// 檔案列表
const fileListArray = ref([]);
// 檔案上傳前限制檔案型別和大小
const beforeUpload = (file) => {
  // const mimeTypes = ['audio/mpeg', 'audio/x-m4a', 'audio/aac', 'video/mp4', 'video/x-m4v']
  // if (!mimeTypes.includes(file.type)) {
  //   ElMessage({
  //     type: 'error',
  //     message: '只能上傳 MP3、M4A、AAC、MP4、M4V 格式的檔案',
  //     duration: 6000
  //   })
  //   return false
  // }
  if (file.size / 1024 / 1024 / 1024 > 1.5) {
    ElMessage({
      type: "error",
      message: "檔案大小不能超過 1.5G",
      duration: 6000,
    });
    return false;
  }
  return true;
};
// 當前切片上傳 AbortController
let controller = null;
// 上傳進度
const percentage = ref(0);
const dialogVisible = ref(false);
// 取消上傳
const cancelUpload = ref(false);
// 檔案總大小
let fileTotal = 0;
// 已上傳檔案切片的大小
let loadedSize = 0;
// 檔案的名字
let fileCancelName = '';
// 分割檔案為多個切片
const sliceFile = (file, chunkSize = 1024 * 10) => {
  // file 接受檔案物件,chunkSize 檔案切片大小,1024*1024表示為1M,這裏預設是是10kb,實際可以根據專案情況進行設定
  // 存放切片檔案的陣列
  let chunks = [];
  // file.size 檔案物件取出檔案大小,單位是位元組byte,1024byte = 1kb
  for (let i = 0; i < file.size; i += chunkSize) {
    // 切割檔案:[0-10kb,10kbM-20kb,20kb-30kb,...]
    // i切片開始位置,i + chunkSize切片結束位置
    chunks.push(file.slice(i, Math.min(i + chunkSize, file.size)));
  }
  return chunks;
};
// 上傳單個檔案切片,對每個切片進行包裝並上傳:首先將資料放進 formdata 中,然後發起請求
const uploadChunk = (chunk, index, filename, mimeTypes) => {
  controller = new AbortController(); // 每一次上傳切片都要新生成一個 AbortController ,否則重新上傳會失敗
  return new Promise((resolve, reject) => {
    // 將資料放進 formdata 中
    const formData = new FormData();
    // 每個切片標識欄位
    formData.append("index", index);
    // 檔名欄位
    formData.append("filename", filename);
    // 檔案MIME型別欄位
    formData.append("type", mimeTypes);
    // 切片檔案,上傳的檔案必須寫在最後(因為後端multer模組讀取引數時,當讀取到檔案時就會停止讀取引數,其後麵的引數就不會再處理了)
    formData.append("file", chunk);
    if (cancelUpload.value) {
      // 若已經取消上傳,則不再上傳切片
      console.log("cacel");
      return;
    }
    // 發起請求
    axios({
      url: "http://localhost:3000/upload",
      method: "POST",
      data: formData,
      // `onUploadProgress` 允許為上傳處理進度事件
      onUploadProgress: (progressEvent) => {
        // progressEvent.loaded 為已上傳檔案位元組數,progressEvent.total 為檔案總位元組數
        // 處理原生進度事件,計算上傳進度,將每個已經上傳的切片檔案的位元組數相加再除以檔案總的大小
        loadedSize += progressEvent.loaded;
        percentage.value = Number(
          ((Math.min(fileTotal, loadedSize) / fileTotal) * 100).toFixed(2)
        );
      },
      signal: controller.signal, // 取消上傳
    })
      .then(resolve)
      .catch(reject);
  });
};
// 使用Promise.all上傳所有切片
const uploadFileChunks = async (chunks, filename, mimeTypes) => {
  // chunks 儲存切割完成的檔案切片陣列 filename 上傳的檔案切片的檔名
  try {
    const res = await Promise.all(
      chunks.map((chunk, index) =>
        uploadChunk(chunk, index, filename, mimeTypes)
      )
    );
    // 合併檔案的名字
    const fileName = res[0].data.data.filename;
    // 檔案型別
    const type = res[0].data.data.type;
    console.log("All chunks uploaded successfully");
    setTimeout(() => {
      // 延遲關閉上傳進度框使用者體驗會更好
      dialogVisible.value = false;
      ElMessage({
        message: "上傳成功",
        type: "success",
      });
      axios.post("http://localhost:3000/merge", {
        filename: fileName,
        mimeTypes: type,
      }); // 呼叫後端合併切片介面,引數需要與後端對齊
    }, 500);
  } catch (error) {
    console.error("Error uploading chunks", error);
  }
};
// 自行實現上傳檔案的請求,使用Promise.all上傳所有切片
const upload = async (file) => {
  percentage.value = 0; // 每次上傳檔案前清空進度條
  dialogVisible.value = true; // 顯示上傳進度
  cancelUpload.value = false; // 每次上傳檔案前將取消上傳標識置為 false
  // 上傳的檔案
  const uoloadfile = file.file;
  fileTotal = uoloadfile.size;
  // 獲取檔名
  const filename = uoloadfile.name.split(".")[0];
  fileCancelName = filename;
  // 獲取檔案的MIME型別
  const mimeTypes = uoloadfile.name.split(".")[1];
  // 將檔案進行切片
  const chunks = sliceFile(uoloadfile);
  // 開始上傳
  uploadFileChunks(chunks, filename, mimeTypes);
};
// 取消上傳
const cancel = () => {
  dialogVisible.value = false;
  cancelUpload.value = true;
  controller?.abort();
  axios.post("http://localhost:3000/cancelUpload", { filename: fileCancelName }); // 呼叫後端介面,刪除已上傳的切片
};
</script>

<template>
  <div>
    <el-upload
     
      :before-upload="beforeUpload"
      :http-request="upload"
      :show-file-list="false"
    >
      <template #trigger>
        <el-button type="primary">選取檔案</el-button>
      </template>
      <template #tip>
        <div>不能低於1M</div>
      </template>
    </el-upload>
    <el-dialog
      v-model="dialogVisible"
      :fullscreen="true"
      :show-close="false"
      custom-class="dispute-upload-dialog"
    >
      <div>
        <div class="fz-18 ellipsis">正在上傳</div>
        <el-progress
          :text-inside="true"
          :stroke-width="16"
          :percentage="percentage"
        />
        <el-button @click="cancel">取消上傳</el-button>
      </div>
    </el-dialog>
  </div>
</template>


<style>
.dispute-upload-dialog {
  background: none;
}
</style>

<style scoped>
.center {
  color: #fff;
  width: 50%;
  text-align: center;
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
}
</style>

輕量級中介軟體實現大檔案上傳:檔案切片,附完整程式碼

輕量級中介軟體實現大檔案上傳:檔案切片,附完整程式碼

輕量級中介軟體實現大檔案上傳:檔案切片,附完整程式碼

上傳切片:

輕量級中介軟體實現大檔案上傳:檔案切片,附完整程式碼

取消上傳:

輕量級中介軟體實現大檔案上傳:檔案切片,附完整程式碼

上傳完成

輕量級中介軟體實現大檔案上傳:檔案切片,附完整程式碼

後端(node.js)

後端主要提供兩個介面

  1. upload:用來儲存切片,用multer中介軟體幫忙處理檔案的儲存

  2. merge:用於合併切片,根據前端上傳的檔名合併檔案切片,並按照切片的索引從小到大進行合併。合併完的切片需要刪除切片並將合併的檔案儲存。

後端程式碼:

import fs from 'node:fs' // 檔案操作
import path from 'node:path' // 路勁操作
import express from 'express' // 提供介面服務
import multer from 'multer' // 上傳檔案處理
import cors from 'cors' // 解決跨域
// 自定義磁碟儲存引擎
const storage = multer.diskStorage({
    // 儲存檔案的目錄
    destination(req, file, callback) {
        callback(null, 'uploads/'); // 第一個引數是error錯誤物件,不需要設定為null,第二個引數是每個上傳檔案儲存的目錄
    },
    // 儲存檔案的名稱
    filename(req, file, callback) {
        // req.body接受前端傳的引數,multipart/form-data 型別
        callback(null, `${req.body.index}-${req.body.filename}`); // 檔案的名稱
    }
});
// 初始化multer中介軟體,multer是一個函式,接受的引數是一個配置物件
const uploadContainer = multer({
    storage  // 自定義儲存
});
const app = express();
// 註冊跨域中介軟體,支援跨域
app.use(cors());
app.use(express.json());
// 上傳切片介面,在上傳檔案的介面加上multer中介軟體
app.post('/upload', uploadContainer.single('file'), (req, res) => {
    // 前端傳入的欄位
    const { filename, type } = req.body;
    res.send({
        succes: true,
        msg: '切片上傳成功',
        data: {
            filename,
            type
        }
    });
});
// 合併切片介面
app.post('/merge', async (req, res) => {
    // 讀取存放切片的目錄:process.cwd()會返回當前檔案的工作目錄的絕對路徑;path.join()使用系統的分隔符將路徑片段進行拼接
    const uploadDir = path.join(process.cwd(), 'uploads');
    console.log(uploadDir, 'path join');
    // 讀取存放切片目錄下的所有切片檔案,返回的是一個數組,但是陣列是亂序的
    let files = fs.readdirSync(uploadDir);
    console.log(files, 'files Array');
    // 給陣列排序,按照切片的索引進行升序排序
    files = files.sort((a, b) => a.split('-')[0] - b.split('-')[0]);
    console.log(files, 'files Array by order');
    // 合併切片後完整的檔案存放的路徑
    const writePath = path.join(process.cwd(), `finallyFile`, `${req.body.filename}.${req.body.mimeTypes}`);
    console.log(writePath, 'writePath');
    // 遍歷切片陣列,合併切片
    files.forEach((item) => {
        // 合併切片
        // readFileSync()讀取檔案,appendFile()以追加方式寫檔案
        // writePath:寫入檔案的路徑
        // data:要寫入檔案的資料此處是fs.readFileSync(path.join(uploadDir, item))讀取出來的每個切片
        // path.join(uploadDir, item)):拼接的每個切片的路徑
        fs.appendFileSync(writePath, fs.readFileSync(path.join(uploadDir, item)));
        // 刪除合併完的切片
        fs.unlinkSync(path.join(uploadDir, item));
    });
    res.send({
        succes: true,
        msg: '檔案上傳成功',
    });
});
// 刪除切片
app.post('/cancelUpload', (req, res) => {
    // 讀取存放切片的目錄:process.cwd()會返回當前檔案的工作目錄的絕對路徑;path.join()使用系統的分隔符將路徑片段進行拼接
    const uploadDir = path.join(process.cwd(), 'uploads');
    // 讀取存放切片目錄下的所有切片檔案,返回的是一個數組
    let files = fs.readdirSync(uploadDir);
    // 需要刪除的切片陣列
    const unlinkArr = files.filter(item => {
        // 前端傳入的檔名
        if (item.indexOf(req.body.filename) != -1) {
            return true;
        }
    });
    unlinkArr.forEach(item => {
        // 刪除需要刪除的切片
        fs.unlinkSync(path.join(uploadDir, item));
    });
    res.send({
        succes: true,
        msg: '檔案取消上傳成功',
    });
});
app.listen(3000, () => {
    console.log('3000 port is running');
});


輕量級中介軟體實現大檔案上傳:檔案切片,附完整程式碼

輕量級中介軟體實現大檔案上傳:檔案切片,附完整程式碼

輕量級中介軟體實現大檔案上傳:檔案切片,附完整程式碼

0則評論

您的電子郵件等資訊不會被公開,以下所有項目均必填

OK! You can skip this field.