前端大文件分片下載具體實(shí)現(xiàn)方法(看這一篇就夠了)
概要
本文從前端方面出發(fā)實(shí)現(xiàn)瀏覽器下載大文件的功能。不考慮網(wǎng)絡(luò)異常、關(guān)閉網(wǎng)頁(yè)等原因造成傳輸中斷的情況。分片下載采用串行方式(并行下載需要對(duì)切片計(jì)算hash,比對(duì)hash,丟失重傳,合并chunks的時(shí)候需要按順序合并等,很麻煩。對(duì)傳輸速度有追求的,并且在帶寬允許的情況下可以做并行分片下載)
整體架構(gòu)流程
1, 使用分片下載: 將大文件分割成多個(gè)小塊進(jìn)行下載,可以降低內(nèi)存占用和網(wǎng)絡(luò)傳輸中斷的風(fēng)險(xiǎn)。這樣可以避免一次性下載整個(gè)大文件造成的性能問(wèn)題。
2, 斷點(diǎn)續(xù)傳: 實(shí)現(xiàn)斷點(diǎn)續(xù)傳功能,即在下載中途中斷后,可以從已下載的部分繼續(xù)下載,而不需要重新下載整個(gè)文件。
3, 進(jìn)度條顯示: 在頁(yè)面上展示下載進(jìn)度,讓用戶清晰地看到文件下載的進(jìn)度。如果一次全部下載可以從process中直接拿到參數(shù)計(jì)算得出(很精細(xì)),如果是分片下載,也是計(jì)算已下載的和總大小,只不過(guò)已下載的會(huì)成片成片的增加(不是很精細(xì))。
4, 取消下載和暫停下載功能: 提供取消下載和暫停下載的按鈕,讓用戶可以根據(jù)需要中止或暫停下載過(guò)程。
5, 合并文件: 下載完成后,將所有分片文件合并成一個(gè)完整的文件。
具體實(shí)現(xiàn)
一,分片下載之本地儲(chǔ)存(localForage)
前言:
瀏覽器的安全策略禁止網(wǎng)頁(yè)(JS)直接訪問(wèn)和操作用戶計(jì)算機(jī)上的文件系統(tǒng)。
在分片下載過(guò)程中,每個(gè)下載的文件塊(chunk)都需要在客戶端進(jìn)行緩存或存儲(chǔ),方便實(shí)現(xiàn)斷點(diǎn)續(xù)傳功能,同時(shí)也方便后續(xù)將這些文件塊合并成完整的文件。這些文件塊可以暫時(shí)保存在內(nèi)存中或者存儲(chǔ)在客戶端的本地存儲(chǔ)(如 IndexedDB、LocalStorage 等)中。
使用封裝,直接上代碼:
import axios from 'axios'; import ElementUI from 'element-ui' import Vue from 'vue' import localForage from 'localforage' import streamSaver from 'streamsaver'; import store from "@/store/index" /** * localforage–是一個(gè)高效而強(qiáng)大的離線存儲(chǔ)方案。 * 它封裝了IndexedDB, WebSQL, or localStorage,并且提供了一個(gè)簡(jiǎn)化的類似localStorage的API。 * 在默認(rèn)情況下會(huì)優(yōu)先采用使用IndexDB、WebSQL、localStorage進(jìn)行后臺(tái)存儲(chǔ), * 即瀏覽器不支持IndexDB時(shí)嘗試采用WebSQL,既不支持IndexDB又不支持WebSQL時(shí)采用 * localStorage來(lái)進(jìn)行存儲(chǔ)。 * */ /** * @description 創(chuàng)建數(shù)據(jù)庫(kù)實(shí)例,創(chuàng)建并返回一個(gè) localForage 的新實(shí)例。每個(gè)實(shí)例對(duì)象都有獨(dú)立的數(shù)據(jù)庫(kù),而不會(huì)影響到其他實(shí)例 * @param {Object} dataBase 數(shù)據(jù)庫(kù)名 * @return {Object} storeName 實(shí)例(倉(cāng)庫(kù)實(shí)例) */ function createInstance(dataBase){ return localForage.createInstance({ name: dataBase }); } /** * @description 保存數(shù)據(jù) * @param {Object} name 鍵名 * @param {Object} value 數(shù)據(jù) * @param {Object} storeName 倉(cāng)庫(kù) */ async function saveData(name, value, storeName){ await storeName.setItem(name, value).then(() => { // 當(dāng)值被存儲(chǔ)后,可執(zhí)行其他操作 console.log("save success"); }).catch(err => { // 當(dāng)出錯(cuò)時(shí),此處代碼運(yùn)行 console.log(err) }) } /** * @description 獲取保存的數(shù)據(jù) * @param {Object} name 鍵名 * @param {Object} storeName 倉(cāng)庫(kù) */ async function getData(name, storeName, callback){ await storeName.getItem(name).then((val) => { // 獲取到值后,可執(zhí)行其他操作 callback(val); }).catch(err => { // 當(dāng)出錯(cuò)時(shí),此處代碼運(yùn)行 callback(false); }) } /** * @description 移除保存的數(shù)據(jù) * @param {Object} name 鍵名 * @param {Object} storeName 倉(cāng)庫(kù) */ async function removeData(name, storeName){ await storeName.removeItem(name).then(() => { console.log('Key is cleared!'); }).catch(err => { // 當(dāng)出錯(cuò)時(shí),此處代碼運(yùn)行 console.log(err); }) } /** * @description 刪除數(shù)據(jù)倉(cāng)庫(kù),將刪除指定的 “數(shù)據(jù)庫(kù)”(及其所有數(shù)據(jù)倉(cāng)庫(kù))。 * @param {Object} dataBase 數(shù)據(jù)庫(kù)名 */ async function dropDataBase(dataBase){ await localForage.dropInstance({ name: dataBase }).then(() => { console.log('Dropped ' + dataBase + ' database'); }).catch(err => { // 當(dāng)出錯(cuò)時(shí),此處代碼運(yùn)行 console.log(err); }) } /** * @description 刪除數(shù)據(jù)倉(cāng)庫(kù) * @param {Object} dataBase 數(shù)據(jù)庫(kù)名 * @param {Object} storeName 倉(cāng)庫(kù)名(實(shí)例) */ async function dropDataBaseNew(dataBase, storeName){ await localForage.dropInstance({ name: dataBase, storeName: storeName }).then(() => { console.log('Dropped',storeName) }).catch(err => { // 當(dāng)出錯(cuò)時(shí),此處代碼運(yùn)行 console.log(err); }) }
二,本地?cái)?shù)據(jù)獲?。ǐ@取下載列表)
/** * @description 獲取下載列表 * @param {String} page 頁(yè)面 * @param {String} user 用戶 */ export async function getDownloadList(page, user, callback){ // const key = user + "_" + page + "_"; // const key = user + "_"; const key = "_"; // 因?yàn)橛脩魰?huì)過(guò)期 所以不保存用戶名 直接以頁(yè)面為鍵即可 // 基礎(chǔ)數(shù)據(jù)庫(kù) const baseDataBase = createInstance(baseDataBaseName); await baseDataBase.keys().then(async function(keys) { // 包含所有 key 名的數(shù)組 let fileList = []; for(let i = 0; i < keys.length; i++){ if(keys[i].indexOf(key)>-1){ // 獲取數(shù)據(jù) await getData(keys[i], baseDataBase, async (res) =>{ fileList.push( { 'fileName': res, // 文件名 'dataInstance': keys[i] // 文件對(duì)應(yīng)數(shù)據(jù)庫(kù)實(shí)例名 } ); }) } } // 獲取進(jìn)度 for(let i = 0; i < fileList.length; i++){ const dataBase = createInstance(fileList[i].dataInstance); await getData(progressKey, dataBase, async (progress) => { if(progress){ fileList[i].fileSize = progress.fileSize; fileList[i].progress = progress.progress ? progress.progress : 0; fileList[i].status = progress.status ? progress.status : "stopped"; fileList[i].url = progress.url; } }); } callback(fileList); }).catch(function(err) { // 當(dāng)出錯(cuò)時(shí),此處代碼運(yùn)行 callback(err); }); }
三,操作JS
1,下載進(jìn)度監(jiān)聽(tīng)
/** * 文件下載進(jìn)度監(jiān)聽(tīng) */ const onDownloadProgres = (progress) =>{ // progress對(duì)象中的loaded表示已經(jīng)下載的數(shù)量,total表示總數(shù)量,這里計(jì)算出百分比 let downProgress = Math.round(100 * progress.loaded / progress.total); store.commit('setProgress', { dataInstance: uniSign, fileName: data.downLoad, progress: downProgress, status: downProgress == 100 ? 'success' : 'downloading', cancel: cancel }) }
2,文件下載狀態(tài)控制
// 數(shù)據(jù)庫(kù)進(jìn)度數(shù)據(jù)主鍵 const progressKey = "progress"; /** * @description 狀態(tài)控制 * @param {String} fileName 文件名 * @param {String} type 下載方式 continue:續(xù)傳 again:重新下載 cancel:取消 * @param {String} dataBaseName 數(shù)據(jù)庫(kù)名 每個(gè)文件唯一 * */ async function controlFile(fileName, type, dataBaseName){ if(type == 'continue'){ } else if(type == 'again'){ // 刪除文件數(shù)據(jù) await dropDataBase(dataBaseName); // 基礎(chǔ)數(shù)據(jù)庫(kù) const baseDataBase = createInstance(baseDataBaseName); // 基礎(chǔ)數(shù)據(jù)庫(kù)刪除數(shù)據(jù)庫(kù)實(shí)例 removeData(dataBaseName, baseDataBase); } else if(type == 'cancel'){ // 刪除文件數(shù)據(jù) await dropDataBase(dataBaseName); // 基礎(chǔ)數(shù)據(jù)庫(kù) const baseDataBase = createInstance(baseDataBaseName); // 基礎(chǔ)數(shù)據(jù)庫(kù)刪除數(shù)據(jù)庫(kù)實(shí)例 await removeData(dataBaseName, baseDataBase); store.commit('delProgress', { dataInstance: dataBaseName }) } else if(type == 'stop'){ store.commit('setProgress', { dataInstance: dataBaseName, status: 'stopped', }) } }
3,分片下載
/** * @description 分片下載 * @param {String} fileName 文件名 * @param {String} url 下載地址 * @param {String} dataBaseName 數(shù)據(jù)庫(kù)名 每個(gè)文件唯一 * @param {Object} progress 下載進(jìn)度 type: continue:續(xù)傳 again:重新下載 cancel:取消 * */ export async function downloadByBlock(fileName, url, dataBaseName, progress) { //調(diào)整下載狀態(tài) if(progress.type){ await controlFile(fileName, progress.type, dataBaseName); } // 判斷是否超過(guò)最大下載數(shù)量 let downloadNum = store.state.progressList; if(downloadNum){ if(!progress.type && downloadNum.length == downloadMaxNum){ ElementUI.Message.error("已超過(guò)最大下載量,請(qǐng)等待其他文件下載完再嘗試!"); return; } } // 基礎(chǔ)數(shù)據(jù)庫(kù) const baseDataBase = createInstance(baseDataBaseName); // 判斷數(shù)據(jù)庫(kù)中是否已存在該文件的下載任務(wù) let isError = false; await getData(dataBaseName, baseDataBase, async (res) =>{ if(res && !progress.type){ ElementUI.Message.error("該文件已在下載任務(wù)列表,無(wú)需重新下載!"); isError = true; } }); if(isError){ return; } // 儲(chǔ)存數(shù)據(jù)的數(shù)據(jù)庫(kù) const dataBase = createInstance(dataBaseName); // 獲取文件大小 const response = await axios.head(url); // 文件大小 const fileSize = +response.headers['content-length']; // 分片大小 const chunkSize = +response.headers['buffer-size']; // 開(kāi)始節(jié)點(diǎn) let start = 0; // 結(jié)束節(jié)點(diǎn) let end = chunkSize - 1; if (fileSize < chunkSize) { end = fileSize - 1; } // 所有分片 let ranges = []; // 計(jì)算需要分多少次下載 const numChunks = Math.ceil(fileSize / chunkSize); // 回寫(xiě)文件大小 progress.fileSize = fileSize; progress.url = url; // 保存數(shù)據(jù)庫(kù)實(shí)例 await saveData(dataBaseName, fileName, baseDataBase); if(!progress.progress){ // 保存進(jìn)度數(shù)據(jù) await saveData(progressKey, progress, dataBase); } // 保存下載狀態(tài)至localstorage 如果頁(yè)面刷新 可重新讀取狀態(tài) 判斷是否需要繼續(xù)下載 sessionStorage.setItem(dataBaseName,"downloading"); // 組轉(zhuǎn)參數(shù) for (let i = 0; i < numChunks; i++) { // 創(chuàng)建請(qǐng)求控制器 const controller = new AbortController(); const range = `bytes=${start}-${end}`; // 如果是續(xù)傳 先判斷是否已下載 if(progress.type == 'continue'){ // 先修改狀態(tài) store.commit('setProgress', { dataInstance: dataBaseName, status: 'downloading' }) let isContinue = false; await getData(range, dataBase, async function(res){ if(res) isContinue = true}); if(isContinue){ ranges.push(range); // 重置開(kāi)始節(jié)點(diǎn) start = end + 1; // 重置結(jié)束節(jié)點(diǎn) end = Math.min(end + chunkSize, fileSize - 1); continue; } } let cancel; const config = { headers: { Range: range }, responseType: 'arraybuffer', // 綁定取消請(qǐng)求的信號(hào)量 signal: controller.signal, // 文件下載進(jìn)度監(jiān)聽(tīng) onDownloadProgress: function (pro){ if(progress.type == 'stop' || progress.type == 'cancel'){ // 終止請(qǐng)求 controller.abort(); } // 已加載大小 // progress對(duì)象中的loaded表示已經(jīng)下載的數(shù)量,total表示總數(shù)量,這里計(jì)算出百分比 let downProgress = (pro.loaded / pro.total); downProgress = Math.round( (i / numChunks) * 100 + downProgress / numChunks * 100); progress.progress = downProgress; // 設(shè)置為異常 如果是正常下載完 這個(gè)記錄會(huì)被刪除 如果合并失敗 則會(huì)顯示異常 progress.status = downProgress == 100 ? 'error' : 'stopped'; store.commit('setProgress', { dataInstance: dataBaseName, fileName: fileName, progress: downProgress, status: 'downloading', cancel: cancel, url: url, dataBaseName: dataBaseName }) } }; try { // 開(kāi)始分片下載 const response = await axios.get(url, config); // 保存分片數(shù)據(jù) await saveData(range, response.data, dataBase); ranges.push(range); // 重置開(kāi)始節(jié)點(diǎn) start = end + 1; // 重置結(jié)束節(jié)點(diǎn) end = Math.min(end + chunkSize, fileSize - 1); } catch (error) { console.log("終止請(qǐng)求時(shí)catch的error---", error); // 判斷是否為取消上傳 if (error.message == "canceled"){ // 進(jìn)行后續(xù)處理 if(progress.type == 'stop'){ // 暫停 await controlFile(fileName, progress.type, dataBaseName); sessionStorage.setItem(dataBaseName,"stop"); // 終止請(qǐng)求 console.log("stopped"); return; } else if(progress.type == 'cancel'){ // 取消 await controlFile(fileName, progress.type, dataBaseName); sessionStorage.removeItem(dataBaseName); // 終止請(qǐng)求 console.log("canceled"); return; } }; } } console.log("開(kāi)始合入"); // 流操作 const fileStream = streamSaver.createWriteStream(fileName, {size: fileSize}); const writer = fileStream.getWriter(); // 合并數(shù)據(jù) // 循環(huán)取出數(shù)據(jù)合并 for (let i=0; i < ranges.length; i++) { var range = ranges[i]; // 從數(shù)據(jù)庫(kù)獲取分段數(shù)據(jù) await getData(range, dataBase, async function(res){ if(res){ // 讀取流 const reader = new Blob([res]).stream().getReader(); while (true) { const { done, value } = await reader.read(); if (done) break; // 寫(xiě)入流 writer.write(value) } // 結(jié)束寫(xiě)入 if(i==ranges.length-1){ writer.close(); // 清空數(shù)據(jù)庫(kù) dropDataBase(dataBaseName); // 基礎(chǔ)數(shù)據(jù)庫(kù)刪除數(shù)據(jù)庫(kù)實(shí)例 removeData(dataBaseName, baseDataBase); // 清除store store.commit('delProgress', { dataInstance: dataBaseName }) } } }); } }
四,VUE頁(yè)面
<script> import { downloadByBlock } from "上面的js"; export default { name: "suspension", data() { return { // 下載列表 downloadDialog: false, fileStatus: {} }; }, watch: { "$store.state.progressList": function() { this.$nextTick(function() { let progressList = this.$store.state.progressList; progressList.forEach(item => { // 獲取之前下載狀態(tài) 還原操作 const status = sessionStorage.getItem(item.dataInstance); if (status == "downloading" && item.status != status) { this.startDownload(item); } }); }); } }, methods: { /** * @description 重試 * @param {Object} row */ retryDownload(row) { this.startDownload(row); }, /** * @description 開(kāi)始下載 * @param {Object} row */ startDownload(row) { this.fileStatus[row.dataInstance] = { type: "continue", progress: row.progress }; downloadByBlock( row.fileName, row.url, row.dataInstance, this.fileStatus[row.dataInstance] ); }, /** * @description 暫停下載 * @param {Object} row */ stopDownload(row) { this.$set(this.fileStatus[row.dataInstance], "type", "stop"); }, /** * @description 刪除下載 * @param {Object} row */ delDownload(row) { if (this.fileStatus[row.dataInstance] && row.status != "stopped") { this.$set(this.fileStatus[row.dataInstance], "type", "cancel"); } else { this.fileStatus[row.dataInstance] = { type: "cancel" }; downloadByBlock( row.fileName, row.url, row.dataInstance, this.fileStatus[row.dataInstance] ); } }, /** * @description 分片下載文件 */ downloadByBlock(fileName, url, dataBaseName, type) { this.fileStatus[dataBaseName] = { type: type }; downloadByBlock( fileName, url, dataBaseName, this.fileStatus[dataBaseName] ); this.btnDownload(); }, /** * @description 下載列表 */ btnDownload() { // 通過(guò)路由信息判斷當(dāng)前處于哪一個(gè)頁(yè)面 const page = this.$route.name; this.downloadDialog = true; }, /** * @description 取消彈窗 */ downloadBtnCancel() { this.downloadDialog = false; } } }; </script>
小結(jié)
這只是一個(gè)基本示例。在實(shí)際應(yīng)用中,你可能需要考慮以下問(wèn)題:
并發(fā)下載: 如果多個(gè)用戶同時(shí)下載相同的大文件,可能會(huì)對(duì)服務(wù)器造成很大的壓力??梢允褂貌l(fā)下載來(lái)提高性能。
安全性: 確保只有授權(quán)用戶可以下載文件,并且不會(huì)泄漏敏感信息。
性能優(yōu)化: 使用緩存、壓縮等技術(shù)來(lái)提高下載速度和用戶體驗(yàn)。
服務(wù)器資源管理: 下載大文件可能會(huì)占用服務(wù)器的網(wǎng)絡(luò)帶寬和內(nèi)存資源,需要適當(dāng)?shù)墓芾怼?/p>
總之,大文件分片下載和合并是一個(gè)復(fù)雜的任務(wù),需要綜合考慮性能、安全性和用戶體驗(yàn)。在實(shí)際項(xiàng)目中,你可能會(huì)使用更高級(jí)的工具和技術(shù)來(lái)處理這些問(wèn)題。
總結(jié)
到此這篇關(guān)于前端大文件分片下載具體實(shí)現(xiàn)方法的文章就介紹到這了,更多相關(guān)前端大文件分片下載內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
JavaScript判斷數(shù)據(jù)類型的四種方式總結(jié)
JavaScript 作為一門(mén)動(dòng)態(tài)語(yǔ)言,其靈活性是把雙刃劍,一方面帶來(lái)了開(kāi)發(fā)的便利性,另一方面也給我們?cè)陬愋团袛鄷r(shí)帶來(lái)了挑戰(zhàn),特別是在處理類型轉(zhuǎn)換和隱式轉(zhuǎn)換的時(shí)候,所以本篇文章我們將探討 JavaScript 中的數(shù)據(jù)類型判斷方式及在實(shí)際項(xiàng)目中的應(yīng)用,需要的朋友可以參考下2025-04-04JavaScript實(shí)現(xiàn)點(diǎn)擊切換驗(yàn)證碼及校驗(yàn)
這篇文章主要為大家詳細(xì)介紹了JavaScript實(shí)現(xiàn)點(diǎn)擊切換驗(yàn)證碼及校驗(yàn),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-01-01JS優(yōu)雅的使用function實(shí)現(xiàn)一個(gè)class
這篇文章主要為大家介紹了JS優(yōu)雅的使用function實(shí)現(xiàn)一個(gè)class示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12JavaScript處理數(shù)組數(shù)據(jù)的示例詳解
這篇文章主要為大家詳細(xì)介紹了JavaScript如何處理數(shù)組數(shù)據(jù),包括數(shù)據(jù)匹配和剔除,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起了解一下2023-10-10最簡(jiǎn)單的JavaScript驗(yàn)證整數(shù)、小數(shù)、實(shí)數(shù)、有效位小數(shù)正則表達(dá)式
這篇文章主要介紹了最簡(jiǎn)單的JavaScript驗(yàn)證整數(shù)、小數(shù)、實(shí)數(shù)、有效位小數(shù)正則表達(dá)式,其中包含保留1位小數(shù)、保留2位小數(shù)、保留3位小數(shù)等正則,需要的朋友可以參考下2015-04-04JS中Map、WeakMap和Object的區(qū)別解析
Map、WeakMap和Object都是JavaScript中用于存儲(chǔ)鍵值對(duì)的數(shù)據(jù)結(jié)構(gòu),它們?cè)阪I類型、垃圾回收、可枚舉性、方法和操作、以及繼承等方面存在一些區(qū)別,適用于不同的場(chǎng)景,本文給大家詳細(xì)講解js map、weakmap和object區(qū)別,需要的朋友可以參考下2023-04-04