文章目录
概要
本文从前端方面出发实现浏览器下载大文件的功能。不考虑网络异常、关闭网页等原因造成传输中断的情况。分片下载采用串行方式(并行下载需要对切片计算hash,比对hash,丢失重传,合并chunks的时候需要按顺序合并等,很麻烦。对传输速度有追求的,并且在带宽允许的情况下可以做并行分片下载)
整体架构流程
1, 使用分片下载: 将大文件分割成多个小块进行下载,可以降低内存占用和网络传输中断的风险。这样可以避免一次性下载整个大文件造成的性能问题。
2, 断点续传: 实现断点续传功能,即在下载中途中断后,可以从已下载的部分继续下载,而不需要重新下载整个文件。
3, 进度条显示: 在页面上展示下载进度,让用户清晰地看到文件下载的进度。如果一次全部下载可以从process中直接拿到参数计算得出(很精细),如果是分片下载,也是计算已下载的和总大小,只不过已下载的会成片成片的增加(不是很精细)。
4, 取消下载和暂停下载功能: 提供取消下载和暂停下载的按钮,让用户可以根据需要中止或暂停下载过程。
5, 合并文件: 下载完成后,将所有分片文件合并成一个完整的文件。
具体实现
一,分片下载之本地储存(localForage)
前言:
浏览器的安全策略禁止网页(JS)直接访问和操作用户计算机上的文件系统。
在分片下载过程中,每个下载的文件块(chunk)都需要在客户端进行缓存或存储,方便实现断点续传功能,同时也方便后续将这些文件块合并成完整的文件。这些文件块可以暂时保存在内存中或者存储在客户端的本地存储(如 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。
* 在默认情况下会优先采用使用IndexDB、WebSQL、localStorage进行后台存储,
* 即浏览器不支持IndexDB时尝试采用WebSQL,既不支持IndexDB又不支持WebSQL时采用
* localStorage来进行存储。
*
*//**
* @description 创建数据库实例,创建并返回一个 localForage 的新实例。每个实例对象都有独立的数据库,而不会影响到其他实例
* @param {Object} dataBase 数据库名
* @return {Object} storeName 实例(仓库实例)
*/functioncreateInstance(dataBase){return localForage.createInstance({name: dataBase
});}/**
* @description 保存数据
* @param {Object} name 键名
* @param {Object} value 数据
* @param {Object} storeName 仓库
*/asyncfunctionsaveData(name, value, storeName){await storeName.setItem(name, value).then(()=>{// 当值被存储后,可执行其他操作
console.log("save success");}).catch(err=>{// 当出错时,此处代码运行
console.log(err)})}/**
* @description 获取保存的数据
* @param {Object} name 键名
* @param {Object} storeName 仓库
*/asyncfunctiongetData(name, storeName, callback){await storeName.getItem(name).then((val)=>{// 获取到值后,可执行其他操作callback(val);}).catch(err=>{// 当出错时,此处代码运行callback(false);})}/**
* @description 移除保存的数据
* @param {Object} name 键名
* @param {Object} storeName 仓库
*/asyncfunctionremoveData(name, storeName){await storeName.removeItem(name).then(()=>{
console.log('Key is cleared!');}).catch(err=>{// 当出错时,此处代码运行
console.log(err);})}/**
* @description 删除数据仓库,将删除指定的 “数据库”(及其所有数据仓库)。
* @param {Object} dataBase 数据库名
*/asyncfunctiondropDataBase(dataBase){await localForage.dropInstance({name: dataBase
}).then(()=>{
console.log('Dropped '+ dataBase +' database');}).catch(err=>{// 当出错时,此处代码运行
console.log(err);})}/**
* @description 删除数据仓库
* @param {Object} dataBase 数据库名
* @param {Object} storeName 仓库名(实例)
*/asyncfunctiondropDataBaseNew(dataBase, storeName){await localForage.dropInstance({name: dataBase,storeName: storeName
}).then(()=>{
console.log('Dropped',storeName)}).catch(err=>{// 当出错时,此处代码运行
console.log(err);})}
二,本地数据获取(获取下载列表)
/**
* @description 获取下载列表
* @param {String} page 页面
* @param {String} user 用户
*/exportasyncfunctiongetDownloadList(page, user, callback){// const key = user + "_" + page + "_";// const key = user + "_";const key ="_";// 因为用户会过期 所以不保存用户名 直接以页面为键即可// 基础数据库const baseDataBase =createInstance(baseDataBaseName);await baseDataBase.keys().then(asyncfunction(keys){// 包含所有 key 名的数组let fileList =[];for(let i =0; i < keys.length; i++){if(keys[i].indexOf(key)>-1){// 获取数据awaitgetData(keys[i], baseDataBase,async(res)=>{
fileList.push({'fileName': res,// 文件名'dataInstance': keys[i]// 文件对应数据库实例名});})}}// 获取进度for(let i =0; i < fileList.length; i++){const dataBase =createInstance(fileList[i].dataInstance);awaitgetData(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){// 当出错时,此处代码运行callback(err);});}
二,操作JS
1,下载进度监听
/**
* 文件下载进度监听
*/constonDownloadProgres=(progress)=>{// progress对象中的loaded表示已经下载的数量,total表示总数量,这里计算出百分比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,文件下载状态控制
// 数据库进度数据主键const progressKey ="progress";/**
* @description 状态控制
* @param {String} fileName 文件名
* @param {String} type 下载方式 continue:续传 again:重新下载 cancel:取消
* @param {String} dataBaseName 数据库名 每个文件唯一
*
*/asyncfunctioncontrolFile(fileName, type, dataBaseName){if(type =='continue'){}elseif(type =='again'){// 删除文件数据awaitdropDataBase(dataBaseName);// 基础数据库const baseDataBase =createInstance(baseDataBaseName);// 基础数据库删除数据库实例removeData(dataBaseName, baseDataBase);}elseif(type =='cancel'){// 删除文件数据awaitdropDataBase(dataBaseName);// 基础数据库const baseDataBase =createInstance(baseDataBaseName);// 基础数据库删除数据库实例awaitremoveData(dataBaseName, baseDataBase);
store.commit('delProgress',{dataInstance: dataBaseName
})}elseif(type =='stop'){
store.commit('setProgress',{dataInstance: dataBaseName,status:'stopped',})}}
3,分片下载
/**
* @description 分片下载
* @param {String} fileName 文件名
* @param {String} url 下载地址
* @param {String} dataBaseName 数据库名 每个文件唯一
* @param {Object} progress 下载进度 type: continue:续传 again:重新下载 cancel:取消
*
*/exportasyncfunctiondownloadByBlock(fileName, url, dataBaseName, progress){//调整下载状态if(progress.type){awaitcontrolFile(fileName, progress.type, dataBaseName);}// 判断是否超过最大下载数量let downloadNum = store.state.progressList;if(downloadNum){if(!progress.type && downloadNum.length == downloadMaxNum){
ElementUI.Message.error("已超过最大下载量,请等待其他文件下载完再尝试!");return;}}// 基础数据库const baseDataBase =createInstance(baseDataBaseName);// 判断数据库中是否已存在该文件的下载任务let isError =false;awaitgetData(dataBaseName, baseDataBase,async(res)=>{if(res &&!progress.type){
ElementUI.Message.error("该文件已在下载任务列表,无需重新下载!");
isError =true;}});if(isError){return;}// 储存数据的数据库const dataBase =createInstance(dataBaseName);// 获取文件大小const response =await axios.head(url);// 文件大小const fileSize =+response.headers['content-length'];// 分片大小const chunkSize =+response.headers['buffer-size'];// 开始节点let start =0;// 结束节点let end = chunkSize -1;if(fileSize < chunkSize){
end = fileSize -1;}// 所有分片let ranges =[];// 计算需要分多少次下载const numChunks = Math.ceil(fileSize / chunkSize);// 回写文件大小
progress.fileSize = fileSize;
progress.url = url;// 保存数据库实例awaitsaveData(dataBaseName, fileName, baseDataBase);if(!progress.progress){// 保存进度数据awaitsaveData(progressKey, progress, dataBase);}// 保存下载状态至localstorage 如果页面刷新 可重新读取状态 判断是否需要继续下载
sessionStorage.setItem(dataBaseName,"downloading");// 组转参数for(let i =0; i < numChunks; i++){// 创建请求控制器 const controller =newAbortController();const range =`bytes=${start}-${end}`;// 如果是续传 先判断是否已下载if(progress.type =='continue'){// 先修改状态
store.commit('setProgress',{dataInstance: dataBaseName,status:'downloading'})let isContinue =false;awaitgetData(range, dataBase,asyncfunction(res){if(res) isContinue =true});if(isContinue){
ranges.push(range);// 重置开始节点
start = end +1;// 重置结束节点
end = Math.min(end + chunkSize, fileSize -1);continue;}}let cancel;const config ={headers:{Range: range
},responseType:'arraybuffer',// 绑定取消请求的信号量signal: controller.signal,// 文件下载进度监听onDownloadProgress:function(pro){if(progress.type =='stop'|| progress.type =='cancel'){// 终止请求
controller.abort();}// 已加载大小// progress对象中的loaded表示已经下载的数量,total表示总数量,这里计算出百分比let downProgress =(pro.loaded / pro.total);
downProgress = Math.round((i / numChunks)*100+ downProgress / numChunks *100);
progress.progress = downProgress;// 设置为异常 如果是正常下载完 这个记录会被删除 如果合并失败 则会显示异常
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);// 保存分片数据awaitsaveData(range, response.data, dataBase);
ranges.push(range);// 重置开始节点
start = end +1;// 重置结束节点
end = Math.min(end + chunkSize, fileSize -1);}catch(error){
console.log("终止请求时catch的error---", error);// 判断是否为取消上传if(error.message =="canceled"){// 进行后续处理if(progress.type =='stop'){// 暂停awaitcontrolFile(fileName, progress.type, dataBaseName);
sessionStorage.setItem(dataBaseName,"stop");// 终止请求
console.log("stopped");return;}elseif(progress.type =='cancel'){// 取消awaitcontrolFile(fileName, progress.type, dataBaseName);
sessionStorage.removeItem(dataBaseName);// 终止请求
console.log("canceled");return;}};}}
console.log("开始合入");// 流操作const fileStream = streamSaver.createWriteStream(fileName,{size: fileSize});const writer = fileStream.getWriter();// 合并数据// 循环取出数据合并for(let i=0; i < ranges.length; i++){var range = ranges[i];// 从数据库获取分段数据awaitgetData(range, dataBase,asyncfunction(res){if(res){// 读取流const reader =newBlob([res]).stream().getReader();while(true){const{ done, value }=await reader.read();if(done)break;// 写入流
writer.write(value)}// 结束写入if(i==ranges.length-1){
writer.close();// 清空数据库dropDataBase(dataBaseName);// 基础数据库删除数据库实例removeData(dataBaseName, baseDataBase);// 清除store
store.commit('delProgress',{dataInstance: dataBaseName
})}}});}}
二,VUE页面
<script>import{ downloadByBlock }from"上面的js";exportdefault{name:"suspension",data(){return{// 下载列表downloadDialog:false,fileStatus:{}};},watch:{"$store.state.progressList":function(){this.$nextTick(function(){let progressList =this.$store.state.progressList;
progressList.forEach(item=>{// 获取之前下载状态 还原操作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(){// 通过路由信息判断当前处于哪一个页面const page =this.$route.name;this.downloadDialog =true;},/**
* @description 取消弹窗
*/downloadBtnCancel(){this.downloadDialog =false;}}};</script>
小结
这只是一个基本示例。在实际应用中,你可能需要考虑以下问题:
并发下载: 如果多个用户同时下载相同的大文件,可能会对服务器造成很大的压力。可以使用并发下载来提高性能。
安全性: 确保只有授权用户可以下载文件,并且不会泄漏敏感信息。
性能优化: 使用缓存、压缩等技术来提高下载速度和用户体验。
服务器资源管理: 下载大文件可能会占用服务器的网络带宽和内存资源,需要适当的管理。
总之,大文件分片下载和合并是一个复杂的任务,需要综合考虑性能、安全性和用户体验。在实际项目中,你可能会使用更高级的工具和技术来处理这些问题。
版权归原作者 littleMonster_LC 所有, 如有侵权,请联系我们删除。