整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

你不知道的浏览器渲染原理

浏览器的内核是指支持浏览器运行的最核心的程序,分为两个部分的,一是渲染引擎,另一个是 JS 引擎。渲染引擎在不同的浏览器中也不是都相同的。目前市面上常见的浏览器内核可以分为这四种:Trident(IE)、Gecko(火狐)、Blink(Chrome、Opera)、Webkit(Safari)。这里面大家最耳熟能详的可能就是 Webkit 内核了,Webkit 内核是当下浏览器世界真正的霸主。

本文我们就以 Webkit 为例,对现代浏览器的渲染过程进行一个深度的剖析。

想阅读更多优质文章请猛戳GitHub 博客。

页面加载过程

在介绍浏览器渲染过程之前,我们简明扼要介绍下页面的加载过程,有助于更好理解后续渲染过程。

要点如下:

  • 浏览器根据 DNS 服务器得到域名的 IP 地址;
  • 向这个 IP 的机器发送 HTTP 请求;
  • 服务器收到、处理并返回 HTTP 请求;
  • 浏览器得到返回内容。

例如在浏览器输入https://juejin.im/timeline,然后经过 DNS 解析,juejin.im对应的 IP 是36.248.217.149(不同时间、地点对应的 IP 可能会不同)。然后浏览器向该 IP 发送 HTTP 请求。

服务端接收到 HTTP 请求,然后经过计算(向不同的用户推送不同的内容),返回 HTTP 请求,返回的内容如下:


其实就是一堆 HMTL 格式的字符串,因为只有 HTML 格式浏览器才能正确解析,这是 W3C 标准的要求。接下来就是浏览器的渲染过程。

浏览器渲染过程


浏览器渲染过程大体分为如下三部分:

1)浏览器会解析三个东西:

一是 HTML/SVG/XHTML,HTML 字符串描述了一个页面的结构,浏览器会把 HTML 结构字符串解析转换 DOM 树形结构。


二是 CSS,解析 CSS 会产生 CSS 规则树,它和 DOM 结构比较像。


三是 Javascript 脚本,等到 Javascript 脚本文件加载后, 通过 DOM API 和 CSSOM API 来操作 DOM Tree 和 CSS Rule Tree。


2)解析完成后,浏览器引擎会通过 DOM Tree 和 CSS Rule Tree 来构造 Rendering Tree。

  • Rendering Tree 渲染树并不等同于 DOM 树,渲染树只会包括需要显示的节点和这些节点的样式信息。
  • CSS 的 Rule Tree 主要是为了完成匹配并把 CSS Rule 附加上 Rendering Tree 上的每个 Element(也就是每个 Frame)。
  • 然后,计算每个 Frame 的位置,这又叫 layout 和 reflow 过程。

3)最后通过调用操作系统 Native GUI 的 API 绘制。

接下来我们针对这其中所经历的重要步骤详细阐述

构建 DOM

浏览器会遵守一套步骤将 HTML 文件转换为 DOM 树。宏观上,可以分为几个步骤:


浏览器从磁盘或网络读取 HTML 的原始字节,并根据文件的指定编码(例如 UTF-8)将它们转换成字符串。

在网络中传输的内容其实都是 0 和 1 这些字节数据。当浏览器接收到这些字节数据以后,它会将这些字节数据转换为字符串,也就是我们写的代码。

将字符串转换成 Token,例如:<html>、<body>等。Token 中会标识出当前 Token 是“开始标签”或是“结束标签”亦或是“文本”等信息

这时候你一定会有疑问,节点与节点之间的关系如何维护?

事实上,这就是 Token 要标识“起始标签”和“结束标签”等标识的作用。例如“title”Token 的起始标签和结束标签之间的节点肯定是属于“head”的子节点。


上图给出了节点之间的关系,例如:“Hello”Token 位于“title”开始标签与“title”结束标签之间,表明“Hello”Token 是“title”Token 的子节点。同理“title”Token 是“head”Token 的子节点。

  • 生成节点对象并构建 DOM

事实上,构建 DOM 的过程中,不是等所有 Token 都转换完成后再去生成节点对象,而是一边生成 Token 一边消耗 Token 来生成节点对象。换句话说,每个 Token 被生成后,会立刻消耗这个 Token 创建出节点对象。注意:带有结束标签标识的 Token 不会创建节点对象。

接下来我们举个例子,假设有段 HTML 文本:

复制代码

<html>
<head>
 <title>Web page parsing</title>
</head>
<body>
 <div>
 <h1>Web page parsing</h1>
 <p>This is an example Web page.</p>
 </div>
</body>
</html>

上面这段 HTML 会解析成这样:


构建 CSSOM

DOM 会捕获页面的内容,但浏览器还需要知道页面如何展示,所以需要构建 CSSOM。

构建 CSSOM 的过程与构建 DOM 的过程非常相似,当浏览器接收到一段 CSS,浏览器首先要做的是识别出 Token,然后构建节点并生成 CSSOM。


在这一过程中,浏览器会确定下每一个节点的样式到底是什么,并且这一过程其实是很消耗资源的。因为样式你可以自行设置给某个节点,也可以通过继承获得。在这一过程中,浏览器得递归 CSSOM 树,然后确定具体的元素到底是什么样式。

注意:CSS 匹配 HTML 元素是一个相当复杂和有性能问题的事情。所以,DOM 树要小,CSS 尽量用 id 和 class,千万不要过渡层叠下去

构建渲染树

当我们生成 DOM 树和 CSSOM 树以后,就需要将这两棵树组合为渲染树。


在这一过程中,不是简单的将两者合并就行了。渲染树只会包括需要显示的节点和这些节点的样式信息,如果某个节点是 display: none 的,那么就不会在渲染树中显示。

我们或许有个疑惑:浏览器如果渲染过程中遇到 JS 文件怎么处理

渲染过程中,如果遇到<script>就停止渲染,执行 JS 代码。因为浏览器有 GUI 渲染线程与 JS 引擎线程,为了防止渲染出现不可预期的结果,这两个线程是互斥的关系。JavaScript 的加载、解析与执行会阻塞 DOM 的构建,也就是说,在构建 DOM 时,HTML 解析器若遇到了 JavaScript,那么它会暂停构建 DOM,将控制权移交给 JavaScript 引擎,等 JavaScript 引擎运行完毕,浏览器再从中断的地方恢复 DOM 构建。

也就是说,如果你想首屏渲染的越快,就越不应该在首屏就加载 JS 文件,这也是都建议将 script 标签放在 body 标签底部的原因。当然在当下,并不是说 script 标签必须放在底部,因为你可以给 script 标签添加 defer 或者 async 属性(下文会介绍这两者的区别)。

JS 文件不只是阻塞 DOM 的构建,它会导致 CSSOM 也阻塞 DOM 的构建

原本 DOM 和 CSSOM 的构建是互不影响,井水不犯河水,但是一旦引入了 JavaScript,CSSOM 也开始阻塞 DOM 的构建,只有 CSSOM 构建完毕后,DOM 再恢复 DOM 构建。

这是什么情况?

这是因为 JavaScript 不只是可以改 DOM,它还可以更改样式,也就是它可以更改 CSSOM。因为不完整的 CSSOM 是无法使用的,如果 JavaScript 想访问 CSSOM 并更改它,那么在执行 JavaScript 时,必须要能拿到完整的 CSSOM。所以就导致了一个现象,如果浏览器尚未完成 CSSOM 的下载和构建,而我们却想在此时运行脚本,那么浏览器将延迟脚本执行和 DOM 构建,直至其完成 CSSOM 的下载和构建。也就是说,在这种情况下,浏览器会先下载和构建 CSSOM,然后再执行 JavaScript,最后在继续构建 DOM


布局与绘制

当浏览器生成渲染树以后,就会根据渲染树来进行布局(也可以叫做回流)。这一阶段浏览器要做的事情是要弄清楚各个节点在页面中的确切位置和大小。通常这一行为也被称为“自动重排”。

布局流程的输出是一个“盒模型”,它会精确地捕获每个元素在视口内的确切位置和尺寸,所有相对测量值都将转换为屏幕上的绝对像素。

布局完成后,浏览器会立即发出“Paint Setup”和“Paint”事件,将渲染树转换成屏幕上的像素。

以上我们详细介绍了浏览器工作流程中的重要步骤,接下来我们讨论几个相关的问题:

几点补充说明

1.async 和 defer 的作用是什么?有什么区别?

接下来我们对比下 defer 和 async 属性的区别:


其中蓝色线代表 JavaScript 加载;红色线代表 JavaScript 执行;绿色线代表 HTML 解析。

1)情况 1<script src="script.js"></script>

没有 defer 或 async,浏览器会立即加载并执行指定的脚本,也就是说不等待后续载入的文档元素,读到就加载并执行。

2)情况 2<script async src="script.js"></script> (异步下载)

async 属性表示异步执行引入的 JavaScript,与 defer 的区别在于,如果已经加载好,就会开始执行——无论此刻是 HTML 解析阶段还是 DOMContentLoaded 触发之后。需要注意的是,这种方式加载的 JavaScript 依然会阻塞 load 事件。换句话说,async-script 可能在 DOMContentLoaded 触发之前或之后执行,但一定在 load 触发之前执行。

3)情况 3 <script defer src="script.js"></script>(延迟执行)

defer 属性表示延迟执行引入的 JavaScript,即这段 JavaScript 加载时 HTML 并未停止解析,这两个过程是并行的。整个 document 解析完毕且 defer-script 也加载完成之后(这两件事情的顺序无关),会执行所有由 defer-script 加载的 JavaScript 代码,然后触发 DOMContentLoaded 事件。

defer 与相比普通 script,有两点区别:载入 JavaScript 文件时不阻塞 HTML 的解析,执行阶段被放到 HTML 标签解析完成之后。

在加载多个 JS 脚本的时候,async 是无顺序的加载,而 defer 是有顺序的加载。

2. 为什么操作 DOM 慢?

把 DOM 和 JavaScript 各自想象成一个岛屿,它们之间用收费桥梁连接。——《高性能 JavaScript》

JS 是很快的,在 JS 中修改 DOM 对象也是很快的。在 JS 的世界里,一切是简单的、迅速的。但 DOM 操作并非 JS 一个人的独舞,而是两个模块之间的协作。

因为 DOM 是属于渲染引擎中的东西,而 JS 又是 JS 引擎中的东西。当我们用 JS 去操作 DOM 时,本质上是 JS 引擎和渲染引擎之间进行了“跨界交流”。这个“跨界交流”的实现并不简单,它依赖了桥接接口作为“桥梁”(如下图)。


过“桥”要收费——这个开销本身就是不可忽略的。我们每操作一次 DOM(不管是为了修改还是仅仅为了访问其值),都要过一次“桥”。过“桥”的次数一多,就会产生比较明显的性能问题。因此“减少 DOM 操作”的建议,并非空穴来风。

3. 你真的了解回流和重绘吗?

渲染的流程基本上是这样(如下图黄色的四个步骤):

1. 计算 CSS 样式

2. 构建 Render Tree

3.Layout – 定位坐标和大小

4. 正式开画


注意:上图流程中有很多连接线,这表示了 Javascript 动态修改了 DOM 属性或是 CSS 属性会导致重新 Layout,但有些改变不会重新 Layout,就是上图中那些指到天上的箭头,比如修改后的 CSS rule 没有被匹配到元素。

这里重要要说两个概念,一个是 Reflow,另一个是 Repaint

重绘:当我们对 DOM 的修改导致了样式的变化、却并未影响其几何属性(比如修改了颜色或背景色)时,浏览器不需重新计算元素的几何属性、直接为该元素绘制新的样式(跳过了上图所示的回流环节)。

回流:当我们对 DOM 的修改引发了 DOM 几何尺寸的变化(比如修改元素的宽、高或隐藏元素等)时,浏览器需要重新计算元素的几何属性(其他元素的几何属性和位置也会因此受到影响),然后再将计算的结果绘制出来,这个过程就是回流(也叫重排)。

我们知道,当网页生成的时候,至少会渲染一次。在用户访问的过程中,还会不断重新渲染。重新渲染会重复回流 + 重绘或者只有重绘。

回流必定会发生重绘,重绘不一定会引发回流。重绘和回流会在我们设置节点样式时频繁出现,同时也会很大程度上影响性能。回流所需的成本比重绘高的多,改变父节点里的子节点很可能会导致父节点的一系列回流。

1)常见引起回流属性和方法

任何会改变元素几何信息 (元素的位置和尺寸大小) 的操作,都会触发回流,

  • 添加或者删除可见的 DOM 元素;
  • 元素尺寸改变——边距、填充、边框、宽度和高度;
  • 内容变化,比如用户在 input 框中输入文字;
  • 浏览器窗口尺寸改变——resize 事件发生时;
  • 计算 offsetWidth 和 offsetHeight 属性;
  • 设置 style 属性的值。

2)常见引起重绘属性和方法


3)如何减少回流、重绘

  • 使用 transform 替代 top;
  • 使用 visibility 替换 display: none ,因为前者只会引起重绘,后者会引发回流(改变了布局);
  • 不要把节点的属性值放在一个循环里当成循环里的变量。

复制代码

for(let i = 0; i < 1000; i++) {
 // 获取 offsetTop 会导致回流,因为需要去获取正确的值
 console.log(document.querySelector('.test').style.offsetTop)
}
  • 不要使用 table 布局,可能很小的一个小改动会造成整个 table 的重新布局;
  • 动画实现的速度的选择,动画速度越快,回流次数越多,也可以选择使用 requestAnimationFrame;
  • CSS 选择符从右往左匹配查找,避免节点层级过多;
  • 将频繁重绘或者回流的节点设置为图层,图层能够阻止该节点的渲染行为影响别的节点。比如对于 video 标签来说,浏览器会自动将该节点变为图层。

性能优化策略

基于上面介绍的浏览器渲染原理,DOM 和 CSSOM 结构构建顺序,初始化可以对页面渲染做些优化,提升页面性能。

  • JS 优化: <script> 标签加上 defer 属性 和 async 属性 用于在不阻塞页面文档解析的前提下,控制脚本的下载和执行。
  • defer 属性: 用于开启新的线程下载脚本文件,并使脚本在文档解析完成后执行。
  • async 属性: HTML5 新增属性,用于异步下载脚本文件,下载完毕立即解释执行代码。
  • CSS 优化: <link> 标签的 rel 属性 中的属性值设置为 preload 能够让你在你的 HTML 页面中可以指明哪些资源是在页面加载完成后即刻需要的,最优的配置加载顺序,提高渲染性能。

总结

综上所述,我们得出这样的结论:

  • 浏览器工作流程:构建 DOM -> 构建 CSSOM -> 构建渲染树 -> 布局 -> 绘制。
  • CSSOM 会阻塞渲染,只有当 CSSOM 构建完毕后才会进入下一个阶段构建渲染树。
  • 通常情况下 DOM 和 CSSOM 是并行构建的,但是当浏览器遇到一个不带 defer 或 async 属性的 script 标签时,DOM 构建将暂停,如果此时又恰巧浏览器尚未完成 CSSOM 的下载和构建,由于 JavaScript 可以修改 CSSOM,所以需要等 CSSOM 构建完毕后再执行 JS,最后才重新 DOM 构建。

参考文章

  • https://segmentfault.com/q/1010000000640869
  • https://coolshell.cn/articles/9666.html
  • https://juejin.im/book/5bdc715fe51d454e755f75ef/section/5c024ecbf265da616a476638
  • https://mp.weixin.qq.com/s?__biz=MzA5NzkwNDk3MQ==&mid=2650588806&idx=1&sn=408a54e7c8102fd6944c9a40b119015a&chksm=8891d6a2bfe65fb42f493fe9a4dab672dd7e440f31e753196cee0cfbc6696e4f8dd3a669e040&mpshare=1&scene=1&srcid=1228ZrXsmbZKcgCSu7zTVDwy#
  • https://juejin.im/book/5b936540f265da0a9624b04b/section/5bac3a4df265da0aa81c043c
  • https://juejin.im/book/5c47343bf265da612b13e5c0/section/5c4737375188255de8397ae3
  • https://juejin.im/book/5a8f9ddcf265da4e9f6fb959/section/5a8f9f7bf265da4e82635e46

更多内容,请关注前端之巅。

着css3实现各种炫酷动画效果越来越流行。今天给大家推荐一些css3和SVG实现loading加载动画效果。

先上一波令人愉悦的动画效果。

怎么样,是不是感觉很nice,那就继续往下看吧。这里为大家整理了一些不错的效果。

1、CSSFX

简单酷炸的css3效果,可一键复制css样式。

# 演示地址
https://cssfx.lovejade.cn/

# github地址
https://github.com/TheHumanComedy/cssfx

2、SVG Loaders

一个超棒的svg实现loading动画效果。

# 演示地址
http://samherbert.net/svg-loaders/

# github地址
https://github.com/SamHerbert/SVG-Loaders

3、SpinKit

非常棒的一款CSS3加载动画集合。star高达17.4K+

# 演示地址
https://tobiasahlin.com/spinkit/

# github地址
https://github.com/tobiasahlin/SpinKit

4、CSS Spinners

一款有趣的单元素CSS3加载器。

# 演示地址
https://projects.lukehaas.me/css-loaders/

# github地址
https://github.com/lukehaas/css-loaders

5、Canvas Loaders

基于canvas实现的loading效果。

https://github.com/padolsey/sonic.js

6、CSS3 Loading Spinners

一组很酷的css3加载动画效果。

# 演示地址
https://codepen.io/Beaugust/pen/DByiE

7、Pure CSS3 Loader

有一组优秀的CSS3动画效果。

# 演示地址
https://codepen.io/viduthalai1947/pen/JkhDK

8、SVG Loading Images

一组SVG实现加载动画效果。

# 演示地址
https://codepen.io/aurer/pen/jEGbA

好了,就介绍到这里。希望对大家有所帮助!

者开源了一个Web思维导图,在做导出为图片的功能时走了挺多弯路,所以通过本文来记录一下。

思维导图的节点和连线都是通过 svg渲染的,作为一个纯 js 库,我们不考虑通过后端来实现,所以只能思考如何通过纯前端的方式来实现将svg或html转换为图片。

使用img标签结合canvas导出

我们都知道 img 标签可以显示 svg,然后 canvas 又可以渲染 img,那么是不是只要将svg渲染到img标签里,再通过canvas导出为图片就可以呢,答案是肯定的。

const svgToPng = async (svgStr) => {
    // 转换成blob数据
    let blob = new Blob([svgStr], {
      type: 'image/svg+xml'
    })
    // 转换成data:url数据
    let svgUrl = await blobToUrl(blob)
    // 绘制到canvas上
    let imgData = await drawToCanvas(svgUrl)
    // 下载
    downloadFile(imgData, '图片.png')
}

svgStr是要导出的svg字符串,比如:

然后通过Blob构造函数创建一个类型为image/svg+xml的blob数据,接下来将blob数据转换成data:URL:

const blobToUrl = (blob) => {
    return new Promise((resolve, reject) => {
        let reader = new FileReader()
        reader.onload = evt => {
            resolve(evt.target.result)
        }
        reader.onerror = err => {
            reject(err)
        }
        reader.readAsDataURL(blob)
    })
}

其实就是base64格式的字符串。

接下来就可以通过img来加载,并渲染到canvas里进行导出:

const drawToCanvas = (svgUrl) => {
    return new Promise((resolve, reject) => {
      const img = new Image()
      // 跨域图片需要添加这个属性,否则画布被污染了无法导出图片
      img.setAttribute('crossOrigin', 'anonymous')
      img.onload = async () => {
        try {
          let canvas = document.createElement('canvas')
          canvas.width = img.width
          canvas.height = img.height
          let ctx = canvas.getContext('2d')
          ctx.drawImage(img, 0, 0, img.width, img.height)
          resolve(canvas.toDataURL())
        } catch (error) {
          reject(error)
        }
      }
      img.onerror = e => {
        reject(e)
      }
      img.src = svgUrl
    })
}

canvas.toDataURL()方法返回的也是一个base64格式的data:URL字符串:

最后就可以通过a标签来下载:

const downloadFile = (file, fileName) => {
  let a = document.createElement('a')
  a.href = file
  a.download = fileName
  a.click()
}

实现很简单,效果也不错,不过这样就没问题了吗,接下来我们插入两张图片试试。

处理存在图片的情况

第一张图片是使用base64的data:URL方式插入的,第二张图片是使用普通url插入的:

导出结果如下:

可以看到,第一张图片没有问题,第二张图片裂开了,可能你觉得同源策略的问题,但实际上换成同源的图片,同样也是裂开的,解决方法很简单,遍历svg节点树,将图片都转换成data:URL的形式即可:

// 操作svg使用了@svgdotjs/svg.js库
const transfromImg = (svgNode) => {
    let imageList = svgNode.find('image')
    let task = imageList.map(async item => {
      // 获取图片url
      let imgUlr = item.attr('href') || item.attr('xlink:href')
      // 已经是data:URL形式不用转换
      if (/^data:/.test(imgUlr)) {
        return
      }
      // 转换并替换图片url
      let imgData = await drawToCanvas(imgUlr)
      item.attr('href', imgData)
    })
    await Promise.all(task)
    return svgNode.svg()// 返回svg html字符串
}

这里使用了前面的drawToCanvas方法来将图片转换成data:URL,这样导出就正常了:

到这里,将纯 svg 转换为图片就基本没啥问题了。

处理存在foreignObject标签的情况

svg提供了一个foreignObject标签,可以插入html节点,实际上,笔者就是使用它来实现节点的富文本编辑效果的:

接下来使用前面的方式来导出,结果如下:

明明显示没有问题,导出时foreignObject内容却发生了偏移,这是为啥呢,其实是因为默认样式的问题,页面全局清除了margin和padding,以及将box-sizing设置成了border-box:

那么当svg存在于文档树中时是没有问题的,但是导出时使用的是svg字符串,是脱离于文档的,所以没有这个样式覆盖,那么显示自然会出现问题,知道了原因,解决方法有两种,一是遍历所有嵌入的html节点,手动添加内联样式,注意一定要给所有的html节点都添加,只给svg、foreignObject或最外层的html节点添加都是不行的;第二种是直接在foreignObject标签里添加一个style标签,通过style标签来加上样式,并且只要给其中一个foreignObject标签添加就可以了,两种方式看你喜欢哪种,笔者使用的是第二种:

const transformForeignObject = (svgNode) => {
    let foreignObjectList = svgNode.find('foreignObject')
    if (foreignObjectList.length > 0) {
        foreignObjectList[0].add(SVG(`<style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }
        </style>`))
    }
    return svgNode.svg()
}

导出结果如下:

可以看到,一切正常。

关于兼容性的问题,笔者测试了最新的chrome、firefox、opera、safari、360急速浏览器,运行都是正常的。

踩坑记录

前面介绍的是笔者目前采用的方案,看着实现其实非常简单,但是过程漫长且坎坷,接下来,开始我的表演。

foreignObject标签内容在firefox浏览器上无法显示

对于svg的操作笔者使用的是svg.js库,创建富文本节点的核心代码大致如下:

import { SVG, ForeignObject } from '@svgdotjs/svg.js'

let html = `<div>节点文本</div>`
let foreignObject = new ForeignObject()
foreignObject.add(SVG(html))
g.add(foreignObject)

SVG方法是用来将一段html字符串转换为dom节点的。

在chrome浏览器和opera浏览器上渲染非常正常,但是在firefox浏览器上foreignObject标签的内容完全渲染不出来:

检查元素也看不出有任何问题,并且神奇的是只要在控制台元素里编辑一下嵌入的html内容,它就可以显示了,百度搜索了一圈,也没找到解决方法,然后因为firefox浏览器占有率并不高,于是这个问题就搁浅了。

使用img结合canvas导出图片里foreignObject标签内容为空

chrome浏览器虽然渲染是正常的:

但是使用前面的方式导出时foreignObject标签内容却是跟在firefox浏览器里显示一样是空的:

firefox能忍这个不能忍,于是尝试使用一些将html转换为图片的库。

使用html2canvas、dom-to-image等库

使用html2canvas:

import html2canvas from 'html2canvas'

const useHtml2canvas = async (svgNode) => {
    let el = document.createElement('div')
    el.style.position = 'absolute'
    el.style.left = '-9999999px'
    el.appendChild(svgNode)
    document.body.appendChild(el)// html2canvas转换需要被转换的节点在文档中
    let canvas = await html2canvas(el, {
        backgroundColor: null
    })
    mdocument.body.removeChild(el)
    return canvas.toDataURL()
}

html2canvas可以成功导出,但是存在一个问题,就是foreignObject标签里的文本样式会丢失:

这应该是html2canvas的一个bug,不过看它这issues数量和提交记录:

指望html2canvas改是不现实的,于是又尝试使用dom-to-image:

import domtoimage from 'dom-to-image'

const dataUrl = domtoimage.toPng(el)

发现dom-to-image更不行,导出完全是空白的:

并且它上一次更新时间已经是五六年前,所以没办法,只能回头使用html2canvas。

后来有人建议使用dom-to-image-more,粗略看了一下,它是在dom-to-image库的基础上修改的,尝试了一下,发现确实可以,于是就改为使用这个库,然后又有人反馈在一些浏览器上导出节点内容是空的,包括firefox、360,甚至chrome之前的版本都不行,笔者只能感叹,太难了,然后又有人建议使用上一个大版本,可以解决在firefox上的导出问题,但是笔者试了一下,在其他一些浏览器上依旧存在问题,于是又在考虑要不要换回html2canvas,虽然它存在一定问题,但至少不是完全空的。

解决foreignObject标签内容在firefox浏览器上无法显示的问题

用的人多了,这个问题又有人提了出来,于是笔者又尝试看看能不能解决,之前一直认为是firefox浏览器的问题,毕竟在chrome和opera上都是正常的,这一次就想会不会是svgjs库的问题,于是就去搜它的issue,没想到,还真的搜出来了issue,大意就是因为通过SVG方法转换的dom节点是在svg的命名空间下,也就是使用document.createElementNS方法创建的,导致部分浏览器渲染不出来,归根结底,这还是不同浏览器对于规范的不同实现导致的:

你说chrome很强吧,确实,但是无形中它阻止了问题的暴露。

知道了原因,那么修改也很简单了,只要将SVG方法第二个参数设为true即可,或者自己来创建节点也可以:

foreignObject.add(document.createElemnt('div'))

果然,在firefox浏览器上正常渲染了。

解决img结合canvas导出图片为空的问题

解决了在firefox浏览器上foreignObject标签为空的问题后,自然会怀疑之前使用img结合canvas导出图片时foreignObject标签为空会不会也是因为这个问题,同时了解了一下dom-to-image库的实现原理,发现它也是通过将dom节点添加到svg的foreignObject标签中实现将html转换成图片的,那么就很搞笑了,我本身要转换的内容就是一个嵌入了foreignObject标签的svg,使用dom-to-image转换,它会再次把传给它的svg添加到一个foreignObject标签中,这不是套娃吗,既然dom-to-image-more能通过foreignObject标签成功导出,那么不用它必然也可以,到这里基本确信之前不行就是因为命名空间的问题。

果然,在去掉了dom-to-image-more库后,重新使用之前的方式成功导出了,并且在firefox、chrome、opera、360等浏览器中都不存在问题,兼容性反而比dom-to-image-more库好。

总结

虽然笔者的实现很简单,但是dom-to-image-more这个库实际上有一千多行代码,那么它到底多做了些什么呢,点个关注,我们下一篇文章再见。