前端大文件上傳,即以流的方式上傳

語言: 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

```