平时业务中下载文件方式常见的有俩种:
第一种,直接访问服务器的文件地址,自动下载文件;
第二种 ,服务器返回blob文件流,再对文件流进行处理和下载。
一般小文件适用于第一种下载方案,不占用过多服务器资源,而对于体积庞大的文件,常常使用文件流的方式进行传输,如图:
文件流传输成功后通过代码可以立即发起浏览器下载该文件流:
这种方式也有弊端,在文件流传输过程中,用户无法感知文件流的传输状态(进度),会造成一些困扰(无法确定当前下载操作是否已经生效)。针对这种情况,我们可以在页面显示文件流的状态和传输进度,提高页面交互性和友好性。
接下来是具体实现:
封装js方法:
/** * @param {Object} data: {url: 文件地址, download: 文件名称} */ export function downLoadAll(data) { let downProgress = {}; let uniSign = new Date().getTime() + ''; // 可能会连续点击下载多个文件,这里用时间戳来区分每一次下载的文件,也可以用生成器和迭代器实现文件的唯一标识(见下文) axios.get( data.url, { responseType: 'blob', headers: { "Content-Type": "application/json; charset=utf-8" }, onDownloadProgress (progress) { downProgress = Math.round(100 * progress.loaded / progress.total) // progress对象中的loaded表示已经下载的数量,total表示总数量,这里计算出百分比 store.commit('caseInformation/SET_PROGRESS', {path: uniSign, 'progress': downProgress}) // 将此次下载的文件名和下载进度组成对象再用vuex状态管理 }}).then( (res)=>{ // 文件流传输完成后,开启文件下载 if(data.downLoad){ jsFileDownload(res.data,data.downLoad+'.'+data.url.replace(/.+\./,"")); // jsFileDownLoad是用来下载文件流的,下载插件:npm i js-file-download,import引入:import jsFileDownLoad from 'js-file-download' } else { jsFileDownload(res.data, data.url.split('/')[data.url.split('/').length-1]); } }).catch((e)=>{ this.$message.error('该文件无法下载') }) }
ps:使用迭代器和生成器实现唯一性:
function* IdGenerator() { // 定义生成器IdGenerator let id = 0; while(true){ //循环生成无线长度的id序列 yield ++id; } } const idIterator = IdGenerator(); //定义idIterator迭代器
export function downLoadAll(data) {
let unSign = idIterator.next().value; //调用迭代器的next()方法获取新值
...
}
在浏览器中测试:
nice!这里用迭代器和生成器也可以唯一标识文件。有的朋友可能会产生疑问,这里用while死循环不会发生溢出吗?正常情况下是这样的,但在生成器中没问题!当生成器遇到一条yield语句,它会一直挂起执行,直到下次调用next方法,所以每次调用一次next方法,while循环才会迭代一次并返回下一个id值。
store中的caseInfomation.js:
... const state = { progressList: [], // 文件下载进度列表 ... } const mutations = { SET_PROGRESS: (state, progressObj)=>{ // 修改进度列表 if(state.progressList.length){ // 如果进度列表存在 if(state.progressList.find(item=>item.path == progressObj.path)){ // 前面说的path时间戳是唯一存在的,所以如果在进度列表中找到当前的进度对象 state.progressList.find(item=>item.path == progressObj.path).progress = progressObj.progress // 改变当前进度对象的progress } }else{ state.progressList.push(progressObj) // 当前进度列表为空,没有下载任务,直接将该进度对象添加到进度数组内 } }, DEL_PROGRESS: (state, props) => { state.progressList.splice(state.progressList.findIndex(item=>item.path == props), 1) // 删除进度列表中的进度对象 }, ... }
页面显示进度弹框代码,downLoadNotice.vue:
<template> </template> <script> import { mapState } from 'vuex' export default { name: 'downLoadNotice', computed: { ...mapState({ 'progressList': state => state.caseInformation.progressList }) }, data() { return { notify: {} // 用来维护下载文件进度弹框对象 } }, watch: { // 监听进度列表 progressList: { handler(n) { let data = JSON.parse(JSON.stringify(n)) data.forEach(item => { const domList = [...document.getElementsByClassName(item.path)] if (domList.find(i => i.className == item.path)) { // 如果页面已经有该进度对象的弹框,则更新它的进度progress domList.find(i => i.className == item.path).innerHTML = item.progress + '%' } else { if (item.progress === null) { // 此处容错处理,如果后端传输文件流报错,删除当前进度对象 this.$store.commit('caseInformation/DEL_PROGRESS', item.path) return }// 如果页面中没有该进度对象所对应的弹框,页面新建弹框,并在notify中加入该弹框对象,属性名为该进度对象的path(上文可知path是唯一的),属性值为$notify(element ui中的通知组件)弹框对象 this.notify[item.path] = this.$notify.success({ // title: 'Info', dangerouslyUseHTMLString: true, message: `<p style="width: 100px;">正在下载<span class="${item.path}" style="float: right">${item.progress}%</span></p>`, // 显示下载百分比,类名为进度对象的path(便于后面更新进度百分比) showClose: false, duration: 0 }) } console.log(item.progress + '%', '-------------------------->') if (item.progress == 100) { // 如果下载进度到了100%,关闭该弹框,并删除notify中维护的弹框对象 this.notify[item.path].close() // delete this.notify[item.path] 上面的close()事件是异步的,这里直接删除会报错,利用setTimeout,将该操作加入异步队列 setTimeout(() => { delete this.notify[item.path] }, 1000) this.$store.commit('caseInformation/DEL_PROGRESS', item.path)// 删除caseInformation中state的progressList中的进度对象 } }) }, deep: true } } } </script> <style scoped> </style>
以上代码我们可以封装到mixins文件夹中,在页面内用mixins混入:
页面中触发下载操作:
downLoad(item){ let downData = { url: `ipdoc${item.url}`, downLoad: item.fileName } this.$commonUtils.downLoadAll(downData) // 下载 },
最终页面的效果:
最后注意一点,以上的下载进度并不是真正下载的文件,而是文件流,文件流下载完成后,再通过上文的js-file-download插件下载真正的文件!
脚踏实地行,海阔天空飞