引言
今天小明收到一个不同寻常的需求,批量导出页面内的模块作为图片,小明一开始还有点愣住,后面做完这个需求觉得有点意思,就又双叒叕来和大家分享一起学习一下。
需求就如下面的图片所示,可分为三个部分
- 整个页面导出
- 分模块导出
- 可以选择不同的背景颜色
技术调研
要做这个需求前,小明是就去做了个调研,调研的内容有如下几点
- 前端如何将dom节点导出图片
- 前端如何把多张图片压缩成zip包下载
调研结果如下
-
导出技术
使用的是dom-to-image这个包对dom节点进行导出
-
压缩技术
使用的是jszip对各个图片进行压缩处理
有了这个dom-to-image,就可以把目标dom节点转化成一个图片,转一个图片知道怎么做了,剩下的工作就是如何多个节点,并将结果用jszip整合导出。
实现方案
小明思虑良久,突然想到上下文那个好东西,用在这个场景,恰到好处,大概步骤如下
- 在父组件提供一个导出图片的注册方法
- 在子组件要导出图片的地方将导出方法注册(封装为组件更方便)
- 点击导出时候,将注册的导出图片方法依次调用获取结果,拼接资源下载
首先,先在上层组件(就是上图的父组件)使用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,会有问题,换成别的格式就好