前端大檔案上傳,即以流的方式上傳

語言: CN / TW / HK
ead>

前言

在上傳較大的檔案時,將檔案切割成多個小塊,然後每次只發送一小塊,等到全部傳輸完畢之後,服務端將接受的多個小塊進行合併,組成上傳的檔案,這就是前端上傳大檔案的方式,也就是所謂的以流的方式上傳

下面會介紹如下幾個快內容 - 前端程式碼如何編寫 - 後端程式碼如何編寫(node) - vue 中如何處理 - 使用外掛如何處理

1. 前端程式碼實現

這裡先不通過 vue,而是通過原生的 html、js 的方式實現上傳,如此更加容易理解邏輯,等後面再將其轉換成 vue 寫法 檔案上傳通過 axios ,所以,可以先配置其 baseurl,我這裡為axios.defaults.baseURL =http://localhost:3000;

html 程式碼 ```

```

1.1 選擇上傳檔案

為 檔案域 新增 change 事件,當用戶選擇要上傳的檔案後,將檔案資訊賦值給一個變數,方便上傳檔案時使用

```js document .getElementById("uploadInput") .addEventListener("change", handleFileChange);

let file = null; // 檔案被更改 function handleFileChange(event) { const file = event.target.files[0]; if (!file) return; window.file = file; } ```

1.2 檔案上傳

檔案上傳分為如下幾個步驟

① 建立切片

② 上傳切片

③ 全部上傳成功後,告訴後端,後端將所有的切片整合成一個檔案

首先編寫幾個函式,用於切片的處理及上傳,最後再組合到一起實現完整功能

1.2.1 建立切片

js // 建立切片 const createFileChunks = function (file, size = 1024*100) { // 建立陣列,儲存檔案的所有切片 let fileChunks = []; for (let cur = 0; cur < file.size; cur += size) { // file.slice 方法用於切割檔案,從 cur 位元組開始,切割到 cur+size 位元組 fileChunks.push(file.slice(cur, cur + size)); } return fileChunks; }; createFileChunks 方法接收兩個引數

  • 要進行切片的檔案物件
  • 切片大小,這裡設定預設值為 1024*100,單位為位元組

1.2.2 拼接 formData

上傳的時候,通過 formData 物件組裝要上傳的切片資料

js /** * 2、拼接 formData * 引數1:儲存檔案切片資訊的陣列 * 引數2:上傳時的檔名稱 */ const concatFormData = function (fileChunks, filename) { /** * map 方法會遍歷切片陣列 fileChunks中的元素map 方法會遍歷切片陣列 fileChunks中的元素, * 陣列中有多少個切片,建立幾個 formData,在其中上傳的檔名稱、hash值和切片,並將此 formData * 返回,最終chunksList中儲存的就是多個 formData(每個切片對應一個 formData) * */ const chunksList = fileChunks.map((chunk, index) => { let formData = new FormData(); // 這個'filename' 字串的名字要與後端約定好 formData.append("filename", filename); // 作為區分每個切片的編號,後端會以此作為切片的檔名稱,此名稱也應該與後端約定好 formData.append("hash", index); // 後端會以此作為切片檔案的內容 formData.append("chunk", chunk); return { formData, }; }); return chunksList; };

1.2.3 上傳切片

遍歷上面的 chunksList 陣列,呼叫 axios 對每個 formData 資訊進行提交

js // 3、上傳切片 const uploadChunks=async (chunksList)=>{ const uploadList = chunksList.map(({ formData }) => axios({ method: "post", url: "/upload", data: formData, }) ); await Promise.all(uploadList); }

1.2.4 合併切片

當所有切片都已經上傳成功後,告訴後端一聲

js // 合併切片 const mergeFileChunks = async function (filename) { await axios({ method: "get", url: "/merge", params: { filename, }, }); };

1.2.5 方法組合

上面編寫了幾個函式,下面將幾個方法串聯起來,實現切片上傳功能

為上傳按鈕繫結單擊事件

js document .getElementById("uploadBtn") .addEventListener("click", handleFileUpload);

handleFileUpload 函式

```js // 大檔案上傳 async function handleFileUpload(event) { event.preventDefault();

const file = window.file;
if (!file) return;
// 1、切片切割,第二個引數採用預設值
const fileChunks = createFileChunks(file);
// 2、將切片資訊拼接成 formData 物件
const chunksList = concatFormData(fileChunks, file.name);
// 3、上傳切片
await uploadChunks(chunksList);
// 4、所有切片上傳成功後後,再告訴後端所有切片都已完成
await mergeFileChunks(file.name);
console.log("上傳完成");

}

```

1.2.6 完整程式碼

```js

大檔案上傳

```

2. 後端程式碼實現

因為後端不是我們主要關注點,所以直接上程式碼,就不做太過詳細的解釋了,有以下幾點提起注意

  • 因為前端通過 Promise.all 的方式執行所有的請求,所以切片傳送的順序是隨機的,也就是說,後端獲取的切片並儲存切片的順序可能是隨機的,所以切片檔案的名稱不一定是從小到大排序的,所以讀取切片組成檔案時,要先按照切片名稱從小答案排序,然後再組合,否則檔案可能出錯,這在上傳大檔案的時候非常明顯

```js const multiparty = require("multiparty"); const EventEmitter = require("events"); const express = require("express"); const cors = require("cors"); const fs = require("fs"); const path = require("path"); const { Buffer } = require("buffer");

const server = express(); server.use(cors());

const STATIC_TEMPORARY = path.resolve(__dirname, "static/temporary"); const STATIC_FILES = path.resolve(__dirname, "static/files");

server.post("/upload", (req, res) => { const multipart = new multiparty.Form(); const myEmitter = new EventEmitter();

const formData = { filename: undefined, hash: undefined, chunk: undefined, };

let isFieldOk = false, isFileOk = false;

multipart.parse(req, function (err, fields, files) { formData.filename = fields["filename"][0]; formData.hash = fields["hash"][0];

isFieldOk = true;
myEmitter.emit("start");

});

multipart.on("file", function (name, file) { formData.chunk = file; isFileOk = true; myEmitter.emit("start"); });

myEmitter.on("start", function () { if (isFieldOk && isFileOk) { const { filename, hash, chunk } = formData; const dir = ${STATIC_TEMPORARY}/${filename};

  try {
    if (!fs.existsSync(dir)) fs.mkdirSync(dir);

    const buffer = fs.readFileSync(chunk.path);
    const ws = fs.createWriteStream(`${dir}/${hash}`);
    ws.write(buffer);
    ws.close();

    res.send(`${filename}-${hash} 切片上傳成功`);
  } catch (error) {
    console.error(error);
  }

  isFieldOk = false;
  isFileOk = false;
}

}); });

server.get("/merge", async (req, res) => { const { filename } = req.query;

try { let len = 0; const hash_arr = fs.readdirSync(${STATIC_TEMPORARY}/${filename}); // 將 hash 值按照大小進行排序 hash_arr.sort((n1, n2) => { return Number(n1) - Number(n2); }); const bufferList = hash_arr.map((hash) => { console.log(hash); const buffer = fs.readFileSync(${STATIC_TEMPORARY}/${filename}/${hash}); len += buffer.length; return buffer; });

const buffer = Buffer.concat(bufferList, len);
const ws = fs.createWriteStream(`${STATIC_FILES}/${filename}`);
ws.write(buffer);
ws.close();

res.send(`切片合併完成`);

} catch (error) { console.error(error); } });

function deleteFolder(filepath) { if (fs.existsSync(filepath)) { fs.readdirSync(filepath).forEach((filename) => { const fp = ${filepath}/${filename}; if (fs.statSync(fp).isDirectory()) deleteFolder(fp); else fs.unlinkSync(fp); }); fs.rmdirSync(filepath); } }

server.listen(3000, () => { console.log("Server is running at http://127.0.0.1:3000"); });

```

3. vue 改造

當然只需要改造前端程式碼,後端程式碼是不用修改的

新建單檔案元件

```js

```