注册
web

记一次页面截图需求

需求背景



上图是我所负责的监控产品,页面上有大量的图表,用户的述求是能对页面截屏从而直接分享给别人。


那么就有小伙伴要发问了,为什么不直接把页面链接分享给别人呢?


首先,页面可能有权限校验,被分享的人可能没有该页面的访问权限,而图片不会有这个问题;其次,实践表明,如果分享的是链接,用户的点击意愿很低,如果不是直接相关的人往往不会点开链接查看,而如果是图片的话,非常直观,往往第一眼就传递了很多信息给被分享的人。


那么又有小伙伴要发问了,既然如此,为何不让用户自己装一个截屏软件自己截算了?


考虑两个点,第一是不一定所有用户都有一个好用的截屏的软件(特别是在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


html2canvas可以说是最古老的前端截屏实现方案了,也称得上是独一档的实现。它的原理简单来说就是克隆传入的dom,遍历克隆树,通过getBoundingClientRect获取元素的位置、大小,getComputedStyle获取元素样式,然后使用canvas的底层API,一点一点画出来的。


可想而知,这个过程是多么复杂,相当于自己实现了一套渲染引擎,并且css越来越复杂,想要完全绘制到canvas,够呛,所以html2canvas现在有一个很大的缺点就是对css的支持不够好。


另外,由于它自建了一套渲染,需要处理的情况非常多,所以包体积相当大,官网标注的gzip压缩后也有45kB。



除了上述原因外,真正让我放弃这个库的原因是,它太老了,它真的太老了,作为一个十几年前的库,它现在已经年久失修,上次更新都是两年前,而且看着只是文档修改。



并且已经堆积了800+ issue没有处理,基本上是不维护状态了。



更有意思的是,即使这个库已经存在了十几年,并且有大量页面将其应用到了生产环境,其中不乏一些大公司产品,比如腾讯文档(别问我怎么知道的,问就是我写的),但是它的作者仍在Readme里边写到:



dom-to-image


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


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


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


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来实现截屏的需求。


modern-screenshot使用


modern-screenshot用起来也很简单,安装完成之后,只需少量代码即可使用:


// html
<div class="container">
<tw-el-tooltip content="生成图片" placement="top" hide-after="0">
<div class="config-button" @click="generateImage">
<el-icon
:config="common_system_picture"
color="#898A8C"
>
</el-icon>
</div>
</tw-el-tooltip>

</div>

// js
import { domToJpeg } from 'modern-screenshot';

const generateImage = async () => {
// 获取要生成图片的dom节点
const node = document.getElementById('service-analyzer-main');
if (!node) {
return;
}
try {
const dataUrl = await domToJpeg(node, {
// 传入配置
scale: 2,
});

// 通过a标签自动下载图片
const a = document.createElement('a');
a.href = dataUrl;
a.download = route.path.split('/').at(-1) + '.jpg';
a.click();
a.remove();
ElMessage.success('图片生成成功,请耐心等待下载');
} catch (error) {
ElMessage.error('图片生成失败');
}
};

原图(使用截屏软件截的长图):



通过modern-screenshot生成的图片:



当然,这是最基本的使用。如果涉及到一些复杂的操作,比如需要在截图时,对某些元素进行修改,比如把图片转成url展示,就需要在截图前遍历dom树进行一些转换再生成图片了。如果这都还不能满足需求,可能就需要专门实现一个预览模式,通过iframe打开预览模式的页面再生成图片了,腾讯文档就是这么做的。


还有些坑


苹果两倍屏


用Mac的小伙伴应该知道,Mac的屏幕是所谓的Retina屏,它的像素密度是普通屏幕的两倍,因此如果直接生成图片的话,在Mac上看起来会是比较模糊的,因此在生成图片时需要将其放大两倍。


截图元素有滚动条导致图片截断


如果截图元素有滚动条,会导致最终生成的图片只包含当前滚动条区域内的内容。如果想要截取完整内容,可以通过将临时截图元素的宽高设置为fit-content,让其展示完整,截图完成后再修改为原始宽高即可。不可避免的,这种方式会对原始元素产生一些影响,滚动条会“跳一下”,不过问题不大,实在介意的话可以加个遮罩层,在生成图片时盖住就好。


缩放后canvas元素模糊


如果在生成图片时设置scale选项,将其放大,可以看到最终生成的图片上canvas元素虽然放大了,但是并不够清晰。这个是没办法避免的,毕竟canvas是像素级别的画布,做不到无损放大,同理可以推断像素图比如jpg、png这些经过放大后也会模糊。


元素内部的滚动元素


另外还有一个坑点,如果截图元素内部的元素有滚动条,生成的图片只能包含可视区域内的部分。其实这是合理的,当然不能全都包含进来,问题是,我有一个页面,截图元素内部存在滚动元素,但是这个滚动元素的默认滚动位置是居中的,而生成图片时这个元素的滚动位置只能是左上角,因此最终生成的图片就没有我想要的内容:


原图:



生成的图片:



而且好像并没有办法指定滚动条的位置。


这让我想到之前看到别人分享的一个有意思的bug,说是他的服务接入了第三方地图服务,但是不管他如何调试,找遍了官方文档,他的页面始终是蓝色的。后续排查了很久,终于发现,原来这个地图默认定位是在经纬度(0, 0),而这里是一片大海。。。



内容过长时生成空白图片


这个其实就是我一开始提到的canvas存在限制的问题:javascript - Maximum size of a element - Stack Overflow


小结


那么事情就是这么个事情啦,主要是记录一下当时我做截图需求过程中调研的一些方案,以及对应的优缺点,并记录了一些坑。其中也包含了一些我在选择三方库时的考量,比如npm下载量、最近更新情况、仓库维护情况、包体积大小、项目功能完善程度、仓库质量、ts支持情况等。


看到这里,如果对你有所帮助的话,可以给我一些鼓励哦。


作者:超级无敌大怪兽
来源:juejin.cn/post/7339671825646338057

0 个评论

要回复文章请先登录注册