上一篇文章
【文件上傳系列】No.1 大文件分片、進度圖展示(原生前端 + Node 后端 & Koa)
秒傳效果展示
秒傳思路
整理的思路是:根據文件的二進制內容生成
Hash
值,然后去服務器里找,如果找到了,說明已經上傳過了,所以又叫做秒傳(笑)
整理文件夾、path.resolve()
介紹
接著上一章的內容,因為前端和后端的服務都寫在一起了,顯得有點凌亂,所以我打算分類一下
改了文件路徑的話,那么各種引用也要修改,引用就很好改了,這里就不多說了
這里講一下 path
的修改,為了方便修改 path
,引用了 path
依賴,使用 path.resolve()
方法就很舒服的修改路徑,常見的拼接方法如下圖測試:(如果不用這個包依賴的話,想一下如何返回上一個路徑呢?可能使用 split('/)[1]
類似這種方法吧。)
會使用這個包依賴之后就可以修改服務里的代碼了:
200
頁面正常!資源也都加載了!
前端
思路
具體思路如下
- 計算文件整體
hash
,因為不同的文件,名字可能相同,不具有唯一性,所以根據文件內容計算出來的hash
值比較靠譜,并且為下面秒傳做準備。- 利用 web-worker 線程:因為如果是很大的文件,那么分塊的數量也會很多,讀取文件計算
hash
是非常耗時消耗性能的,這樣會使頁面阻塞卡頓,體驗不好,解決的一個方法是,我們開一個新線程來計算hash
工作者線程簡介
《高級JavaScript程序設計》27 章簡介:
工作者線程的數據傳輸如下:
注意在 worker
中引入的腳本也是個請求!
// index.html
function handleCalculateHash(fileChunkList) {let worker = new Worker('./hash.js');worker.postMessage('你好 worker.js');worker.onmessage = function (e) {console.log('e:>>', e);};
}
handleCalculateHash();
// worker.js
self.onmessage = (work_e) => {console.log('work_e:>>', work_e);self.postMessage('你也好 index.html');
};
計算整體文件 Hash
前端拿到
Blob
,然后通過fileReader
轉化成ArrayBuffer
,然后用append()
方法灌入SparkMD5.ArrayBuffer()
實例中,最后SparkMD5.ArrayBuffer().end()
拿到hash
結果
SparkMD5 計算 Hash 性能簡單測試
js-spark-md5 的 github 地址
配置 x99 2643v3
六核十二線程 基礎速度:3.4GHz
,睿頻 3.6GHz
,只測試了一遍。
// 計算時間的代碼
self.onmessage = (e) => {const { data } = e;self.postMessage('你也好 index.html');const spark = new SparkMD5.ArrayBuffer();const fileReader = new FileReader();const blob = data[0].file;fileReader.readAsArrayBuffer(blob);fileReader.onload = (e) => {console.time('append');spark.append(e.target.result);console.timeEnd('append');spark.end();};
};
工作者線程:計算 Hash
這里有個注意點,就是我們一定要等到 fileReader.onload
讀完一個 chunk 之后再去 append
下一個塊,一定要注意這個順序,我之前想當然寫了個如下的錯誤版本,就是因為回調函數 onload
還沒被調用(文件沒有讀完),我這里只是定義了回調函數要干什么,但沒有保證順序是一塊一塊讀的。
// 錯誤版本
const chunkLength = data.length;
let curr = 0;
while (curr < chunkLength) {const blob = data[curr].file;curr++;const fileReader = new FileReader();fileReader.readAsArrayBuffer(blob);fileReader.onload = (e) => {spark.append(e.target.result);};
}
const hash = spark.end();
console.log(hash);
如果想保證在回調函數內處理問題,我目前能想到的辦法:一種方法是遞歸
,另一種方法是配合 await
這個是非遞歸版本的,比較好理解。
// 非遞歸版本
async function handleBlob2ArrayBuffer(blob) {return new Promise((resolve) => {const fileReader = new FileReader();fileReader.readAsArrayBuffer(blob);fileReader.onload = function (e) {resolve(e.target.result);};});
}
self.onmessage = async (e) => {const { data } = e;self.postMessage('你也好 index.html');const spark = new SparkMD5.ArrayBuffer();for (let i = 0, len = data.length; i < len; i++) {const eachArrayBuffer = await handleBlob2ArrayBuffer(data[i].file);spark.append(eachArrayBuffer); // 這個是同步的,可以 debugger 打斷點試一試。}const hash = spark.end();
};
遞歸的版本代碼比較簡潔
// 遞歸版本
self.onmessage = (e) => {const { data } = e;console.log(data);self.postMessage('你也好 index.html');const spark = new SparkMD5.ArrayBuffer();function loadNext(curr) {const fileReader = new FileReader();fileReader.readAsArrayBuffer(data[curr].file);fileReader.onload = function (e) {const arrayBuffer = e.target.result;spark.append(arrayBuffer);curr++;if (curr < data.length) {loadNext(curr);} else {const hash = spark.end();console.log(hash);return hash;}};}loadNext(0);
};
我們在加上計算 hash
進度的變量 percentage
就差不多啦
官方建議用小切塊計算體積較大的文件,點我跳轉官方包說明
ok 這個工作者線程的整體代碼如下:
importScripts('./spark-md5.min.js');
/*** 功能:blob 轉換成 ArrayBuffer* @param {*} blob* @returns*/
async function handleBlob2ArrayBuffer(blob) {return new Promise((resolve) => {const fileReader = new FileReader();fileReader.readAsArrayBuffer(blob);fileReader.onload = function (e) {resolve(e.target.result);};});
}/*** 功能:求整個文件的 Hash* - self.SparkMD5 和 SparkMD5 都一樣* - 1. FileReader.onload 處理 load 事件。該事件在讀取操作完成時觸發。* - 流程圖展示* - 注意這里的 percentage += 100 / len; 的位置,要放到后面* - 因為如果是小文件的話,塊的個數可能是1,最后 100/1 就直接是 100 了* ┌────┐ ┌───────────┐ ┌────┐* │ │ Object fileReader │ │ new SparkMD5.ArrayBuffer() │ │* │Blob│ ────────────────────────────────? │ArrayBuffer│ ───────────────┬──────────────────? │Hash│* │ │ Method readAsArrayBuffer │ │ append() └────? end() │ │* └────┘ └───────────┘ └────┘*/
self.onmessage = async (e) => {const { data } = e;const spark = new SparkMD5.ArrayBuffer();let percentage = 0;for (let i = 0, len = data.length; i < len; i++) {const eachArrayBuffer = await handleBlob2ArrayBuffer(data[i].file);percentage += 100 / len;self.postMessage({percentage,});spark.append(eachArrayBuffer);}const hash = spark.end();self.postMessage({percentage: 100,hash,});self.close();
};
主線程調用 Hash 工作者線程
把處理 hash
的函數包裹成 Promise
,前端處理完 hash
之后傳遞給后端
把每個chunk
的包裹也精簡了一下,只傳遞 Blob
和 index
再把后端的參數調整一下
最后我的文件結構如下:
添加 hash 進度
簡單寫一下頁面,效果如下:
后端
接口:判斷秒傳
寫一個接口判斷一下是否存在即可
/*** 功能:驗證服務器中是否存在文件* - 1. 主要是拼接的任務* - 2. ext 的值前面是有 . 的,注意一下。我之前合并好的文件 xxx..mkv 有兩個點...* - 導致 fse.existsSync 怎么都找不到,哭* @param {*} req* @param {*} res* @param {*} MERGE_DIR*/
async handleVerify(req, res, MERGE_DIR) {const postData = await handlePostData(req);const { fileHash, fileName } = postData;const ext = path.extname(fileName);const willCheckMergedName = `${fileHash}${ext}`;const willCheckPath = path.resolve(MERGE_DIR, willCheckMergedName);if (fse.existsSync(willCheckPath)) {res.end(JSON.stringify({code: 0,message: 'existed',}));} else {res.end(JSON.stringify({code: 1,message: 'no exist',}));}
}
前端這邊在 hash
計算后把結果傳給后端,讓后端去驗證
秒傳就差不多啦!
參考文章
path.resolve()
解析- 字節跳動面試官:請你實現一個大文件上傳和斷點續傳
- 《高級JavaScript設計》第四版:第 27 章
- Spark-MD5
- 布隆過濾器