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