JavaScript大文件上傳的處理方法之切片上傳
前言
本篇介紹了切片上傳的基本實(shí)現(xiàn)方式(前端),以及實(shí)現(xiàn)切片上傳后的一些附加功能,切片上傳原理較為簡單,代碼注釋比較清晰就不多贅述了,后面的附加功能介紹了實(shí)現(xiàn)原理,并貼出了在原本代碼上的改進(jìn)方式。有什么錯(cuò)誤希望大佬可以指出,感激不盡。
切片后上傳
切片上傳的原理較為簡單,即獲取文件后切片,切片后整理好每個(gè)切片的參數(shù)并發(fā)請(qǐng)求即可。
下面直接上代碼:
HTML
<template>
<div>
<input type="file" @change="handleFileChange" />
<el-button @click="handleUpload">上傳</el-button>
</div>
</template>JavaScript
<script>
const SIZE = 10 * 1024 * 1024; // 切片大小
export default {
data: () => ({
// 存放文件信息
container: {
file: null
hash: null
},
data: [] // 用于存放加工好的文件切片列表
hashPercentage: 0 // 存放hash生成進(jìn)度
}),
methods: {
// 獲取上傳文件
handleFileChange(e) {
const [file] = e.target.files;
if (!file) {
this.container.file = null;
return;
}
this.container.file = file;
},
// 生成文件切片
createFileChunk(file, size = SIZE) {
const fileChunkList = [];
let cur = 0;
while (cur < file.size) {
fileChunkList.push({ file: file.slice(cur, cur + size) });
cur += size;
}
return fileChunkList;
},
// 生成文件hash
calculateHash(fileChunkList) {
return new Promise(resolve => {
this.container.worker = new Worker("/hash.js");
this.container.worker.postMessage({ fileChunkList });
this.container.worker.onmessage = e => {
const { percentage, hash } = e.data;
// 可以用來顯示進(jìn)度條
this.hashPercentage = percentage;
if (hash) {
resolve(hash);
}
};
});
},
// 切片加工(上傳前預(yù)處理 為文件添加hash等)
async handleUpload() {
if (!this.container.file) return;
// 切片生成
const fileChunkList = this.createFileChunk(this.container.file);
// hash生成
this.container.hash = await this.calculateHash(fileChunkList);
this.data = fileChunkList.map(({ file },index) => ({
chunk: file,
// 這里的hash為文件名 + 切片序號(hào),也可以用md5對(duì)文件進(jìn)行加密獲取唯一hash值來代替文件名
hash: this.container.hash + "-" + index
}));
await this.uploadChunks();
}
// 上傳切片
async uploadChunks() {
const requestList = this.data
// 構(gòu)造formData
.map(({ chunk,hash }) => {
const formData = new FormData();
formData.append("chunk", chunk);
formData.append("hash", hash);
formData.append("filename", this.container.file.name);
return { formData };
})
// 發(fā)送請(qǐng)求 上傳切片
.map(async ({ formData }) =>
request(formData)
);
await Promise.all(requestList); // 等待全部切片上傳完畢
await merge(this.container.file.name) // 發(fā)送請(qǐng)求合并文件
},
}
};
</script>生成hash
無論是前端還是服務(wù)端,都必須要生成文件和切片的 hash,之前我們使用文件名 + 切片下標(biāo)作為切片 hash,這樣做文件名一旦修改就失去了效果,而事實(shí)上只要文件內(nèi)容不變,hash 就不應(yīng)該變化,所以正確的做法是根據(jù)文件內(nèi)容生成 hash,所以我們修改一下 hash 的生成規(guī)則
這里用到另一個(gè)庫 spark-md5,它可以根據(jù)文件內(nèi)容計(jì)算出文件的 hash 值,另外考慮到如果上傳一個(gè)超大文件,讀取文件內(nèi)容計(jì)算 hash 是非常耗費(fèi)時(shí)間的,并且會(huì)引起 UI 的阻塞,導(dǎo)致頁面假死狀態(tài),所以我們使用 web-worker 在 worker 線程計(jì)算 hash,這樣用戶仍可以在主界面正常的交互
由于實(shí)例化 web-worker 時(shí),參數(shù)是一個(gè) js 文件路徑且不能跨域,所以我們單獨(dú)創(chuàng)建一個(gè) hash.js 文件放在 public 目錄下,另外在 worker 中也是不允許訪問 dom 的,但它提供了importScripts`函數(shù)用于導(dǎo)入外部腳本,通過它導(dǎo)入 spark-md5
// /public/hash.js
self.importScripts("/spark-md5.min.js"); // 導(dǎo)入腳本
// 生成文件 hash
self.onmessage = e => {
const { fileChunkList } = e.data;
const spark = new self.SparkMD5.ArrayBuffer();
let percentage = 0;
let count = 0;
const loadNext = index => {
// 新建讀取器
const reader = new FileReader();
// 設(shè)定讀取數(shù)據(jù)格式并開始讀取
reader.readAsArrayBuffer(fileChunkList[index].file);
// 監(jiān)聽讀取完成
reader.onload = e => {
count++;
// 獲取讀取結(jié)果并交給spark計(jì)算hash
spark.append(e.target.result);
if (count === fileChunkList.length) {
self.postMessage({
percentage: 100,
// 獲取最終hash
hash: spark.end()
});
self.close();
} else {
percentage += 100 / fileChunkList.length;
self.postMessage({
percentage
});
// 遞歸計(jì)算下一個(gè)切片
loadNext(count);
}
};
};
loadNext(0);
};小結(jié)
- 獲取上傳文件
- 文件切片后存入數(shù)組 fileChunkList.push({ file: file.slice(cur, cur + size) });
- 生成文件hash(非必須)
- 根據(jù)文件切片列表生成請(qǐng)求列表
- 并發(fā)請(qǐng)求
- 待全部請(qǐng)求完成后發(fā)送合并請(qǐng)求
文件秒傳
實(shí)際是障眼法,用來欺騙用戶的。
原理:在文件上傳之前先計(jì)算出文件的hash,然后發(fā)送給后端進(jìn)行驗(yàn)證,看后端是否存在這個(gè)hash,如果存在,則證明這個(gè)文件上傳過,則直接提示用戶秒傳成功
// 切片加工(上傳前預(yù)處理 為文件添加hash等)
async handleUpload() {
if (!this.container.file) return;
// 切片生成
const fileChunkList = this.createFileChunk(this.container.file);
// hash生成
this.container.hash = await this.calculateHash(fileChunkList);
// hash驗(yàn)證 (verify為后端驗(yàn)證接口請(qǐng)求)
const { haveExisetd } = await verify(this.container.hash)
// 判斷
if(haveExisetd) {
this.$message.success("秒傳:上傳成功")
return
}
this.data = fileChunkList.map(({ file },index) => ({
chunk: file,
// 這里的hash為文件名 + 切片序號(hào),也可以用md5對(duì)文件進(jìn)行加密獲取唯一hash值來代替文件名
hash: this.container.hash + "-" + index
}));
await this.uploadChunks();
}暫停上傳
原理:將所有的切片存在一個(gè)數(shù)組中,每當(dāng)一個(gè)切片上傳完畢,從數(shù)組中移除,這樣就可以實(shí)現(xiàn)用一個(gè)數(shù)組只保存上傳中的文件。此外,因?yàn)橐獣和I蟼鳎孕枰袛嗾?qǐng)求 axios中斷請(qǐng)求可以利用AbortController
中斷請(qǐng)求示例
const controller = new AbortController()
axios({
signal: controller.signal
}).then(() => {});
// 取消請(qǐng)求
controller.abort()
添加暫停上傳功能
// 上傳切片
async uploadChunks() {
// 需要把requestList放到全局,因?yàn)橐ㄟ^操控requestList來實(shí)現(xiàn)中斷
this.requestList = this.data
// 構(gòu)造formData
.map(({ chunk,hash }) => {
const formData = new FormData();
formData.append("chunk", chunk);
formData.append("hash", hash);
formData.append("filename", this.container.file.name);
return { formData };
})
// 發(fā)送請(qǐng)求 上傳切片
.map(async ({ formData }, index) =>
request(formData).then(() => {
// 將請(qǐng)求成功的請(qǐng)求剝離出requestList
this.requestList.splice(index, 1)
})
);
await Promise.all(this.requestList); // 等待全部切片上傳完畢
await merge(this.container.file.name) // 發(fā)送請(qǐng)求合并文件
},
// 暫停上傳
handlePause() {
this.requestList.forEach((req) => {
// 為每個(gè)請(qǐng)求新建一個(gè)AbortController實(shí)例
const controller = new AbortController();
req.signal = controller.signal
controller.abort()
})
}恢復(fù)上傳
原理:上傳切片之前,向后臺(tái)發(fā)送請(qǐng)求,接口將已上傳的切片列表返回,通過切片hash將后臺(tái)已存在的切片過濾,只上傳未存在的切片
// 切片加工(上傳前預(yù)處理 為文件添加hash等)
async handleUpload() {
if (!this.container.file) return;
// 切片生成
const fileChunkList = this.createFileChunk(this.container.file);
// 文件hash生成
this.container.hash = await this.calculateHash(fileChunkList);
// hash驗(yàn)證 (verify為后端驗(yàn)證接口請(qǐng)求)
const { haveExisetd, uploadedList } = await verify(this.container.hash)
// 判斷
if(haveExisetd) {
this.$message.success("秒傳:上傳成功")
return
}
this.data = fileChunkList.map(({ file },index) => ({
chunk: file,
// 注:這個(gè)是切片hash 這里的hash為文件名 + 切片序號(hào),也可以用md5對(duì)文件進(jìn)行加密獲取唯一hash值來代替文件名
hash: this.container.hash + "-" + index
}));
await this.uploadChunks(uploadedList);
}
// 上傳切片
async uploadChunks(uploadedList = []) {
// 需要把requestList放到全局,因?yàn)橐ㄟ^操控requestList來實(shí)現(xiàn)中斷
this.requestList = this.data
// 過濾出來未上傳的切片
.filter(({ hash }) => !uploadedList.includes(hash))
// 構(gòu)造formData
.map(({ chunk,hash }) => {
const formData = new FormData();
formData.append("chunk", chunk);
formData.append("hash", hash);
formData.append("filename", this.container.file.name);
return { formData };
})
// 發(fā)送請(qǐng)求 上傳切片
.map(async ({ formData }, index) =>
request(formData).then(() => {
// 將請(qǐng)求成功的請(qǐng)求剝離出requestList
this.requestList.splice(index, 1)
})
);
await Promise.all(this.requestList); // 等待全部切片上傳完畢
// 合并之前添加一層驗(yàn)證 驗(yàn)證全部切片傳送完畢
if(uploadedList.length + this.requestList.length == this.data.length){
await merge(this.container.file.name) // 發(fā)送請(qǐng)求合并文件
}
},
// 暫停上傳
handlePause() {
this.requestList.forEach((req) => {
// 為每個(gè)請(qǐng)求新建一個(gè)AbortController實(shí)例
const controller = new AbortController();
req.signal = controller.signal
controller.abort()
})
}
// 恢復(fù)上傳
async handleRecovery() {
//獲取已上傳切片列表 (verify為后端驗(yàn)證接口請(qǐng)求)
const { uploadedList } = await verify(this.container.hash)
await uploadChunks(uploadedList)
}添加功能總結(jié)
- 1.文件秒傳其實(shí)就是一個(gè)簡單的驗(yàn)證,把文件的hash發(fā)送給后端,后端驗(yàn)證是否存在該文件后將結(jié)果返回,如果存在則提示文件秒傳成功
- 2.斷點(diǎn)傳送分為兩步,暫停上傳和恢復(fù)上傳。暫停上傳是通過獲取到未上傳完畢切片列表(完整切片列表剝離請(qǐng)求已完成的切片后形成),對(duì)列表請(qǐng)求進(jìn)行請(qǐng)求中斷實(shí)現(xiàn)的?;謴?fù)上傳實(shí)質(zhì)也是一層驗(yàn)證,在上傳文件之前,將文件的hash發(fā)送給后端,后端返回已經(jīng)上傳完畢的切片列表,然后根據(jù)切片hash將后端返回的切片列表中的切片過濾出去,只上傳未上傳完成的切片。
到此這篇關(guān)于JavaScript大文件上傳的處理方法之切片上傳的文章就介紹到這了,更多相關(guān)JS切片上傳內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
不使用JavaScript實(shí)現(xiàn)菜單的打開和關(guān)閉效果demo
本文通過實(shí)例代碼給大家分享在不使用JavaScript實(shí)現(xiàn)菜單的打開和關(guān)閉效果,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友參考下吧2018-05-05
微信小程序?qū)崿F(xiàn)的canvas合成圖片功能示例
這篇文章主要介紹了微信小程序?qū)崿F(xiàn)的canvas合成圖片功能,結(jié)合實(shí)例形式分析了微信小程序canvas合成圖片相關(guān)組件使用、操作步驟與注意事項(xiàng),需要的朋友可以參考下2019-05-05
Next.js應(yīng)用轉(zhuǎn)換為TypeScript方法demo
這篇文章主要為大家介紹了Next.js應(yīng)用轉(zhuǎn)換為TypeScript方法demo,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12
通過循環(huán)優(yōu)化 JavaScript 程序
這篇文章主要介紹了通過循環(huán)優(yōu)化 JavaScript 程序,對(duì)于提高 JavaScript 程序的性能這個(gè)問題,最簡單同時(shí)也是很容易被忽視的方法就是學(xué)習(xí)如何正確編寫高性能循環(huán)語句。下面我們來學(xué)習(xí)一下吧2019-06-06

