前端图片批量导出实践

145 阅读3分钟

引言

今天小明收到一个不同寻常的需求,批量导出页面内的模块作为图片,小明一开始还有点愣住,后面做完这个需求觉得有点意思,就又双叒叕来和大家分享一起学习一下。

需求就如下面的图片所示,可分为三个部分

  • 整个页面导出
  • 分模块导出
  • 可以选择不同的背景颜色

image.png

技术调研

要做这个需求前,小明是就去做了个调研,调研的内容有如下几点

  • 前端如何将dom节点导出图片
  • 前端如何把多张图片压缩成zip包下载

调研结果如下

有了这个dom-to-image,就可以把目标dom节点转化成一个图片,转一个图片知道怎么做了,剩下的工作就是如何多个节点,并将结果用jszip整合导出。

实现方案

小明思虑良久,突然想到上下文那个好东西,用在这个场景,恰到好处,大概步骤如下

  • 在父组件提供一个导出图片的注册方法
  • 在子组件要导出图片的地方将导出方法注册(封装为组件更方便)
  • 点击导出时候,将注册的导出图片方法依次调用获取结果,拼接资源下载

image.png

首先,先在上层组件(就是上图的父组件)使用useImgExporter

这个useImgExporter是给下层组件提供一个注册和解除注册的函数

并暴露出一个exportAllImages去对所有子组件注册于这里的导出函数进行调用,之后合并压缩成zip进行下载

export const useImgExporter = () => {
  let exportFuncArray: IExportFunc[] = [];
  const regeistExportFunc = (func: IExportFunc) => {
    exportFuncArray = [...exportFuncArray, func];
  };
  const unregeistExportFunc = (func: IExportFunc) => {
    exportFuncArray = exportFuncArray.filter(item => item !== func);
  };

  provide(ctxKey, {
    regeistExportFunc,
    unregeistExportFunc,
  });

  const exportAllImages = async (zipName: string) => {
    // 这里如果全部一起调用太多会卡顿的话,就分批次,我导出的量适中,没造成性能问题就直接promise all了
    const imageList = await Promise.all(exportFuncArray.map(fn => fn()));
    // 创建一个新的 JSZip 实例
    const zip = new JSZip();

    // 将每个图片 Blob 添加到 ZIP 文件中
    imageList.forEach(({ file, fileName }) => {
      zip.file(`${fileName}.png`, file); // 可以根据需要修改文件名和扩展名
    });
    const content = await zip.generateAsync({ type: 'blob' });
    createATagToDownload({ href: content, fileName: `${zipName}.zip` });
  };

  return { exportAllImages };
};
其次,对每一个需要导出的地方使用ImgExportWrapper
<ImgExportWrapper img-name="导出图片的名称">
    <div>客户转化率</div>
    <EmChart :option="option"> </EmChart>
</ImgExportWrapper>

这个ImgExportWrapper里面是使用useImgExportRegister,在组件挂载和卸载时将导出自身节点的方法注册到第一步代码中的exportFuncArray数组中,以备点出导出时候进行调用

下面则是ImgExportWrapper这个组件的实现

export default defineComponent({
  setup(props, { slots, emit }) {
    const currentDom = ref(null);
    // 这个是一个上下文变量,用了支持同一块有一个统一的前缀名称,导出的文件名称比较有序
    const prefixRef = useImgExportNamePrefix();
    // 这个是使用domtoimage导出当前图片的方法
    const exportImage = async () => {
      const prefix = prefixRef.value ? `${prefixRef.value}-` : '';
      const scale = 2;
      const blobData = await domtoimage.toBlob(currentDom.value, {
        quality: 1,
        height: currentDom.value.offsetHeight * scale,
        width: currentDom.value.offsetWidth * scale,
        style: {
          transform: 'scale(' + scale + ')',
          transformOrigin: 'top left',
          width: currentDom.value.offsetWidth + 'px',
          height: currentDom.value.offsetHeight + 'px',
        },
      });
      return { file: blobData as Blob, fileName: `${prefix}${props.imgName}` };
    };

    const { regeistExportFunc = noop, unregeistExportFunc = noop } = inject<ICtxType>(ctxKey) || {};
    // 将上述导出方法在挂载时机注册进去
    onMounted(() => {
      regeistExportFunc(exportFn);
    });
    // 将上述导出方法在卸载时机解除注册
    onUnmounted(() => {
      unregeistExportFunc(exportFn);
    });

    return () => {
      return (
        <div class="img-export-wrapper" ref={currentDom}>
          {slots.default?.()}
        </div>
      );
    };
  },
});

补充

对于需求中导出不同的背景颜色,处理方法就是在导出前在最外层多加个class改变个样式,或者用css变量等方式,比较简单,只是处理起来比较繁琐,这里就不做赘述

问题

  • dom-to-image这个库貌似对svg图片或者用svg去设置background,会有问题,换成别的格式就好