截图方案终极杀招(下)

旧闻重发,由于上一次的图片有些糊,这次分为上下两篇发送,不至于阅读压力太大

上一篇:‘截图方案终极杀招(上)’

 

           换个思路

基于我们对于上篇html2canvas整个流程的实现,会发现中间换算会出现很多不精准的问题,那么怎么做一个可以精准的绘制呢?能不能把所有内部绘制的换算过程全部交给浏览器?

 

基本思路

    上文提到canvas还可以绘制image、svg等等,此处就可以把html处理成svg的结果,然后再绘制到canvas上。

    对于svg是一种可扩展标记语言,在转化的过程中,就需要使用到<foreignObject>这个svg元素。<foreignObject>允许包含不同的XML命名空间,在浏览器的上下文中,很可能是XHTML\HTML,如下是使用方式:

              

这样只需要指定对应的命名空间,就可以把它嵌套到foreignObject中,然后结合SVG,直接渲染。

 

什么是命名空间,相当于是元素名和属性名的一种集合,元素和属性可以有多种不同的集合,为了解决冲突,就需要有命名空间的指派,对于带有属性xmlns=""就是一个命名空间的表现形式。以下是多种命名空间:

  • HTML — http://www.w3.org/1999/xhtml

  • SVG — http://www.w3.org/2000/svg

  • MathML—  http://www.w3.org/1998/math/MathML

  • 对于不同的命名空间,浏览器解析的方式也不一样,所以在SVG中嵌套HTML,解析SVG的时候遇到 http://www.w3.org/2000/svg 转化SVG的解析方式,当遇到了http://www.w3.org/1999/xhtml 就使用html的解析方式。

这是为什么SVG中可以嵌套HTML,并且浏览器能够正常渲染。

 

实现

但是这个过程中,会存在一些问题:

  1. SVG 是不允许连接到外部的资源,比如html中图片链接、css link方式的资源链接等,在SVG中都会有限制;

  2. html中会有脚本执行的情况,比如Vue的SPA单页项目,需要先执行js的逻辑才能够渲染出dom节点。但是SVG中,是不支持js执行的情况。

  3. SVG的位置大小和foreignObject标签的位置大小不能够确定,需要计算。

 

基于以上的情况,需要做一些其他的处理,以下为这个方案渲染的整个流程,看看如何解决存在的问题:

 

             

对于这种方案需要处理以上几个流程:

  1. 初始化不同类型的截图需要,比如DrawHTML(截取部分文档片段)、DrawDocument(截取完整document节点)、DrawURL(截取一个html资源链接)这几种形式,最后都会处理成截取整个document文档节点,以下是流程第一步的处理,。

  • DrawHTML 转换部分文档片段为一个完整的document文档节点,然后使用DrawDocument的方式处理。

             

DrawURL 转换一个html资源链接为截取一个完整的document文档节点,再使用DrawDocument的方式处理。              可以看到最后的方式都是处理成一个document文档,实现到drawDocument这个方法里面,使用绘制document的形式来渲染。

基于上面的思路,把document文档转为SVG,但是document文档里面包含了外部链接的图片资源、外部样式资源和脚本资源。这种情况在SVG是不支持的,所以这一步的处理方式是把所有的外部资源,处理为内联形式的,改造为新的document,比如:

             以上这种文档结构中,所有的资源都是属于外部资源,如果要转变为SVG,就需要处理成内联的形式,构造新的document文档,如下:

             

所以上一步把所有截图形式都处理成为了渲染一个document文档之后,就需要对文档进行重构转换,处理文档内部所有外部资源,不同的资源对应不同的处理方式,这里需要处理的资源情况分为以下几点:

 

在html文档中存在img图片标签的链接为外部资源,需要处理为base64资源,通过loadAndInlineIages函数进行处理,以下是loadAndInlineIages函数。              loadAndInlineImages函数的处理流程是获取到所有和图片有关的标签,在通过ajajx请求下来,然后处理成base64的资源类型,对原有的图片标签进行替换,这样就把所有的标签图片,处理成为了内联资源类型。以下是encodeImageAsDataURI方法内部请求图片资源且转义base64的逻辑:

             通过了以上步骤之后,此时的document文档里面的图片标签元素的资源已经全部为内联形式了

 

    在html中同时也存在着脚本为外部资源的情况,对于脚本的处理逻辑,整体就比较简单了,获取到脚本的链接,请求脚本内容,之后用请求的内容替换原有的外部链接的<script>,以下为脚本处理函数loadAndInlineScript的实现方式:

             

以上处理脚本资源的方法整体比较简单。

 

  • 在处理完成了脚本和图片的情况之后,目前剩余需要处理成为内联资源的情况还剩下外部样式表。但是此处还需要注意一点,对于本来存在的内联样式也需要处理,因为可能会出现使用外链背景图的情况、通过@import导入样式表的情况。

所以对于外部样式表请求下来的内容会存在同样的问题,所以对于外部样式表而言,整体的流程就是通过ajax请求外部样式内容,然后对内容存在背景图片和@import的情况做处理。先供上对于css处理不同情况的流程处理:

             

通过上面的架构流程图,可以看出来远端请求的样式表需要和内联样式做同样的处理,把内部的远端图片资源和字体资源处理为内联形式。

  • 对外部样式表的请求逻辑,大致逻辑如下:              

通过以上代码,可以看见请求和处理逻辑全部在requestStylesheetAndInlineResources方法中,以下为代码方法:

             

从以上的代码逻辑中,可以清楚,有几个promise的处理流程,每一个流程处理的内容主要做了以下几件事情:

    1. 请求远端样式资源表,通过封装的ajax方法;

    2. 处理请求下来的样式表中可能使用到的远端图片或者字体资源链接,使用inlineCss.adjustPathsOfCssResources方法,把使用到资源的相对地址,处理成为绝对地址;

    3. 通过inlineCss.loadCSSImportsForRule方法处理@import资源引入的情况

    4. 请求样式表中使用到的图片和文字资源,并且处理成内联,这一步的逻辑在inlineCss.loadAndInlineCSSResourcesForRules这个方法中

    5. 基于原有样式表构造新的样式表

 

现在我们来看一下,对应每一种处理情况具体所做的事情:

  • ajax请求资源,这一步不做深入,简单的ajax封装

  • 对于adjustPathsOfCssResources方法处理链接相对路劲变为绝对路劲,整体的实现思路是遍历查找所有的CSSRule,查找到background、font-face、@import等对应的Rule,解析属性设置的值,判断引用的地址是否是外部url,处理路劲变换为绝对路劲。构建新的CSSRule。              

通过上面的逻辑处理之后,此时所有的css中包含的外部资源的链接已经处理为绝对路劲,对于整个资源css中的资源内联处理,第一步就已经完成了。

  • 对于处理完成路劲之后,对于上面整个资源处理的大流程loadCSSImportsForRule方法就是把import的外部css请求回来,然后重新构建新的css。大体的思路为搜集当前css中所有的import资源地址,下载下来之后,构建为新的css,在分析新的css是否包含import,递归写入到最后的CSSRule中。              对于以上代码处理@import的函数中,loadAndInlineCSSImport方法就是核心的逻辑了,结合上面讲的整体处理流程,看看以下代码:

             

这样就把所有的css中的@import的资源,也处理进来了。

 

  • 对于css资源,处理到这一步之后,结合我们上面的流程图,就只剩下把所有的资源诸如背景图、font-face等引用的外部链接变为内联资源。这一步的实现和上面css中转换资源相对路劲到绝对路劲,整个思路是一致的。区别在于对于最后一步替换相对路劲为绝对路劲的url不一致,这里需要替换的是资源请求下来之后处理成为base64的data数据之后的链接。

    1. 首先遍历所有CSSRule,找出需要替换的所有Rule

    2. 获取对应Rule中包含的外部链接

    3. 请求资源回来之后,处理为base64类型的data链接

    4. 替换原有Rule中资源的地址,改为内联类型,构造成为新的CSSRule;

 

这样整个流程中的资源就已经处理完成,目前构造出来的文档,全是内联文档,符合构造SVG的要求;

  1. 在处理完成内容之后,就需要计算整个文档需要展示的大小,这是在SVG构建的时候需要使用到的;因为在用户截图的时候回传入对应想要的大小,这个时候,怎么去控制。大致的思路如下:

    • 根据用户传入宽高大小创建iframe,把上面处理过的内联文档装载到iframe中执行

    • 获取到执行之后文档的clientWidth和clientHeight,同时根据zoom计算缩放的大小来作为最后SVG需要渲染的结果

    • 获取装载之后iframe中的文档的font-size来设置SVG的内容字体大小

经过上面这些步骤,我们计算出来了大小,剩下最后一步,序列化处理之后的文档节点构建SVG;

  1. 序列化文档节点的过程,就是把文档节点处理成为整个字符串的过程,在大多数浏览器中都是有序列化api的支持,不过有少数兼容问题,所以最优方法为自己实现序列化的过程,整个过程逻辑主要为递归遍历文档节点,处理节点名称大小写、文本内容中包含<、>、&这几个符号的转义处理及对整个文档添加指定的命名空间。

  2. 在序列化文档文档之后,就需要使用序列化之后的内容和计算出来的展示文档大小值来构建SVG,整个构建的过程代码大致流程:              至此,SVG构建已经构建完成,剩下最后一步就是把SVG处理成图片可以显示的资源;

 

6.处理图片显示的资源这个过程,其实有两种实现:

  • 第一种是通过createObjectURL把图片资源处理为blob数据,img使用时直接使用blob数据;

  • 第二种是直接encode对应的SVG资源,构建data资源链接              这两种生成的连接都可以对应添加到图片的src中;当然,此时也可以拿到对应的SVG调用canvas绘图的api来绘制SVG,做二次加工;

 

至此,这个思路的实现全部完成;

 

 

思路缺点

基于以上两个思路的对比,明显会发现,使用html通过foreignObject构建SVG的方法要简单清晰,但是对于一些浏览器也会有一些小问题,不过已经有一个比较不错的库通过hack的方式,处理了这些问题。rasterizeHTML.js是一个比较不错的截图库,实现的逻辑就是基于上面的思路。

 

 

不过这两种方式都会涉及到一个问题,就是图片资源跨域问题,如果图片为跨域图片,就需要通过CORS来处理。由于在 `canvas` 位图中的像素可能来自多种来源,包括从其他主机检索的图像或视频,因此不可避免的会出现安全问题,所以对于除CORS以外的跨域图片,canvas都会被处理成污染的情况,此时getImageData、toBlob、toDataURL都会被禁止调用,这种机制也可以避免未经许可拉取远程网站信息而导致的用户隐私泄露,这对于webgl的贴图也是同样的处理,不能使用除CORS以外的跨域图片。

 

 

 

             总结

    以上总结了html2canvas的整体思路及优缺点,目前html2canvas源码里面也已经开始融合第二种思路,这说明了第二种截图思路的优点。但是第二种思路的过程中自己手动处理的序列化性能相比浏览器处理而言略微慢一点,等到浏览器序列化都支持的特别好的时候,就可以替代这一部分。当然,咱们也可以打开思路,结合webassembly来重写序列化的部分,打开整个BS架构大门。

加下方小助手微信,回复“前端”加内部交流群,定期抽奖,大厂内推,面试题精讲。

 

 

       

END

获得更多信息

关注公众号