{{format('0')}} {{format('220')}} {{format('4528')}}

【前端】如何优雅的生成网页截图 [ 前端 ]

晚安月亮 文章 正文

挣钱给咖喱买最好吃的罐罐
分享

椰奶冻

{{nature("2023-08-02 11:47:02")}}更新

如何优雅的生成网页截图

这篇文章起源于一个页面生成图片进行下载分享的需求,因为浏览器没有原生的截图API,所以需要借助canvas来实现导出图片实现需求。

首先要知道,svg 到图像的转换过程涉及:

  1. 创建 Blob/Blob URL(参阅什么是 Blob URL 以及为什么使用它?
  2. 将其渲染到画布上
  3. 返回数据url

可行性方案

  • 方案1: 将 DOM 改写成 canvas ,调用canvas的toBlob或者toDataURL方法即刻上传到七牛云或服务器
  • 方案2: 使用第三方库实现 canvas , 在不更改页面已有DOM的情况下优雅产生canvas

解决方案的选择

  • 方案1:需要手动计算每个DOM元素的Computed Style,然后需要计算好元素在canvas的大小位置等属性。 方案1难点
    1. 需要弃用已有的html页面,改用canvas重写。
    2. 页面结构层复杂的情况下用canvas写,不易重构。
    3. 有一定canvas基础。
  • 方案2:在Github上有很多此功能的第三方库,并且作者仍在积极维护。API非常简单,在已有项目中开箱即用。因为是常见的需求,所以社区会有成熟的解决方案,首先试试社区的解决方案。
    • html2canvas
    • html-to-image
    • dom-to-image

使用第三方库

1. html2canvas

  const saveAsImage = async (format: any) => {
    try {
      const dataURL = await new Promise<string>((resolve) => {
        setTimeout(() => {
          html2canvas(ref.current as HTMLDivElement, { imageTimeout: 5000, allowTaint: true, useCORS: true }).then(
            (canvas) => {
              const dataURL = canvas.toDataURL(`image/${format}`);
              resolve(dataURL);
            }
          );
        }, 1000);
      });
      saveAs(dataURL, `report.${format}`);
    } catch (error) {
      console.error(error);
    }
  };

痛点:

  1. 截取过程中如果页面中遇到iframe是不生效的,表现为直接空白。
  2. 对于使用第三方UI组件库的页面进行截屏,会存在文本(像素)偏移问题。
  3. 需要遍历所有dom,截图时间太长,要通过ignoreElements过滤掉大部分没用的标签。
  4. 单文件js过大,增大打包体积。

2. html-to-image

const saveAsImage = async (format: any) => {
    try {
      const dataURL = await new Promise<string>((resolve) => {
        setTimeout(() => {
          htmlToImage
            .toPng(ref.current as HTMLDivElement)
            .then((dataURL) => {
              resolve(dataURL);
            })
            .catch((error: any) => {
              console.error(error);
              resolve('');
            });
        }, 1000);
      });
      if (dataURL) {
        saveAs(dataURL, `report.${format}`);
      } else {
        console.log('保存失败');
      }
    } catch (error) {
      console.error(error);
    }
  };

优点:

  1. 生成速度快
  2. js文件小,打包速度影响较小

痛点:

  1. 不兼容Safari浏览器,会遇到跨域问题

3. dom-to-image

  const saveAsImage = async (format: any) => {
    try {
      const dataURL = await new Promise<string>((resolve) => {
        setTimeout(() => {
          domtoimage
            .toPng(ref.current as HTMLDivElement, {
              width: ref.current.offsetWidth,
              height: ref.current.offsetHeight,
              style: {
                transform: 'scale(1)',
                'transform-origin': 'top left',
                width: `${ref.current.offsetWidth}px`,
                height: `${ref.current.offsetHeight}px`,
              },
              // @ts-ignore
              filter: (node: HTMLElement) => {
                return node.tagName !== 'IFRAME';
              },
              cacheBust: true,
            })
            .then((dataURL) => {
              resolve(dataURL);
            })
            .catch((error: any) => {
              console.error(error);
              resolve('');
            });
        }, 1000);
      });
      if (dataURL) {
        saveAs(dataURL, `report.${format}`);
      } else {
       console.log('保存失败');
      }
    } catch (error) {
      console.error(error);
    }
  };

最后选用了这个方案,原因是

  1. toSVG可以兼容Safari的跨域问题

  2. 包体积相对于html-to-image更小

痛点

  1. 在Safari上只能生成svg格式,不能生成jpg

最后学习一下它的原理: dom-to-image使用SVG的一个特性,它允许在标记中包含任意HTML内容。

  • 递归地克隆原始DOM节点
  • 计算节点和每个子节点的样式,并将其复制到相应的克隆
    • 创建伪元素,因为它们不是以任何方式克隆的
  • 嵌入web字体
    • 查找所有@font face声明的web字体
    • 解析文件URL,下载相应文件
    • base64编码的内联作为data:URLs
    • 将所有已处理的CSS放入中,然后将其附加到克隆
  • 嵌入图片
    • 再嵌入图片URL
    • 使用backgroundCSS属性的图片,方法类似于字体
  • 将克隆的节点序列化为XML
  • 将XML包装到标记中,然后包装到SVG中,然后使其成为data URL
  • 或者,要以Uint8Array的形式获取PNG内容或原始像素数据,可以创建一个以SVG为源的图像元素,并将其呈现在已经创建的canvas上,从canvas读取内容

domtoimage的核心api:

  • toSvg
  • toPng
  • toJpeg
  • toBlob
  • toPixelData

例:toJpeg:将draw函数返回的canvas实例,使用canvas的toDataURL方法生成jpeg图片。toSvg函数将递归地克隆原始DOM节点, 将克隆的节点序列化为XML,将XML包装到标记中,然后包装到SVG中,然后使其转成dataURL。

总结

在一开始优化下载速度的过程时我一直考虑的是哪个第三方库的速度更快?兼容性更强?没有考虑过每个库的实现方式差异,项目上线后才意识到,其实有些步骤是可以在页面渲染完就开始的,比如生成dataURL。因为最耗时的地方就是生成URL的过程,提前生成的话在用户点击时几乎可以达到秒下载。从这也体现出了我思维的局限性,能想到的解决方案还是太少了。最后写这篇文章的时候还学习到了很多别的办法,比如Puppeteer + Nodejs截图,由于对我的项目不适用还没有进行过尝试,但是可以发现一个简单的需求也有很多可以学习的点。今日记一事,明日悟一理,积久而成学~

评论 0
0
{{userInfo.data?.nickname}}
{{userInfo.data?.email}}
TOP 2
【笔经】数字马力前端笔试-22应届

{{nature('2022-06-23 23:10:58')}} {{format('1478')}}人已阅读

TOP 3
【React】React组件卸载生命周期、路由跳转、页面关闭(刷新)拦截提示

{{nature('2023-02-03 16:12:08')}} {{format('1400')}}人已阅读

TOP 4
【前端】npm/yarn报错:getaddrinfo ENOTFOUND registry.nlark.com

{{nature('2024-05-29 15:14:38')}} {{format('1031')}}人已阅读

TOP 5
【前端】jspdf+dom-to-image生成多页A4pdf加防截断处理

{{nature('2024-05-24 15:20:21')}} {{format('910')}}人已阅读

目录

标签云

React

一言

# {{hitokoto.data.from || '来自'}} #
{{hitokoto.data.hitokoto || '内容'}}
作者:{{hitokoto.data.from_who || '作者'}}
自定义UI
配色方案

侧边栏