- 本文略長,建議收藏,文末會附上完整前後端程式碼(vue2&vue3+springboot)
- 湊合算是一套解決方案吧???
- 前端vscode大家都有,後端大家需要下載一個idea,搞一下maven,這一點可以請後端同事幫忙
- 對於普通的單個的大檔案上傳需求,應該可以應對
- 筆者本地測試,兩三個G的大檔案沒有問題,線上嘛,你懂的
大檔案上傳問題描述
問題背景
筆者的一個好友上個月被裁,最近在面試求職,在面試時,最後一個問題是問他有沒有做過大檔案上傳功能,我朋友說沒做過...
現在的就業環境不太好,要求都比之前高一些,當然也有可能面試官刷面試KPI的,或者這個崗位不急著找人,慢慢面試唄。畢竟也算是自己的工作量,能寫進週報裡面...
對於我們每個人而言:【生於黑暗,追逐黎明————《異獸迷城》】
既然面試官會問,那我們們就一起來看看,大檔案上傳功能如何實現吧...
中等檔案上傳解決方案-nginx放行
在我們工作中,上傳功能最常見的就是excel的上傳功能,一般來說,一個excel的大小在10MB以內吧,如果有好幾十MB的excel,就勉強算是中等檔案吧,此時,我們需要設定nginx的client_max_body_size
值,將其放開,只不過一次上傳一個幾十MB的檔案,介面會慢一些,不過也勉強能夠接受。
前端手握狼牙棒,後端手持流星錘,對產品朗聲笑道:要是不能接受,就請忍受???
但是,如果一個檔案有幾百兆,或者好幾個G呢?上述方式就不合適了。
既然一次性上傳不行,那麼我們們就把大檔案拆分開來,進行分批、分堆、分片、一點點上傳的操作,等上傳完了,再將一片片檔案合併一起,再恢復成原來的樣子即可
最常見的這個需求,就是影片的上傳,比如:騰訊影片創作平臺、嗶哩嗶哩後臺等...
大檔案上傳解決方案-檔案分片
一共三步即可:
- 第一步,大檔案拆分成一片又一片(分片操作)
- 第二步,每一次請求給後端帶一片檔案(分片上傳)
- 第三步,當每一片檔案都上傳完,再發請求告知後端將分片的檔案合併即可(合併分片)
檔案分片操作大致可分為上述三步驟,但在這三步驟中,還有一些細節需要我們注意,這個後文中會一一說到,我們繼續往下閱讀
大檔案上傳效果圖
為便於更好理解,我們看一下已經做好的效果圖:
由上述效果圖,我們可以看到,一個58MB的大檔案,被分成了12片上傳,很快啊
!上傳完成。
思考兩個問題:
- 若某個檔案已經存在(曾經上傳過),那我還需要上傳嗎?
- 若同一時刻,兩個人都在分片上傳完大檔案,併發起合併請求,如何才能保證不合並錯呢?如A檔案分片成a1,a2,a3;B檔案分片成b1,b2,b3 。合併操作肯定不能把a1,a2,a3檔案內容合併到B檔案中去。
解決方案就是:
- 要告知後端我這次上傳的檔案是哪一個,下次上傳的檔案又是哪一個
- 就像我們去修改表格中的某條資料時,需要有一個固定的引數id,告知後端去update具體的那一條資料
- 知道具體的檔案id,就不會操作錯了
那新的問題又來了:
前端如何才能確定檔案的id,如何才能得到檔案的唯一標識?
如何得到檔案唯一標識?
樹上沒有兩片相同的葉子,天上沒有兩朵相同的雲彩,檔案是獨一無二的(前提是內容不同,複製一份的不算)
who know?
spark-md5怪笑一聲: 寡人知曉!
什麼是spark-md5?
spark-md5是基於md5的一種優秀的演算法,用處很多,其中就可以去計算檔案的唯一身份證標識-hash值
- 只要檔案內容不同(包含的二進位制01不同),那麼使用spark-md5這個npm包包,得到的結果hash值就不一樣
- 這個獨一無二的hash值,就可以看做大檔案的id
- 發請求時,就可以將這個大檔案的hash值唯一id帶著傳給後端,後端就知道去操作那個檔案了
當然還有別的工具庫,如CryptoJS也可以計算檔案的hash值,不過spark-md5更主流、更優秀
使用spark-md5直接計算整個檔案的hash值(唯一id身份證標識)
直接計算一整個檔案的hash值:
<script src="https://cdn.bootcdn.net/ajax/libs/spark-md5/3.0.2/spark-md5.min.js"></script>
<input type="file" @change="changeFile">
<script>
const inputDom = document.querySelector('input') // 獲取input檔案標籤的dom元素
inputDom.onchange = (e) => {
let file = inputDom.files[0] // 拿到檔案
let spark = new SparkMD5.ArrayBuffer() // 例項化spark-md5
let fileReader = new FileReader() // 例項化檔案閱讀器
fileReader.onload = (e) => {
spark.append(e.target.result) // 新增到spark演算法中計算
let hash = spark.end() // 計算完成得到hash結果
console.log('檔案的hash值為:', hash);
}
fileReader.readAsArrayBuffer(file) // 開始閱讀這個檔案,閱讀完成觸發onload方法
}
</script>
直接計算一個整檔案的hash值,檔案小的話,還是比較快的,但是當檔案比較大的時,直接計算一整個檔案的hash值,就會比較慢了。
此刻大檔案分片的好處,再一次體現出來:大檔案分片不僅僅可以用於傳送請求傳遞給後端,也可以用於計算大檔案的hash值,直接計算一個大檔案hash值任務慢,那就拆分成一些小任務,這樣效率也提升了不少
至此,又延伸出一個問題,如何給大檔案分片?
當我們想解決一個A問題時,我們發現需要進一步,解決其中包含a1問題,當我們想要解決a1問題時,我們發現需要再進一步解決a1的核心a11問題。當a11問題被解決時,a1也就解決了,與此同時A問題也就迎刃而解了
給檔案分片操作
- 檔案分片,別名檔案分堆,又名檔案分塊,也叫作檔案拆分
- 類比,一個大的字串可以擷取slice(切割)成好幾個小的字串
- 同理,一個大檔案也可以slice成好多小檔案,對應api: file.slice
- 檔案file是特殊的二進位制blob檔案(所以file可以用blob的方法)
- 上程式碼
const inputDom = document.querySelector('input') // 獲取input檔案標籤的dom元素
inputDom.onchange = (e) => {
let file = inputDom.files[0] // 拿到檔案
function sliceFn(file, chunkSize = 1 * 1024 * 1024) {
const result = [];
// 從第0位元組開始切割,一次切割1 * 1024 * 1024位元組
for (let i = 0; i < file.size; i = i + chunkSize) {
result.push(file.slice(i, i + chunkSize));
}
return result;
}
const chunks = sliceFn(file)
console.log('檔案分片成陣列', chunks);
}
檔案分片結果效果圖(比如我選了一個5兆多的檔案去分片):
大檔案分片後搭配spark-md5計算整個檔案的hash值
有了上述分好片的chunks陣列(陣列中存放一片又一片小檔案),再結合spark-md5,使用遞迴的寫法,一片一片的再去讀取計算,最終算出結果
/**
* chunks:檔案分好片的陣列、progressCallbackFn回撥函式方法,用於告知外界進度的
* 因為檔案閱讀器是非同步的,所以要套一層Promise方便拿到非同步的計算結果
**/
function calFileMd5Fn(chunks, progressCallbackFn) {
return new Promise((resolve, reject) => {
let currentChunk = 0 // 準備從第0塊開始讀
let spark = new SparkMD5.ArrayBuffer() // 例項化SparkMD5用於計算檔案hash值
let fileReader = new FileReader() // 例項化檔案閱讀器用於讀取blob二進位制檔案
fileReader.onerror = reject // 兜一下錯
fileReader.onload = (e) => {
progressCallbackFn(Math.ceil(currentChunk / chunks.length * 100)) // 丟擲一個函式,用於告知進度
spark.append(e.target.result) // 將二進位制檔案追加到spark中(官方方法)
currentChunk = currentChunk + 1 // 這個讀完就加1,讀取下一個blob
// 若未讀取到最後一塊,就繼續讀取;否則讀取完成,Promise帶出結果
if (currentChunk < chunks.length) {
fileReader.readAsArrayBuffer(chunks[currentChunk])
} else {
resolve(spark.end()) // resolve出去告知結果 spark.end官方api
}
}
// 檔案讀取器的readAsArrayBuffer方法開始讀取檔案,從blob陣列中的第0項開始
fileReader.readAsArrayBuffer(chunks[currentChunk])
})
}
使用:
inputDom.onchange = (e) => {
let file = inputDom.files[0]
function sliceFn(file, chunkSize = 1 * 1024 * 1024) {
const result = [];
for (let i = 0; i < file.size; i = i + chunkSize) {
result.push(file.slice(i, i + chunkSize));
}
return result;
}
const chunks = sliceFn(file)
// 分好片的大檔案陣列,去計算hash。progressFn為進度條函式,需額外定義
const hash = await calFileMd5Fn(chunks,progressFn)
// "233075d0c65166792195384172387deb" // 32位的字串
}
至此,我們大檔案分片上傳操作,已經完成了三分之一了。我們已經完成了大檔案的分片和計算大檔案的hash值唯一身份證id(實際上,計算大檔案的hash值,還是挺耗費時長的,最佳化方案就是開一個輔助執行緒進行非同步計算操作,不過這個是最佳化的點,文末會提到)
接下來,就到了第二步,發請求環節:將已經分好片的每一片和這個大檔案的hash值作為引數傳遞給後端(當然還有別的引數,比如檔名、檔案分了多少片,每次上傳的是那一片【索引】等---看後端定義)
大檔案上傳解決方案:
- 第一步,大檔案拆分成一片又一片(分片操作)✔️
- 第二步,每一次請求給後端帶一片檔案(分片上傳)
- 第三步,當每一片檔案都上傳完,再發請求告知後端將分片的檔案合併即可
分片上傳發請求,一片就是一請求
詩曰:
分片上傳發請求,一片就是一請求。
請求之前帶校驗,這樣操作才規範。
分片上傳請求前的校驗請求
校驗邏輯思路如下:
- 大檔案分好片以後,在分片檔案上傳前,先發個請求帶著大檔案的唯一身份證標識hash值,去問問後端有沒有上傳過這個檔案,或者服務端的這個檔案是否上傳完整(比如曾經上傳一半的時候,突然斷網了,或者重新整理網頁導致上傳中斷)
- 後端去看看已經操作完成的資料夾中的檔案,有沒有叫做這個hash的,根據有沒有返回不同的狀態碼
比如,如下狀態碼:
- 等於0表示沒有上傳過,直接上傳
- 等於1曾經上傳過,不需要再上傳了(或:障眼法檔案秒傳遞)
- 等於2表示曾經上傳過一部分,現在要繼續上傳
對應前端程式碼:
以下程式碼舉例是vue3的語法舉例,大家知道每一步做什麼即可,文章看完,建議大家去筆者的github倉庫把前後端程式碼,都拉下來跑起來,結合程式碼中的註釋,才能夠更好的理解
html結構
<template>
<div id="app">
<input ref="inputRef" class="inputFile" type="file" @change="changeFile" />
<div>大檔案 <span class="bigFileC">?</span> 分了{{ chunksCount }}片:</div>
<div class="pieceItem" v-for="index in chunksCount" :key="index">
<span class="a">{{ index - 1 }}</span>
<span class="b">?</span>
</div>
<div>計算此大檔案的hash值進度</div>
<div class="r">結果為: {{ fileHash }}</div>
<progress max="100" :value="hashProgress"></progress> {{ hashProgress }}%
<div>
<div>上傳檔案的進度</div>
<div class="r" v-show="fileProgress == 100">檔案上傳完成</div>
<progress max="100" :value="fileProgress"></progress> {{ fileProgress }}%
</div>
</div>
</template>
發校驗請求
/**
* 發請求,校驗檔案是否上傳過,分三種情況:見:fileStatus
* */
export function checkFileFn(fileMd5) {
return new Promise((resolve, reject) => {
resolve(axios.post(`http://127.0.0.1:8686/bigfile/check?fileMd5=${fileMd5}`))
})
}
const res = await checkFileFn(fileMd5);
// res.data.resultCode 為0 或1 或2
對應後端程式碼:
筆者後端程式碼是springboot,文末會附上程式碼,大家看一下
private String fileStorePath = "F:\kkk\";
// 大檔案上傳操作在F盤下的kkk資料夾中操作
/**
* @param fileMd5
* @Title: 判斷檔案是否上傳過,是否存在分片,斷點續傳
* @MethodName: checkBigFile
* @Exception
* @Description: 檔案已存在,1
* 檔案沒有上傳過,0
* 檔案上傳中斷過,2 以及現在有的陣列分片索引
*/
@RequestMapping(value = "/check", method = RequestMethod.POST)
@ResponseBody
public JsonResult checkBigFile(String fileMd5) {
JsonResult jr = new JsonResult();
// 秒傳
File mergeMd5Dir = new File(fileStorePath + "/" + "merge" + "/" + fileMd5);
if (mergeMd5Dir.exists()) {
mergeMd5Dir.mkdirs();
jr.setResultCode(1);//檔案已存在
return jr;
}
// 讀取目錄裡的所有檔案
File dir = new File(fileStorePath + "/" + fileMd5);
File[] childs = dir.listFiles();
if (childs == null) {
jr.setResultCode(0);//檔案沒有上傳過
} else {
jr.setResultCode(2);//檔案上傳中斷過,除了狀態碼為2,還有已上傳的檔案分片索引
List<String> list = Arrays.stream(childs).map(f->f.getName()).collect(Collectors.toList());
jr.setResultData(list.toArray());
}
return jr;
}
前端根據介面的狀態碼,作相應控制,沒上傳過正常操作,曾經上傳過了,就做個提示檔案已上傳。這裡需要特別注意一下,曾經上傳中斷的情況
特別情況:當前上傳的檔案曾經中斷過(斷點續傳)
我們來捋一下邏輯就明晰了:
- 假設一個大檔案分為了10片,對應檔案片的索引是0~9
- 在執行上傳的時候,發了10個請求,分別帶上對應的索引檔案片
- 由於不可抗力因素,導致只上傳成功了3片檔案,分別是索引0、索引8、索引9
- 還有索引1、2、3、4、5、6、7這七片檔案沒上傳成功
- 那麼在檢查檔案時,後端除了返回狀態碼2,同時也返回後端已經上傳成功的片的索引有哪些
- 即:
{resultCode:2 , resultData:[0,8,9]}
- 我們在執行上傳檔案操作時候,去掉這三個已經上傳完成的即可,上傳那些未完成的
// 等於2表示曾經上傳過一部分,現在要繼續上傳
if (res.data.resultCode == 2) {
// 若是檔案曾上傳過一部分,後端會返回上傳過得部分的檔案索引,前端透過索引可以知道哪些
// 上傳過,做一個過濾,已上傳的檔案就不用繼續上傳了,上傳未上傳過的檔案片
doneFileList = res.data.resultData.map((item) => {
return item * 1; // 後端給到的是字串索引,這裡轉成數字索引
});
}
doneFileList陣列儲存的就是後端返回的,曾經上傳過一部分的陣列分片檔案索引
比如下面這兩張圖,就是檔案曾經上傳中斷以後的,再次上傳的檢查介面返回的資料
示例圖一:
返回的是分片檔案的名,也就是分片的索引,如下圖:
前端根據doneFileList判斷,去準備引數
// 說明沒有上傳過,組裝一下,直接使用
if (doneFileList.length == 0) {
formDataList = chunks.map((item, index) => {
// 後端接參大致有:檔案片、檔案分的片數、每次上傳是第幾片(索引)、檔名、此完整大檔案hash值
// 具體後端定義的引數prop屬性名,看他們如何定義的,這個無妨...
let formData = new FormData();
formData.append("file", item); // 使用FormData可以將blob檔案轉成二進位制binary
formData.append("chunks", chunks.length);
formData.append("chunk", index);
formData.append("name", fileName);
formData.append("md5", fileMd5);
return { formData };
});
}
// 說明曾經上傳過,需要過濾一下,曾經上傳過的就不用再上傳了
else {
formDataList = chunks
.filter((index) => {
return !doneFileList.includes(index);
})
.map((item, index) => {
let formData = new FormData();
// 這幾個是後端需要的引數
formData.append("file", item); // 使用FormData可以將blob檔案轉成二進位制binary
formData.append("chunks", chunks.length);
formData.append("chunk", index);
formData.append("name", fileName);
formData.append("md5", fileMd5);
return { formData };
});
}
// 帶著分片陣列請求引數,和檔名 fileName = file.name
// 準備一次併發很多的請求
fileUpload(formDataList, fileName);
上述程式碼實現了,正常上傳以及曾經中斷過的檔案繼續上傳,這就是斷點續傳
上述程式碼實現了,正常上傳以及曾經中斷過的檔案繼續上傳,這就是斷點續傳
上述程式碼實現了,正常上傳以及曾經中斷過的檔案繼續上傳,這就是斷點續傳
使用Promise.allSettled(arr)併發上傳分好片的檔案
- 使用Promise.allSettled發請求好一些,掛了的就掛了,不影響後續不掛的分片上傳請求
- Promise.all則不行,一個掛了都掛了
前端程式碼
const fileUpload = (formDataList, fileName) => {
const requestListFn = formDataList.map(async ({ formData }, index) => {
const res = await sliceFileUploadFn(formData);
// 每上傳完畢一片檔案,後端告知已上傳了多少片,除以總片數,就是進度
fileProgress.value = Math.ceil(
(res.data.resultData / chunksCount.value) * 100
);
return res;
});
// 使用allSettled發請求好一些,掛了的就掛了,不影響後續不掛的請求
Promise.allSettled(requestListFn).then((many) => {
// 都上傳完畢了,檔案上傳進度條就為100%了
});
};
後端程式碼
/**
* 上傳檔案
* @param param
* @param request
* @return
* @throws Exception
*/
@RequestMapping(value = "/upload", method = RequestMethod.POST)
@ResponseBody
public JsonResult filewebUpload(MultipartFileParam param, HttpServletRequest request) {
JsonResult jr = new JsonResult();
boolean isMultipart = ServletFileUpload.isMultipartContent(request);
// 檔名
String fileName = param.getName();
// 檔案每次分片的下標
int chunkIndex = param.getChunk();
if (isMultipart) {
File file = new File(fileStorePath + "/" + param.getMd5());
if (!file.exists()) { // 沒有檔案建立檔案
file.mkdir();
}
File chunkFile = new File(
fileStorePath + "/" + param.getMd5() + "/" + chunkIndex);
try {
FileUtils.copyInputStreamToFile(param.getFile().getInputStream(), chunkFile); // 流檔案操作
} catch (Exception e) {
jr.setResultCode(-1);
e.printStackTrace();
}
}
logger.info("檔案-:{}的小標-:{},上傳成功", fileName, chunkIndex);
File dir = new File(fileStorePath + "/" + param.getMd5());
File[] childs = dir.listFiles();
if(childs!=null){
jr.setResultData(childs.length); // 返回上傳了幾個,即為上傳進度
}
return jr;
}
最後別忘了去合併這些檔案分片
添一個上傳檔案的效果圖
- 由上述動態圖,我們可以看到把檔案切割成了12份,所以傳送了12個上傳分片請求
- 當然,上傳完成以後,最後,再發一個請求,告知後端去合併這些一片片檔案即可
- 即merge請求,當然也要帶上此大檔案的hash值
- 告知後端具體合併哪一個檔案,這樣才不會出錯
前端程式碼
// 使用allSettled發請求好一些,掛了的就掛了,不影響後續不掛的請求
Promise.allSettled(requestListFn).then(async (many) => {
// 都上傳完畢了,檔案上傳進度條就為100%了
fileProgress.value = 100;
// 最後再告知後端合併一下已經上傳的檔案碎片了即可
const loading = ElLoading.service({
lock: true,
text: "檔案合併中,請稍後???...",
background: "rgba(0, 0, 0, 0.7)",
});
const res = await tellBackendMergeFn(fileName, fileHash.value);
if (res.data.resultCode === 0) {
console.log("檔案併合成功,大檔案上傳任務完成");
loading.close();
} else {
console.log("檔案併合失敗,大檔案上傳任務未完成");
loading.close();
}
});
後端程式碼
/**
* 分片上傳成功之後,合併檔案
* @param request
* @return
*/
@RequestMapping(value = "/merge", method = RequestMethod.POST)
@ResponseBody
public JsonResult filewebMerge(HttpServletRequest request) {
FileChannel outChannel = null;
JsonResult jr = new JsonResult();
int code =0;
try {
String fileName = request.getParameter("fileName");
String fileMd5 = request.getParameter("fileMd5");
// 讀取目錄裡的所有檔案
File dir = new File(fileStorePath + "/" + fileMd5);
File[] childs = dir.listFiles();
if (Objects.isNull(childs) || childs.length == 0) {
jr.setResultCode(-1);
return jr;
}
// 轉成集合,便於排序
List<File> fileList = new ArrayList<File>(Arrays.asList(childs));
Collections.sort(fileList, new Comparator<File>() {
@Override
public int compare(File o1, File o2) {
if (Integer.parseInt(o1.getName()) < Integer.parseInt(o2.getName())) {
return -1;
}
return 1;
}
});
// 合併後的檔案
File outputFile = new File(fileStorePath + "/" + "merge" + "/" + fileMd5 + "/" + fileName);
// 建立檔案
if (!outputFile.exists()) {
File mergeMd5Dir = new File(fileStorePath + "/" + "merge" + "/" + fileMd5);
if (!mergeMd5Dir.exists()) {
mergeMd5Dir.mkdirs();
}
logger.info("建立檔案");
outputFile.createNewFile();
}
outChannel = new FileOutputStream(outputFile).getChannel();
FileChannel inChannel = null;
try {
for (File file : fileList) {
inChannel = new FileInputStream(file).getChannel();
inChannel.transferTo(0, inChannel.size(), outChannel);
inChannel.close();
// 刪除分片
file.delete();
}
} catch (Exception e) {
code =-1;
e.printStackTrace();
//發生異常,檔案合併失敗 ,刪除建立的檔案
outputFile.delete();
dir.delete();//刪除資料夾
} finally {
if (inChannel != null) {
inChannel.close();
}
}
dir.delete(); //刪除分片所在的資料夾
} catch (IOException e) {
code =-1;
e.printStackTrace();
} finally {
try {
if (outChannel != null) {
outChannel.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
jr.setResultCode(code);
return jr;
}
}
至此,大檔案上傳的三步都完成了
大檔案上傳解決方案:
- 第一步,大檔案拆分成一片又一片(分片操作)✔️
- 第二步,每一次請求給後端帶一片檔案(分片上傳)✔️
- 第三步,當每一片檔案都上傳完,再發請求告知後端將分片的檔案合併即可✔️
- 筆者用本機測試了一下,兩三個G的檔案都是沒有問題的
- 實際專案上線,大檔案上傳功能,會受到網路頻寬、裝置效能等各種因素影響
- 一定要注意檔案分片時切分的大小,例:
CHUNK_SIZE = 5 * 1024 * 1024;
- 這個檔案的大小決定了切割多少片,決定了併發多少請求(不可過大,也不可能非常小)
- 太大單個請求就太慢了,太小瀏覽器一次發幾千上萬個請求,也扛不住
輔助執行緒去最佳化
開啟輔助執行緒計算大檔案的hash值
首先,定義函式非同步,開啟輔助執行緒,計算
const calFileMd5ByThreadFn = (chunks) => {
return new Promise((resolve) => {
worker = new Worker("./hash.js"); // 例項化一個webworker執行緒
worker.postMessage({ chunks }); // 主執行緒向輔助執行緒傳遞資料,發分片陣列用於計算
worker.onmessage = (e) => {
const { hash } = e.data; // 輔助執行緒將相關計算資料發給主執行緒
hashProgress.value = e.data.hashProgress; // 更改進度條
if (hash) {
// 當hash值被算出來時,就可以關閉主執行緒了
worker.terminate();
resolve(hash); // 將結果帶出去
}
};
});
};
然後,在public目錄下新建hash.js去撰寫輔助執行緒程式碼
// 使用importScripts引入cdn使用
self.importScripts('https://cdn.bootcdn.net/ajax/libs/spark-md5/3.0.2/spark-md5.min.js')
self.onmessage = e => {
const { chunks } = e.data // 獲取到分片陣列
const spark = new self.SparkMD5.ArrayBuffer() // 例項化spark物件用於計算檔案hash
let currentChunk = 0
let fileReader = new FileReader()
fileReader.onload = (e) => {
spark.append(e.target.result)
currentChunk = currentChunk + 1
if (currentChunk < chunks.length) {
fileReader.readAsArrayBuffer(chunks[currentChunk])
// 未曾計算完只告知主執行緒計算進度
self.postMessage({
hashProgress: Math.ceil(currentChunk / chunks.length * 100)
})
} else {
// 計算完了進度和hash結果就都可以告知了
self.postMessage({
hash: spark.end(),
hashProgress: 100
})
self.close();
}
}
fileReader.readAsArrayBuffer(chunks[currentChunk])
}
使用的話,直接傳遞分好片檔案陣列引數即可
const fileMd5 = await calFileMd5ByThreadFn(chunks); // 根據分片計算
console.log('hash',fileMd5) // 得出此大檔案的hash值了
單純計算加減乘除啥的倒是可以使用vue-worker這個外掛,參見筆者之前的文章:https://segmentfault.com/a/1190000043411552
這樣的話,速度就會快一些了...
附錄
大檔案上傳流程圖
- 當我們把上述文章讀完以後,一個大檔案上傳的流程圖就清晰的浮現在我們的腦海中了
- 筆者用processOn畫了一個流程圖,如下:
程式碼倉庫
程式碼倉庫:https://github.com/shuirongshuifu/bigfile
歡迎star,您的認可是我們創作的動力哦
當下後端程式碼是java同事濤哥提供的,感謝之。
後續空閒了(star多了),筆者再補充node版本的後端程式碼吧
參考資料
- webuploader(百度團隊開源專案):http://fex.baidu.com/webuploader/
- 大檔案上傳:https://juejin.cn/post/7177045936298786872
- Buzut:https://github.com/Buzut/huge-uploader
思考
- 到這裡的話,普通公司的大檔案上傳需求(一次上傳一個),基本上湊合解決
- 本文的內容也應該基本上能應付面試官了
- 但是如何才能自己做到類似百度網盤那種上傳效果?請研究webuploader原始碼
- 道阻且長,還是需要我們持續最佳化的...