写在前面#
对于经常需要开发企业管理后台的前端开发来说,必不可少的需要使用表格对于数据进行操作,在对于现有项目进行代码优化时,封装一些公共的Hooks.
而在之前的文章中已经总结了封装useDownloadFile.js
的相关内容。为何还要再封装一个useDownloadBlob.js
呢?其实是对之前的封装结合实际情况进行代码优化,满足更多场景。
基于个人项目环境进行封装的Hooks,仅以本文介绍封装Hooks思想心得,故相关代码可能不适用他人
项目环境#
Vue3.x + Ant Design Vue3.x + Vite3.x
封装分解:创建a标签下载文件#
export function createDownload(blob, fileName, fileType) {
if (!blob || !fileName || !fileType) return;
const element = document.createElement('a');
const url = window.URL.createObjectURL(blob);
element.style.display = 'none';
element.href = url;
element.download = `${fileName}.${fileType}`;
document.body.appendChild(element);
element.click();
if (window.URL) {
window.URL.revokeObjectURL(url);
} else {
window.webkitURL.revokeObjectURL(url);
}
document.body.removeChild(element);
}
封装分解:下载Blob文件#
const downloadBlob = (url, fileName = '', fileType = '', autoDownload = false) => {
return new Promise((resolve, reject) => {
xhr = new XMLHttpRequest();
xhr.responseType = 'blob';
xhr.open('get', url, true);
xhr.onprogress = function (e) {
progress.value = Math.floor((e.loaded / e.total) * 100);
if (progress.value === 100) {
progress.value = 0;
downloading = false;
}
};
xhr.onloadend = function (e) {
if ([200, 304].includes(e.target.status)) {
const blob = e.target.response;
if (autoDownload) {
createDownload(blob, fileName, fileType);
}
xhr = null;
resolve(blob);
}
};
xhr.onerror = function (e) {
downloading = false;
Modal.error({
title: '温馨提示',
content: '下载发生异常,请重试',
});
reject(e);
};
xhr.send();
});
};
相信已经有读者盆友已经看出来以上两段代码均与useDownloadFile.js
内容一致,这也是出于实际工作中代码优化的考虑,对于封装应当尽可能的模块化,这样在处理可能多场景下的内容依旧适用。先前的封装useDownloadFile
已经可以解决管理后台中对于文件下载的需求,结合实际业务,当需要进行管理同一用户/业务内的多个资料统一打包时,可以尝试封装一下下载文件压缩包的方式来解决该业务场景。下面的内容则是介绍相关的实现方法与封装思路。
封装分解:JS压缩—JSZip#
A library for creating, reading and editing .zip files with JavaScript, with a lovely and simple API.
JSZip 支持各种类型的资源uint8array、blob、arraybuffer、nodebuffer、string
等,结合现有封装,非常适用。实际使用到的API有2个,分别是zip.file() 和zip.generateAsync()。相关的API文档已经介绍很详细了,在此不再赘述,毕竟官方文档写的还是很好的~Promise风格的API用起来是非常舒服的~
const zip = new JSZip(); // 创建一个Zip对象
for (let i = 0, len = fileList.length; i < len; i++) {
const item = fileList[i];
const fileType = item.fileType ? item.fileType : item.url.split('.').pop();
curDownloadFileName.value = item.fileName;
const blob = await downloadBlob(item.url);
zip.file(`${item.fileName}.${fileType}`, blob); // 创建/更新文件到Zip File内,blob数据流
successCount.value++;
}
downloading = false;
infoModal && infoModal.destroy();
zip.generateAsync({ type: 'blob' } // 在当前文件夹级别生成完整的 zip 文件
封装分解:用户体验设计#
下载过程中,配合项目使用的Ant Design Vue框架,可以加强用户感知文件下载进度
infoModal = Modal.info({
title: '文件批量下载',
okText: '取消下载',
icon: h('span'),
width: 580,
content: () => {
return h('div', { class: 'mt-4' }, [
h('div', { class: 'fs-16 font-bold' }, ['文件下载过程中请勿关闭当前页面']),
h('div', { className: 'mt-2' }, [`总文件数:${fileList.length},已下载文件数:${successCount.value}`]),
h('div', { className: 'mt-2 ellipsis' }, [`当前下载文件名:${curDownloadFileName.value}`]),
h('div', { className: 'mt-2' }, [`当前文件下载进度:${progress.value}%`]),
]);
},
onOk() {
xhr.abort();
xhr = null;
return Promise.resolve();
},
});
封装分解:下载文件压缩包Zip#
const downloadZip = async (fileList = [], fileName) => {
let infoModal;
const successCount = ref(0);
const curDownloadFileName = ref('');
infoModal = Modal.info({
title: '文件批量下载',
okText: '取消下载',
icon: h('span'),
width: 580,
content: () => {
return h('div', { class: 'mt-4' }, [
h('div', { class: 'fs-16 font-bold' }, ['文件下载过程中请勿关闭当前页面']),
h('div', { className: 'mt-2' }, [`总文件数:${fileList.length},已下载文件数:${successCount.value}`]),
h('div', { className: 'mt-2 ellipsis' }, [`当前下载文件名:${curDownloadFileName.value}`]),
h('div', { className: 'mt-2' }, [`当前文件下载进度:${progress.value}%`]),
]);
},
onOk() {
xhr.abort();
xhr = null;
return Promise.resolve();
},
});
const zip = new JSZip();
for (let i = 0, len = fileList.length; i < len; i++) {
const item = fileList[i];
const fileType = item.fileType ? item.fileType : item.url.split('.').pop();
curDownloadFileName.value = item.fileName;
const blob = await downloadBlob(item.url);
zip.file(`${item.fileName}.${fileType}`, blob);
successCount.value++;
}
downloading = false;
infoModal && infoModal.destroy();
zip
.generateAsync({ type: 'blob' })
.then(content => {
createDownload(content, fileName, 'zip');
})
.catch(error => {
console.error(error);
});
};
到这里,就是针对之前的useDownloadFile
改造的主要内容~
在实际工作中,其实一个良好的习惯就在于保持对代码的”更新”,毕竟随着时间的推移,每个人都会收获成长,那么以前写的代码或多或少有些许不合理。又或许你接手的是前任埋下的”屎山”代码,但毕竟不是每个老板/公司都愿意给你时间大刀阔斧的重构~不止是封装Hooks,亦或是其他的,利用闲碎时间优化一下代码吧,毕竟在这个”寒冷的环境下”,适当的优化也是提高人效的一部分哦~
useDownloadFile.js完整代码#
import { h, onBeforeUnmount, ref } from 'vue';
import { Modal } from 'ant-design-vue';
import JSZip from 'jszip';
import { createDownload } from '@/utils/util';
export function useDownloadFile() {
let xhr = null;
let downloading = false; // 限制同一文件同时触发多次下载
const progress = ref(0);
onBeforeUnmount(() => {
if (xhr) {
xhr.abort();
xhr = null;
}
});
// 下载文件blob
const downloadBlob = (url, fileName = '', fileType = '', autoDownload = false) => {
return new Promise((resolve, reject) => {
xhr = new XMLHttpRequest();
xhr.responseType = 'blob';
xhr.open('get', url, true);
xhr.onprogress = function (e) {
progress.value = Math.floor((e.loaded / e.total) * 100);
if (progress.value === 100) {
progress.value = 0;
downloading = false;
}
};
xhr.onloadend = function (e) {
if ([200, 304].includes(e.target.status)) {
const blob = e.target.response;
if (autoDownload) {
createDownload(blob, fileName, fileType);
}
xhr = null;
resolve(blob);
}
};
xhr.onerror = function (e) {
downloading = false;
Modal.error({
title: '温馨提示',
content: '下载发生异常,请重试',
});
reject(e);
};
xhr.send();
});
};
// 下载文件
const downloadFile = async options => {
try {
let infoModal;
if (downloading || !options.url || !options.fileName) return;
downloading = true;
options.url = options.url.replace('http://', 'https://');
let fileType = '';
if (options.fileType) {
fileType = options.fileType;
} else {
fileType = options.url.split('.').pop();
}
infoModal = Modal.info({
title: '文件下载',
okText: '取消下载',
icon: h('span'),
content: () => {
return h('div', { class: 'mt-4' }, [
h('div', { class: 'fs-16 font-bold' }, ['文件下载过程中请勿关闭当前页面']),
h('div', { className: 'mt-2' }, [`当前下载进度 ${progress.value}%`]),
]);
},
onOk() {
xhr.abort();
xhr = null;
return Promise.resolve();
},
});
await downloadBlob(options.url, options.fileName, fileType, true);
downloading = false;
infoModal && infoModal.destroy();
} catch (e) {
console.error(e);
}
};
// 下载文件压缩包zip
const downloadZip = async (fileList = [], fileName) => {
let infoModal;
const successCount = ref(0);
const curDownloadFileName = ref('');
infoModal = Modal.info({
title: '文件批量下载',
okText: '取消下载',
icon: h('span'),
width: 580,
content: () => {
return h('div', { class: 'mt-4' }, [
h('div', { class: 'fs-16 font-bold' }, ['文件下载过程中请勿关闭当前页面']),
h('div', { className: 'mt-2' }, [`总文件数:${fileList.length},已下载文件数:${successCount.value}`]),
h('div', { className: 'mt-2 ellipsis' }, [`当前下载文件名:${curDownloadFileName.value}`]),
h('div', { className: 'mt-2' }, [`当前文件下载进度:${progress.value}%`]),
]);
},
onOk() {
xhr.abort();
xhr = null;
return Promise.resolve();
},
});
const zip = new JSZip();
for (let i = 0, len = fileList.length; i < len; i++) {
const item = fileList[i];
const fileType = item.fileType ? item.fileType : item.url.split('.').pop();
curDownloadFileName.value = item.fileName;
const blob = await downloadBlob(item.url);
zip.file(`${item.fileName}.${fileType}`, blob);
successCount.value++;
}
downloading = false;
infoModal && infoModal.destroy();
zip
.generateAsync({ type: 'blob' })
.then(content => {
createDownload(content, fileName, 'zip');
})
.catch(error => {
console.error(error);
});
};
return {
downloadFile,
downloadZip,
};
}
备注说明#
由于本篇文章是基于实际封装的优化,故实际项目中Hooks代码依然叫做useDownloadFile。【优化代码/叠加功能,尽可能不破坏原有的结构或引入】