前端大文件上傳,即以流的方式上傳
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
```