公共Hooks封装之文件下载useDownloadBlob

发布时间 2024-01-04 14:50:49作者: 柯基与佩奇

项目环境

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。【优化代码/叠加功能,尽可能不破坏原有的结构或引入】