logo

公共Hooks封装之文件下载useDownloadBlob

Jun 21, 2023 · 12 min

写在前面#

对于经常需要开发企业管理后台的前端开发来说,必不可少的需要使用表格对于数据进行操作,在对于现有项目进行代码优化时,封装一些公共的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。【优化代码/叠加功能,尽可能不破坏原有的结构或引入】

参考链接#

> cd ..