JavaScript利用切片實(shí)現(xiàn)大文件斷點(diǎn)續(xù)傳
最近公司需要用戶上傳比較大的文件,一個(gè)文件可能大于1GB,如果出現(xiàn)網(wǎng)絡(luò)波動(dòng)或者用戶違規(guī)操作導(dǎo)致上傳中斷,那么就必須重新重頭上傳。身為前端,與后端商量后,查看了一些已經(jīng)成熟的的實(shí)現(xiàn)方案,最后使用斷點(diǎn)續(xù)傳優(yōu)化上傳的邏輯。
什么是斷點(diǎn)續(xù)傳
在文件上傳期間因?yàn)橐恍┰蚨鴮?dǎo)致上傳終止(刷新或網(wǎng)絡(luò)中斷)時(shí),下次再次上傳同一個(gè)文件就從上一次上傳到一半的地方繼續(xù)上傳,以節(jié)省上傳時(shí)間。
實(shí)現(xiàn)思路
主體思路是將比較大的文件分成若干個(gè)切片。并非一次性將一個(gè)文件整體傳輸給服務(wù)器,而是將分割的切片一部分一部分地傳給服務(wù)器。服務(wù)器將已上傳的切片暫存,當(dāng)所有的切片都上傳成功了,再將切片合并成一整個(gè)文件。
這么做就可以利用切片使用斷點(diǎn)續(xù)傳的功能。具體邏輯是如果在上傳切片的途中斷了,那么下次再次上傳同一個(gè)文件的時(shí)候可以先向服務(wù)器端發(fā)一個(gè)請(qǐng)求,獲取已經(jīng)上傳了哪些切片,與整體切片進(jìn)行比對(duì)后,再將剩下未上傳的切片繼續(xù)上傳。
其中后端處理切片和合并的一個(gè)細(xì)節(jié)點(diǎn)為:將大文件進(jìn)行切片時(shí),后端需要將每一個(gè)切片的文件名為${原文件HASH值}_${切片序號(hào)}.${文件后綴}
例如'f07ec272dbb0b883eed4b2f415625a90_2.mp4'
。并且將服務(wù)器存的切片的臨時(shí)文件夾的名字命名為hash值。最后所有的切片上傳完成時(shí),再調(diào)用合并接口,后端將所有臨時(shí)文件夾中的切片合并。
需要后端提供的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)上傳了三個(gè)切片,接下來只需要從第四個(gè)切片開始上傳即可。如果是空數(shù)組,說明此文件第一次上傳。
上傳切片
url:/upload_chunk method:POST params:multipart/form-data file:切片數(shù)據(jù) filename:切片名字「文件生成的HASH_切片編號(hào).后綴」 return:application/json code:0成功 1失敗, codeText:狀態(tài)描述, originalFilename:文件原始名稱, servicePath:文件服務(wù)器地址
上傳file
對(duì)象格式的切片文件,并且將切片名字已${原文件HASH值}_${切片序號(hào)}.${文件后綴}
格式傳給后端,后端利用hash值將切片暫存在一個(gè)臨時(shí)文件夾中,最后所有切片上傳完成,就將切片合并,然后刪除這個(gè)臨時(shí)文件夾。
合并切片
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ù)器地址
當(dāng)所有切片上傳完成(前段自行判斷)之后,調(diào)用合并接口,后端會(huì)將切片合并,然后刪除存切片的臨時(shí)文件夾。
前端代碼細(xì)節(jié)實(shí)現(xiàn)
HASH值的獲取方法
- 使用
FileReader
對(duì)象將選擇的文件對(duì)象轉(zhuǎn)為buffer
- 依據(jù)文件的
buffer
使用MD5庫生成文件的HASH值。
封裝成函數(shù):
/** * 傳入文件對(duì)象,返回文件生成的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
對(duì)象中的slice
函數(shù)可以進(jìn)行對(duì)文件進(jìn)行切片處理。可以將文件切為自定義的大小和數(shù)量。
例如file.slice(0,1024)
代表將文件的0-1024字節(jié)數(shù)據(jù)切片,然后返回一個(gè)新的對(duì)象。
總體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ā)送請(qǐng)求
/*把a(bǔ)xios發(fā)送請(qǐng)求的公共信息進(jìn)行提取*/ //創(chuàng)建一個(gè)單獨(dú)的實(shí)例,不去項(xiàng)目全局的或者其他的axios沖突 let instance = axios.create(); instance.defaults.baseURL = 'http://127.0.0.1:8888'; //默認(rèn)是multipart/form-data格式 instance.defaults.headers['Content-Type'] = 'multipart/form-data'; instance.defaults.transformRequest = (data, headers) => { //兼容x-www-form-urlencoded格式的請(qǐng)求發(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) });
整體邏輯和代碼
詳細(xì)的邏輯在注釋當(dāng)中,寫的比較詳細(xì)
(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'); }; /** * 傳入文件對(duì)象,返回文件生成的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) {} // 實(shí)現(xiàn)文件切片處理 「固定數(shù)量 & 固定大小」 let max = 1024 * 100,//切片大小先設(shè)置100KB count = Math.ceil(file.size / max),//得到應(yīng)該上傳的切片 index = 0,//存放切片數(shù)組的時(shí)候遍歷使用 chunks = [];//用以存放切片值 if (count > 100) {//如果切片數(shù)量超過100,那么就只切成100個(gè),因?yàn)榍衅嗟脑捯矔?huì)影響調(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%'; }; //每一次上傳一個(gè)切片成功的處理[進(jìn)度管控&切片合并] const complate = async () => { // 管控進(jìn)度條:每上傳完一個(gè)切片,就將進(jìn)度條長度增加一點(diǎn) index++; upload_progress_value.style.width = `${index/count*100}%`; if (index < count) return; // 當(dāng)所有切片都上傳成功,就合并切片 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('切片合并失敗,請(qǐng)您稍后再試~~'); clear(); } }; // 循環(huán)上傳每一個(gè)切片 chunks.forEach(chunk => { // 已經(jīng)上傳的無需在上傳 //后臺(tái)返回的already格式為['HASH_1.png','HASH_2.png'],既已經(jīng)上傳的文件的切片名 if (already.length > 0 && already.includes(chunk.filename)) { //已經(jīng)上傳過了的切片就無需再調(diào)用接口上傳了 complate();//動(dòng)進(jìn)度條或合并所有切片 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();////動(dòng)進(jìn)度條或合并所有切片 return; } return Promise.reject(data.codeText); }).catch(() => { alert('當(dāng)前切片上傳失敗,請(qǐng)您稍后再試~~'); clear(); }); }); }); //觸發(fā)原生的上傳文件框 upload_button_select.addEventListener('click', function () { if (checkIsDisable(this)) return; upload_inp.click(); }); })();
實(shí)現(xiàn)效果
第一次上傳時(shí),分別調(diào)用/upload_already
,/upload_chunk
方法獲取已上傳的切片(空數(shù)組),然后進(jìn)行切片分割,再一個(gè)個(gè)進(jìn)行上傳。
當(dāng)在此時(shí)刷新頁面,終端切片的上傳行為時(shí)。此時(shí)我們看一下后端的臨時(shí)數(shù)據(jù)
可以看到一個(gè)以hash至命名的臨時(shí)文件夾,并且已經(jīng)上傳了24個(gè)切片
如果我們?cè)俅芜x擇同樣的文件上傳,進(jìn)度條會(huì)立即到上次已經(jīng)上傳的位置,already接口返回已上傳的24個(gè)切片的名字的數(shù)組。
將剩下的切片上傳完成之后,會(huì)調(diào)用merge接口。完成上傳
此時(shí)后端的臨時(shí)文件夾被刪除,合并為一整個(gè)文件。上傳結(jié)束
到此這篇關(guān)于JavaScript利用切片實(shí)現(xiàn)大文件斷點(diǎn)續(xù)傳的文章就介紹到這了,更多相關(guān)JavaScript文件斷點(diǎn)續(xù)傳內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
layui點(diǎn)擊彈框頁面 表單請(qǐng)求的方法
今天小編就為大家分享一篇layui點(diǎn)擊彈框頁面 表單請(qǐng)求的方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2019-09-09JavaScript?數(shù)組方法filter與reduce
這篇文章主要介紹了JavaScript?數(shù)組方法filter與reduce,在ES6新增的數(shù)組方法中,包含了多個(gè)遍歷方法,其中包含了用于篩選的filter和reduce2022-07-07JS中call apply bind函數(shù)手寫實(shí)現(xiàn)demo
這篇文章主要為大家介紹了JS中call apply bind函數(shù)手寫實(shí)現(xiàn)demo,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-03-03js select下拉聯(lián)動(dòng) 更具級(jí)聯(lián)性!
這篇文章主要為大家詳細(xì)介紹了js select下拉聯(lián)動(dòng)的相關(guān)資料,更具級(jí)聯(lián)性!文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-01-01TypeScript?Pinia實(shí)戰(zhàn)分享(Vuex和Pinia對(duì)比梳理總結(jié))
這篇文章主要介紹了TypeScript?Pinia實(shí)戰(zhàn)分享(Vuex和Pinia對(duì)比梳理總結(jié)),今天我們?cè)賮韺?shí)戰(zhàn)下官方推薦的新的vue狀態(tài)管理工具Pini,感興趣的小伙伴可以參考一下2022-06-06