Vue.js+express利用切片實現大文件斷點續(xù)傳
斷點續(xù)傳
在文件上傳期間因為一些事情中斷(比如網絡中斷,服務器出錯,客戶端奔潰),但是在下次上傳同一個文件可以從上一次上傳的位置繼續(xù)上傳,以節(jié)省上傳時間。
實現思路
- 將文件上傳分為多個切片
- 上傳切片
- 合并切片
- 文件切片驗證
- 狀態(tài)的切換
1.文件如何在前端分切片
我們需要為文件生成唯一fileHash,目的是為了每次上傳判斷此文件是否之前存在過斷點,如果hash相同同時存在斷點,那么就需要斷點續(xù)傳,如果上傳成功過就直接重新上傳即可。
使用Blob對象中的slice函數可以進行對文件進行切片處理。可以將文件切為自定義的大小和數量.
創(chuàng)建一個createChunl函數
const createFileChunk = async (file, size = SIZE) => {
const fileChunkList = [];
let spark = new SparkMd5.ArrayBuffer();
let readerComplete = false;
const reader = new FileReader();
for (let cur = 0; cur < file.size; cur += size) {
let data = { file: file.slice(cur, cur + size) };
fileChunkList.push(data);
reader.readAsArrayBuffer(data.file);
await new Promise((resolve) => {
reader.onload = (e) => {
spark.append(e.target.result);
resolve();
};
});
}
fileHash.value = spark.end();
return fileChunkList;
};spark-md5是一個高效的md5加密算法,詳情請看文檔spark-md5,通過spark-md5將文件內容加密為一個hash值,用來作為文件上傳的唯一標識。
創(chuàng)建一個upload函數
const handleUpload = async () => {
if (!uploadFile.value) return;
loading.value = true;
const fileChunkList = await createFileChunk(uploadFile.value);
let vertifyRes = await request({
url: "/vertify",
method: "post",
data: {
fileHash: fileHash.value,
filename: fileHash.value + "." + getSuffix(uploadFile.value.name),
},
});
if (!vertifyRes.data.shouldUpload) {
alert(vertifyRes.msg);
uploadPercentage.value = 100;
loading.value = false;
return;
}
data.value = fileChunkList.map(({ file }, index) => ({
chunk: file,
hashPrefix: fileHash.value,
suffix: getSuffix(uploadFile.value.name),
hash: fileHash.value + "-" + index,
index: index,
percentage: 0,
}));
await uploadChunk(vertifyRes.data.uploadList);
loading.value = false;
};上傳文件切片,首先需要驗證hash值是否在服務端已經存在如果存在實際上相同的文件已經存在了,當效驗未通過時說明不需要再上傳了,只有當效驗通過是才可以上傳文件,驗證接口的響應數據中會帶有切片的hash值,為了是過濾掉已上傳的切片提高上傳效率。
剩下的前端只需要過濾hash的切片,將剩下的切片上傳即可
// 上傳切片
const uploadChunk = async (uploadList = []) => {
const requestList = data.value
.filter(({ hash }) => !uploadList.includes(hash))
.map(({ chunk, hash, index, hashPrefix }) => {
const formData = new FormData();
formData.append("chunk", chunk);
formData.append("hash", hash);
formData.append("filename", uploadFile.value.name);
formData.append("index", index);
formData.append("hashPrefix", hashPrefix);
return { formData, index };
})
.map(async ({ formData, index }) => {
return request({
url: "/upload_chunk",
data: formData,
onUploadProgress: onPregress.bind(null, data.value[index]),
cancelToken: new axios.CancelToken((c) => {
aborts.value.push(c);
}),
headers: {
"Content-Type": "multipart/form-data",
},
method: "post",
});
});
showStopAndResume.value = true;
await Promise.all(requestList);
// 合并切片
await mergeRequest();
aborts.value = [];
};這里的mergeRequest是當所有的切片上傳成功后,要告訴后端進行切片合并。
const mergeRequest = async () => {
let res = await request({
url: "/merge_chunk",
headers: {
"Content-Type": "application/json",
},
method: "post",
data: {
originName: uploadFile.value.name,
filename: fileHash.value + "." + getSuffix(uploadFile.value.name),
size: SIZE,
},
});
};這樣前端一些核心的代碼基本完成了。
2.文件如何在后端實現切片上傳
后端暫且使用express實現
根據前端的要求有以下幾個接口需要實現
- /upload_chunk
- /merge_chunk
- /vertify
實現思路
1./upload_chunk
利用fs相關api將上傳的緩存chunk資源移動到對應目錄中
const multipart = new multiparty.Form({
maxFieldsSize: 200 * 1024 * 1024,
});
multipart.parse(req, async (err, fields, files) => {
if (err) {
res.send({
code: 500,
msg: "服務器錯誤",
});
return;
}
const [chunk] = files.chunk;
const [hash] = fields.hash;
const [hashPrefix] = fields.hashPrefix;
const chunkDir = path.resolve(UPLOAD_DIR, hashPrefix);
if (!fse.existsSync(chunkDir)) {
await fse.mkdirs(chunkDir);
}
await fse.move(chunk.path, `${chunkDir}/${hash}`);
res.send({
code: 200,
msg: "上傳成功",
});
});2./merge_chunk
將上傳的chunk按照hash-index序號進行順序合并,用到比較核心的api就是fs.createReadStream()
// 合并切片
const mergeFileChunk = async (filePath, filename, size) => {
const chunkDir = path.resolve(
UPLOAD_DIR,
filename.slice(0, filename.lastIndexOf("."))
);
const chunkPaths = await fse.readdir(chunkDir);
// 根據切片下標進行排序
// 否則直接讀取目錄的獲取的順序可能會錯亂
chunkPaths.sort((a, b) => a.split("-")[1] - b.split("-")[1]);
let pipeP = chunkPaths.map((chunkPath, index) =>
pipeStream(
path.resolve(chunkDir, chunkPath),
fse.createWriteStream(filePath, {
start: index * size,
end: (index + 1) * size,
})
)
);
await Promise.all(pipeP);
fse.rmdirSync(chunkDir); // 合并后刪除保存的切片目錄
};
const pipeStream = (path, writeStream) => {
return new Promise((resolve) => {
const readStream = fse.createReadStream(path);
readStream.on("end", () => {
fse.unlinkSync(path);
resolve();
});
readStream.pipe(writeStream);
});
};
app.post("/merge_chunk", async (req, res) => {
// size是每一個chunk的size
const { filename, size } = req.body;
const filePath = path.resolve(UPLOAD_DIR, `${filename}`);
await mergeFileChunk(filePath, filename, size);
res.send({
code: 200,
msg: "file merged success",
});
});創(chuàng)建chunk切片路徑的讀入流,然后將讀入流寫入新的地址,當所有切片全部寫入完成,完整的文件就合并成功了。
3./vertify
這個api比較簡單,用來查看當前文件是否存在服務器資源目錄中,存在返回shouldUpload為false,不存在就需要獲取此文件上傳的切片數組用于過濾前端不需要上傳的切片
const createUploadedList = async (fileHash) =>
fse.existsSync(path.resolve(UPLOAD_DIR, fileHash))
? await fse.readdir(path.resolve(UPLOAD_DIR, fileHash))
: [];
app.post("/vertify", async (req, res) => {
const { filename, fileHash } = req.body;
const filePath = path.resolve(UPLOAD_DIR, filename);
if (fse.existsSync(filePath)) {
res.send({
code: 200,
data: {
shouldUpload: false,
},
msg: "文件已上傳成功",
});
return;
}
res.send({
code: 200,
data: {
shouldUpload: true,
uploadList: await createUploadedList(fileHash),
},
msg: "請求成功",
});
});到此,大文件上傳基本工作就已經完成了。其中最核心的思想就是切片思想,將大的文件進行唯一標識+切片上傳,服務器進行先緩存切片在進行合并操作。保證了斷點可續(xù)傳,大文件穩(wěn)定上傳。
到此這篇關于Vue.js+express利用切片實現大文件斷點續(xù)傳的文章就介紹到這了,更多相關Vue express 大文件斷點續(xù)傳內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
淺談Vuex的this.$store.commit和在Vue項目中引用公共方法
這篇文章主要介紹了淺談Vuex的this.$store.commit和在Vue項目中引用公共方法,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-07-07
uni-app中App與webview雙向實時通信詳細代碼示例
在移動應用開發(fā)中,uni-app是一個非常流行的框架,它允許開發(fā)者使用一套代碼庫構建多端應用,包括H5、小程序、App等,這篇文章主要給大家介紹了關于uni-app中App與webview雙向實時通信的相關資料,需要的朋友可以參考下2024-07-07

