JavaScript利用切片實現(xiàn)大文件斷點續(xù)傳
最近公司需要用戶上傳比較大的文件,一個文件可能大于1GB,如果出現(xiàn)網(wǎng)絡(luò)波動或者用戶違規(guī)操作導致上傳中斷,那么就必須重新重頭上傳。身為前端,與后端商量后,查看了一些已經(jīng)成熟的的實現(xiàn)方案,最后使用斷點續(xù)傳優(yōu)化上傳的邏輯。
什么是斷點續(xù)傳
在文件上傳期間因為一些原因而導致上傳終止(刷新或網(wǎng)絡(luò)中斷)時,下次再次上傳同一個文件就從上一次上傳到一半的地方繼續(xù)上傳,以節(jié)省上傳時間。
實現(xiàn)思路
主體思路是將比較大的文件分成若干個切片。并非一次性將一個文件整體傳輸給服務(wù)器,而是將分割的切片一部分一部分地傳給服務(wù)器。服務(wù)器將已上傳的切片暫存,當所有的切片都上傳成功了,再將切片合并成一整個文件。
這么做就可以利用切片使用斷點續(xù)傳的功能。具體邏輯是如果在上傳切片的途中斷了,那么下次再次上傳同一個文件的時候可以先向服務(wù)器端發(fā)一個請求,獲取已經(jīng)上傳了哪些切片,與整體切片進行比對后,再將剩下未上傳的切片繼續(xù)上傳。
其中后端處理切片和合并的一個細節(jié)點為:將大文件進行切片時,后端需要將每一個切片的文件名為${原文件HASH值}_${切片序號}.${文件后綴}
例如'f07ec272dbb0b883eed4b2f415625a90_2.mp4'
。并且將服務(wù)器存的切片的臨時文件夾的名字命名為hash值。最后所有的切片上傳完成時,再調(diào)用合并接口,后端將所有臨時文件夾中的切片合并。
需要后端提供的api
以下api需要后端提供開發(fā)
獲取已經(jīng)上傳的切片
url:/upload_already method:GET params: HASH:文件的HASH名字 return:application/json code:0成功 1失敗, codeText:狀態(tài)描述, fileList:[...]
此方法用來獲取已上傳文件的所有切片的切片名,例如返回:
{ fileList:['f07ec272dbb0b883eed4b2f415625a90_1.mp4','f07ec272dbb0b883eed4b2f415625a90_2.mp4','f07ec272dbb0b883eed4b2f415625a90_3.mp4'] }
意思為HASH值為'f07ec272dbb0b883eed4b2f415625a9'
的文件已經(jīng)上傳了三個切片,接下來只需要從第四個切片開始上傳即可。如果是空數(shù)組,說明此文件第一次上傳。
上傳切片
url:/upload_chunk method:POST params:multipart/form-data file:切片數(shù)據(jù) filename:切片名字「文件生成的HASH_切片編號.后綴」 return:application/json code:0成功 1失敗, codeText:狀態(tài)描述, originalFilename:文件原始名稱, servicePath:文件服務(wù)器地址
上傳file
對象格式的切片文件,并且將切片名字已${原文件HASH值}_${切片序號}.${文件后綴}
格式傳給后端,后端利用hash值將切片暫存在一個臨時文件夾中,最后所有切片上傳完成,就將切片合并,然后刪除這個臨時文件夾。
合并切片
url:/upload_merge method:POST params:application/x-www-form-urlencoded HASH:文件的HASH名字 count:切片數(shù)量 return:application/json code:0成功 1失敗, codeText:狀態(tài)描述, originalFilename:文件原始名稱, servicePath:文件服務(wù)器地址
當所有切片上傳完成(前段自行判斷)之后,調(diào)用合并接口,后端會將切片合并,然后刪除存切片的臨時文件夾。
前端代碼細節(jié)實現(xiàn)
HASH值的獲取方法
- 使用
FileReader
對象將選擇的文件對象轉(zhuǎn)為buffer
- 依據(jù)文件的
buffer
使用MD5庫生成文件的HASH值。
封裝成函數(shù):
/** * 傳入文件對象,返回文件生成的HASH值,后綴,buffer,以HASH值為名的新文件名 * @param file * @returns {Promise<unknown>} */ const changeBuffer = file => { return new Promise(resolve => { let fileReader = new FileReader(); fileReader.readAsArrayBuffer(file); fileReader.onload = ev => { let buffer = ev.target.result, spark = new SparkMD5.ArrayBuffer(), HASH, suffix; spark.append(buffer); HASH = spark.end(); suffix = /.([a-zA-Z0-9]+)$/.exec(file.name)[1]; resolve({ buffer, HASH, suffix, filename: `${HASH}.${suffix}` }); }; }); };
切片處理
使用Blob
對象中的slice
函數(shù)可以進行對文件進行切片處理??梢詫⑽募袨樽远x的大小和數(shù)量。
例如file.slice(0,1024)
代表將文件的0-1024字節(jié)數(shù)據(jù)切片,然后返回一個新的對象。
總體html結(jié)構(gòu)
<div class="container"> <div class="item"> <h3>大文件上傳</h3> <section class="upload_box" id="upload7"> <input type="file" class="upload_inp"> <div class="upload_button_box"> <button class="upload_button select">上傳文件</button> </div> <div class="upload_progress"> <div class="value"></div> </div> </section> </div> </div>
使用axios發(fā)送請求
/*把axios發(fā)送請求的公共信息進行提取*/ //創(chuàng)建一個單獨的實例,不去項目全局的或者其他的axios沖突 let instance = axios.create(); instance.defaults.baseURL = 'http://127.0.0.1:8888'; //默認是multipart/form-data格式 instance.defaults.headers['Content-Type'] = 'multipart/form-data'; instance.defaults.transformRequest = (data, headers) => { //兼容x-www-form-urlencoded格式的請求發(fā)送 const contentType = headers['Content-Type']; if (contentType === "application/x-www-form-urlencoded") return Qs.stringify(data); return data; }; //統(tǒng)一結(jié)果的處理 instance.interceptors.response.use(response => { return response.data; },reason=>{ //統(tǒng)一失敗的處理 return Promise.reject(reason) });
整體邏輯和代碼
詳細的邏輯在注釋當中,寫的比較詳細
(function () { let upload = document.querySelector('#upload7'), upload_inp = upload.querySelector('.upload_inp'), upload_button_select = upload.querySelector('.upload_button.select'), upload_progress = upload.querySelector('.upload_progress'), upload_progress_value = upload_progress.querySelector('.value'); const checkIsDisable = element => { let classList = element.classList; return classList.contains('disable') || classList.contains('loading'); }; /** * 傳入文件對象,返回文件生成的HASH值,后綴,buffer,以HASH值為名的新文件名 * @param file * @returns {Promise<unknown>} */ const changeBuffer = file => { return new Promise(resolve => { let fileReader = new FileReader(); fileReader.readAsArrayBuffer(file); fileReader.onload = ev => { let buffer = ev.target.result, spark = new SparkMD5.ArrayBuffer(), HASH, suffix; spark.append(buffer); HASH = spark.end(); suffix = /.([a-zA-Z0-9]+)$/.exec(file.name)[1]; resolve({ buffer, HASH, suffix, filename: `${HASH}.${suffix}` }); }; }); }; upload_inp.addEventListener('change', async function () { //get native file object let file = upload_inp.files[0]; if (!file) return; //button add loading upload_button_select.classList.add('loading'); //show progress upload_progress.style.display = 'block'; // 獲取文件的HASH let already = [],//已經(jīng)上傳過的切片的切片名 data = null, { HASH, suffix } = await changeBuffer(file);//得到原始文件的hash和后綴 // 獲取已經(jīng)上傳的切片信息 try { data = await instance.get('/upload_already', { params: { HASH } }); if (+data.code === 0) { already = data.fileList; } } catch (err) {} // 實現(xiàn)文件切片處理 「固定數(shù)量 & 固定大小」 let max = 1024 * 100,//切片大小先設(shè)置100KB count = Math.ceil(file.size / max),//得到應(yīng)該上傳的切片 index = 0,//存放切片數(shù)組的時候遍歷使用 chunks = [];//用以存放切片值 if (count > 100) {//如果切片數(shù)量超過100,那么就只切成100個,因為切片太多的話也會影響調(diào)用接口的速度 max = file.size / 100; count = 100; } while (index < count) {//循環(huán)生成切片 //index 0 => 0~max //index 1 => max~max*2 //index*max ~(index+1)*max chunks.push({ file: file.slice(index * max, (index + 1) * max), filename: `${HASH}_${index+1}.${suffix}` }); index++; } index = 0; const clear = () => {//上傳完成后,將狀態(tài)回歸 upload_button_select.classList.remove('loading'); upload_progress.style.display = 'none'; upload_progress_value.style.width = '0%'; }; //每一次上傳一個切片成功的處理[進度管控&切片合并] const complate = async () => { // 管控進度條:每上傳完一個切片,就將進度條長度增加一點 index++; upload_progress_value.style.width = `${index/count*100}%`; if (index < count) return; // 當所有切片都上傳成功,就合并切片 upload_progress_value.style.width = `100%`; try { //調(diào)用合并切片方法 data = await instance.post('/upload_merge', { HASH, count }, { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }); if (+data.code === 0) { alert(`恭喜您,文件上傳成功,您可以基于 ${data.servicePath} 訪問該文件~~`); clear(); return; } throw data.codeText; } catch (err) { alert('切片合并失敗,請您稍后再試~~'); clear(); } }; // 循環(huán)上傳每一個切片 chunks.forEach(chunk => { // 已經(jīng)上傳的無需在上傳 //后臺返回的already格式為['HASH_1.png','HASH_2.png'],既已經(jīng)上傳的文件的切片名 if (already.length > 0 && already.includes(chunk.filename)) { //已經(jīng)上傳過了的切片就無需再調(diào)用接口上傳了 complate();//動進度條或合并所有切片 return; } let fm = new FormData; fm.append('file', chunk.file); fm.append('filename', chunk.filename); instance.post('/upload_chunk', fm).then(data => {//使用form data格式上傳切片 if (+data.code === 0) { complate();////動進度條或合并所有切片 return; } return Promise.reject(data.codeText); }).catch(() => { alert('當前切片上傳失敗,請您稍后再試~~'); clear(); }); }); }); //觸發(fā)原生的上傳文件框 upload_button_select.addEventListener('click', function () { if (checkIsDisable(this)) return; upload_inp.click(); }); })();
實現(xiàn)效果
第一次上傳時,分別調(diào)用/upload_already
,/upload_chunk
方法獲取已上傳的切片(空數(shù)組),然后進行切片分割,再一個個進行上傳。
當在此時刷新頁面,終端切片的上傳行為時。此時我們看一下后端的臨時數(shù)據(jù)
可以看到一個以hash至命名的臨時文件夾,并且已經(jīng)上傳了24個切片
如果我們再次選擇同樣的文件上傳,進度條會立即到上次已經(jīng)上傳的位置,already接口返回已上傳的24個切片的名字的數(shù)組。
將剩下的切片上傳完成之后,會調(diào)用merge接口。完成上傳
此時后端的臨時文件夾被刪除,合并為一整個文件。上傳結(jié)束
到此這篇關(guān)于JavaScript利用切片實現(xiàn)大文件斷點續(xù)傳的文章就介紹到這了,更多相關(guān)JavaScript文件斷點續(xù)傳內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
JavaScript?數(shù)組方法filter與reduce
這篇文章主要介紹了JavaScript?數(shù)組方法filter與reduce,在ES6新增的數(shù)組方法中,包含了多個遍歷方法,其中包含了用于篩選的filter和reduce2022-07-07JS中call apply bind函數(shù)手寫實現(xiàn)demo
這篇文章主要為大家介紹了JS中call apply bind函數(shù)手寫實現(xiàn)demo,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-03-03js select下拉聯(lián)動 更具級聯(lián)性!
這篇文章主要為大家詳細介紹了js select下拉聯(lián)動的相關(guān)資料,更具級聯(lián)性!文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-01-01TypeScript?Pinia實戰(zhàn)分享(Vuex和Pinia對比梳理總結(jié))
這篇文章主要介紹了TypeScript?Pinia實戰(zhàn)分享(Vuex和Pinia對比梳理總結(jié)),今天我們再來實戰(zhàn)下官方推薦的新的vue狀態(tài)管理工具Pini,感興趣的小伙伴可以參考一下2022-06-06