页面截图html2canvas、dom-to-image、html-to-image、modern-screenshot
页面上有大量的图表,用户的述求是能对页面截屏从而直接分享给别人。
那么就有小伙伴要发问了,为什么不直接把页面链接分享给别人呢?
首先,页面可能有权限校验,被分享的人可能没有该页面的访问权限,而图片不会有这个问题;其次,实践表明,如果分享的是链接,用户的点击意愿很低,如果不是直接相关的人往往不会点开链接查看,而如果是图片的话,非常直观,往往第一眼就传递了很多信息给被分享的人。
那么又有小伙伴要发问了,既然如此,为何不让用户自己装一个截屏软件自己截算了?
考虑两个点,第一是不一定所有用户都有一个好用的截屏的软件(特别是在Mac上,大伙应该深有体会),并且页面如果需要滚动截屏,用户的操作就会比较麻烦,因此页面上能提供一个一键截屏的按钮就十分便利了;第二是如果由页面提供截图能力,可以很好地定制最终图片上所呈现的页面,比如可以调整一下布局,修改某些元素。
不过需要注意的是,我们要实现的截屏并不是一个真正的截屏,而是相当于dom的快照,针对传入的dom生成图片。
那么咱就来研究研究,市面上都有哪些截屏的方案。
一种比较常见的方案,是在服务端使用puppeteer(或者playwright啥的)起一个无头浏览器,渲染完页面后截图返回给前端,比如金山文档就是这么做的。
但是吧,这种方案的缺陷很明显。首先毋庸置疑的是,服务端的压力会变大,成本会变高;其次,最终生成的图片往往与用户所看到的页面有些出入,比如金山文档的截屏,如果源文档是些奇奇怪怪的字体,最终生成的图片里的字体就会是默认字体,另外布局什么的也可能会不一致;
源文档:
生成的图片:
那么后端方案优点也就与缺点一一对应,首先是对用户设备的消耗较小,性能较差的设备也能使用;其次是对于同一页面,后端方案生成图片能够完全一致,不会因为用户的机型不同导致页面布局发生变化,而且更重要的一点是,生成图片基本上都依赖于canvas,而canvas这东西有个坑,它对宽、高、面积有一定的限制,并且不同浏览器、不同设备的限制还不太一样,并且同一设备同一浏览器也会因为用户的设备可用资源受到影响,在生成canvas之前也不能拿到这个限制,这个限制在IOS设备上最为严重(有意思的是canvas是苹果提出的标准),参考javascript - Maximum size of a element - Stack Overflow,因此采用后端方案能够保证结果的一致性。
有的小伙伴会说了,浏览器自带截屏功能的,直接用多好呀。是的,浏览器有一个截屏功能,但是我们在JS代码里并没法直接调用,并且浏览器自带的截屏,也无法实现上述所说的修改页面元素的能力。
浏览器自带截屏:
那么比较靠谱的前端截屏方案其实就两种,一种自己实现渲染,将dom一一渲染到canvas上后生成图片,比如html2canvas;另一种是借助foreignObject,将svg绘制到canvas上再生成图片,代表作为dom-to-image。
html2canvas可以说是最古老的前端截屏实现方案了,也称得上是独一档的实现。它的原理简单来说就是克隆传入的dom,遍历克隆树,通过getBoundingClientRect获取元素的位置、大小,getComputedStyle获取元素样式,然后使用canvas的底层API,一点一点画出来的。
可想而知,这个过程是多么复杂,相当于自己实现了一套渲染引擎,并且css越来越复杂,想要完全绘制到canvas,够呛,所以html2canvas现在有一个很大的缺点就是对css的支持不够好。
另外,由于它自建了一套渲染,需要处理的情况非常多,所以包体积相当大,官网标注的gzip压缩后也有45kB。
除了上述原因外,真正让我放弃这个库的原因是,它太老了,它真的太老了,作为一个十几年前的库,它现在已经年久失修,上次更新都是两年前,而且看着只是文档修改。
并且已经堆积了800+ issue没有处理,基本上是不维护状态了。
更有意思的是,即使这个库已经存在了十几年,并且有大量页面将其应用到了生产环境,其中不乏一些大公司产品,比如腾讯文档(别问我怎么知道的,问就是我写的),但是它的作者仍在Readme里边写到:
dom-to-image的基本原理十分简单,不需要做什么复杂的渲染,利用到了svg元素的foreignObject:
只需要把dom丢到foreignObject里边,就会在svg里边渲染出来,因为是浏览器的标准,也不用担心对css的支持不够友好:
其实,到这一步,你会发现已经达到将dom转成图片的目的了,svg本来就是图片。但是你可能会需要其他格式的图片,并且这样生成的svg体积实在是大了点,包含了大量冗余的信息。所以这里还是用到canvas,通过drawImage把svg画到canvas上,再通过canvas的toDataUrl生成图片链接。
从体积上看,不到10kB,是完全可以接受的:
看看它的代码仓库,可以看到已经七八年不更新了,并且有200+ issue没有处理,也基本上处于不维护状态了。:
如果能够满足需求,也不是不能用,遗憾的是,不太能满足我的需求。
首先是资源跨域问题,其实资源本身是支持跨域的,但是原始html中的标签没有加上crossorigin属性,导致生成图片时会报跨域错误,像页面里的图片、外链css啥的得做点特殊处理才能用。另外还有些奇奇怪怪的问题,可以看看issue,反正是不太能用。
dom-to-image-more听名字也能听出来是fork的dom-to-image,解决了dom-to-image的部分bug,增加了一些能力。最重要的能力应该是解决了上述提到的跨域问题,它把link标签做了一下拦截,使用fetch去请求对应的src,加上了跨域配置,然后再对返回结果进行处理。另外还有一个有意思的点,在dom-to-image中,获取元素的样式是通过document.getComputedStyle拿到每个dom节点的样式,然后通过行内样式插入到对应的标签上,会导致最后生成的图片上包含了大量的行内样式,体积自然就比较大;而dom-to-image-more做了一个优化,利用沙盒获取到了元素的默认样式,再和getComputedStyle作比较,只插入不同于默认样式的属性,从而极大地减小了图片的体积,自然而然,这个复杂度高了点,生成图片的耗时稍微长点。
体积很理想,不到6kB:
之前看最新的更新在两年前,但是近期好像又有更新,说明还是有人在维护的:
但是最终还是没有用它,因为有个痛点,在我的场景下用了很多icon,而这些icon都是svg格式的,它们通过defs - SVG定义了一次,然后使用时都是通过 - SVG引用的;但是这个库没有处理这种情况,导致生成图片时只复制了use元素,而没有将其对应的defs元素复制过去,从而导致最终生成的图片上丢失了这些icon。
html-to-image也是fork的dom-to-image,修了部分bug,增加了一些能力。这个库相较于dom-to-image,特点是优化了文件结构,增加typescript支持,对比上述的dom-to-image-more,处理好了svg use和svg defs的情况,在有use的情况下会去找到对应的defs元素并添加进来。但是,它没有解决跨域问题。
另外还有个痛点,之前提到的icon,它们的样式吧,上面我们提到了,是通过getComputedStyle获取到,然后插入到行内样式实现的;对于普通的dom元素而言,这样做没有问题,因为这些dom使用的地方就是它们定义的地方;但是对于svg defs和svg use这样的元素而言,在定义时它的样式就已经被行内样式写死了,使用的时候就没办法覆盖定义时的样式,导致我的彩色icon全变成黑色了:
原图:
生成的图片:
看了下源代码,确实没有针对这点进行处理,所以还是放弃了,另外可想而知的是,像webcomponent这样定义和使用分离的情况,估计也存在样式不能覆盖的问题。
modern-screenshot也是基于dom-to-image,但它不是直接fork的dom-to-image,而是上面提到的html-to-image,所以相当于是dom-to-image的孙子辈了。
这个库既然是fork的html-to-image,自然也就继承了html-to-image良好的文件结构以及优秀的ts支持;并且这个库有意思的是,它还整合了dom-to-image-more的优化,不会产生跨域的问题了;对于svg use和svg defs,它更进一步,复用已有的defs,减小了生成图片的体积;另外还有个点,它用到了webworker并行地发起网络请求。
东抄抄西补补,modern-screenshot是目前我看到的效果最理想的前端截屏方案,并且这个库的作者仍在维护:
最近的更新发生在三周前,包体积gzip压缩后不到10kB,完全可以接受。
美中不足的是,这个库依然没有解决上述提到的svg use样式不能覆盖问题。其实想想也明白,通过getComputedStyle再写入行内样式的方式,这个问题是避免不了的。不过,考虑到svg defs元素一般都是icon在使用,而这些icon一般来说不会被外界样式所影响,所以针对svg defs和svg use标签,我们不通过getComputedStyle获取其样式,而是直接使用dom.cloneNode获取的样式,这样就不会写死行内样式,从而解决了这个问题。于是给该项目提了一个PR,也顺利合入:
当然这种解法并不严谨,但是绝大部分情况下应该够用,至少在我的场景下已经足够满足需求,因此最终我也是选择了使用modern-screenshot来实现截屏的需求。