前端大文件分片下載具體實(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)聽
/**
* 文件下載進(jìn)度監(jiān)聽
*/
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'];
// 開始節(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);
// 回寫文件大小
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);
// 重置開始節(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)聽
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 {
// 開始分片下載
const response = await axios.get(url, config);
// 保存分片數(shù)據(jù)
await saveData(range, response.data, dataBase);
ranges.push(range);
// 重置開始節(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("開始合入");
// 流操作
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;
// 寫入流
writer.write(value)
}
// 結(jié)束寫入
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 開始下載
* @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)文章
JS結(jié)合WebSocket實(shí)現(xiàn)實(shí)時(shí)雙向通信
WebSocket 是一種在 Web 應(yīng)用程序中實(shí)現(xiàn)實(shí)時(shí)、雙向通信的協(xié)議,在本文中,我們將深入介紹 WebSocket 的原理、用法以及一些實(shí)際應(yīng)用場(chǎng)景,x需要的可以參考下2023-11-11
javascript getElementById 使用方法及用法
顧明思義,get-Element-By-Id,就是通過(guò)ID來(lái)設(shè)置/返回HTML標(biāo)簽的屬性及調(diào)用其事件與方法。用這個(gè)方法基本上可以控制頁(yè)面所有標(biāo)簽,條件很簡(jiǎn)單就是給每個(gè)標(biāo)簽分配一個(gè)ID號(hào)2008-11-11
jquery實(shí)現(xiàn)右側(cè)欄菜單選擇操作
這篇文章主要為大家詳細(xì)介紹了jquery實(shí)現(xiàn)右側(cè)欄菜單選擇操作,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-03-03
JavaScript JSON數(shù)據(jù)處理全集(小結(jié))
這篇文章主要介紹了JavaScript JSON數(shù)據(jù)處理全集,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-08-08
基于Javascript實(shí)現(xiàn)二級(jí)聯(lián)動(dòng)菜單效果
這篇文章主要為大家詳細(xì)介紹了基于Javascript實(shí)現(xiàn)二級(jí)聯(lián)動(dòng)菜單效果的相關(guān)資料,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-03-03
bootstrap動(dòng)態(tài)添加面包屑(breadcrumb)及其響應(yīng)事件的方法
這篇文章主要介紹了bootstrap動(dòng)態(tài)添加面包屑(breadcrumb)及其響應(yīng)事件的方法,涉及js數(shù)據(jù)傳輸及定義響應(yīng)事件相關(guān)操作技巧,需要的朋友可以參考下2017-05-05
JS打開圖片另存為對(duì)話框?qū)崿F(xiàn)代碼
使用JS打開圖片另存為對(duì)話框一直都是網(wǎng)頁(yè)應(yīng)用中不可缺少的一部分,本人有些好奇,于是搜集整理了一些實(shí)現(xiàn)代碼,不知道符不符合大眾的口味,在此班門弄斧了,需要的朋友可以了解下2012-12-12
JScript中的undefined和"undefined"的區(qū)別
JScript中的undefined和"undefined"的區(qū)別...2007-03-03

