DevUI是一支兼具设计视角和工程视角的团队,服务于华为云DevCloud平台和华为外部数个中后盾零碎,服务于设计师和前端工程师。

官方网站:devui.design

Ng组件库:ng-devui(欢送Star)

官网交换:增加DevUI小助手(devui-official)

DevUIHelper插件:DevUIHelper-LSP(欢送Star)

引言

有时用户心愿将咱们的报表页面分享到其余的渠道,比方邮件、PPT等,每次都须要本人截图,一是很麻烦,二是截进去的图大小不一。

有没有方法在页面提供一个下载报表页面的性能,用户只须要点击按钮,就主动将以后的报表页面以图片模式下载下来呢?

html2canvas库就能帮咱们做到,无需后盾反对,纯浏览器实现截图,即便页面有滚动条也是没问题的,截进去的图十分清晰。

这个库的保护工夫十分长,早在2013年9月8日它就公布了第一个版本,比Vue的第一个版本(2013年12月8日)还要早。

截止到明天2020年12月18日,html2canvas库在github曾经有22.3k star,在npm的周下载量也有506k,十分了不起!

上一次提交是在2020年8月9日,可见作者仍然在很激情地保护着这个库,而且用TypeScript重构过,不过这个库的作者十分激进,哪怕曾经继续一直地保护了7年,他在README里仍然提到这个库目前还在试验阶段,不倡议在生产环境应用。

事实上我很早就将这个库用在了生产环境,这篇文章就来剖析下这个神奇和了不起的JavaScript库,看看它是怎么实现浏览器端截图的。

1 如何应用

在介绍html2canvas的原理之前,先来看看怎么应用它,应用起来真的非常简单,简直是1分钟上手。

应用html2canvas只有以下3步:

  1. 装置
  2. 引入
  3. 调用

Step 1: 装置

npm i html2canvas

Step 2: 引入

轻易在一个古代框架的工程项目中引入html2canvas

import html2canvas from 'html2canvas';

Step 3: 截图并下载

html2canvas就是一个函数,在页面渲染实现之后间接调用即可。

视图渲染实现的事件:1. Angular的ngAfterViewInit办法2. React的componentDidMount办法3. Vue的mounted办法

能够只传一个参数,就是你要截图的DOM元素,该函数返回一个Promise对象,在它的then办法中能够获取到绘制好的canvas对象,通过调用canvas对象的toDataURL办法就能够将其转换成图片。

拿到图片的URL之后,咱们能够

  1. 将其放到<img>标签的src属性中,让其显示在网页中;
  2. 也能够将其放到<a>标签的href属性中,将该图片下载到本地磁盘中。

咱们抉择后者。

html2canvas(document.querySelector('.main')).then(canvas => {  const link = document.createElement('a'); // 创立一个超链接对象实例  const event = new MouseEvent('click'); // 创立一个鼠标事件的实例  link.download = 'Button.png'; // 设置要下载的图片的名称  link.href = canvas.toDataURL(); // 将图片的URL设置到超链接的href中  link.dispatchEvent(event); // 触发超链接的点击事件});

是不是非常简单?

参数

咱们再来大抵看一眼它的API,该函数的签名如下:

html2canvas(element: HTMLElement, options: object): Promise<HTMLCanvasElement>

options对象可选的值如下:

NameDefaultDescription
allowTaintfalse是否容许跨域图像净化画布
backgroundColor#ffffff画布背景色彩,如果在DOM中没有指定,设置“null”(通明)
canvasnull应用现有的“画布”元素,用来作为绘图的根底
foreignObjectRenderingfalse是否应用ForeignObject渲染(如果浏览器反对的话)
imageTimeout15000加载图像的超时工夫(毫秒),设置为“0”以禁用超时
ignoreElements(element) => false从出现中移除匹配元素
loggingtrue为调试目标启用日志记录
onclonenull回调函数,当文档被克隆以出现时调用,能够用来批改将要出现的内容,而不影响原始源文档。
proxynull用来加载跨域图片的代理URL,如果设置为空(默认),跨域图片将不会被加载
removeContainertrue是否革除html2canvas长期创立的克隆DOM元素
scalewindow.devicePixelRatio用于渲染的缩放比例,默认为浏览器设施像素比
useCORSfalse是否尝试应用CORS从服务器加载图像
widthElement widthcanvas的宽度
heightElement heightcanvas的高度
xElement x-offsetcanvas的x轴地位
yElement y-offsetcanvas的y轴地位
scrollXElement scrollX渲染元素时应用的x轴地位(例如,如果元素应用position: fixed)
scrollYElement scrollY渲染元素时应用的y轴地位(例如,如果元素应用position: fixed)
windowWidthWindow.innerWidth渲染元素时应用的窗口宽度,这可能会影响诸如媒体查问之类的事件
windowHeightWindow.innerHeight渲染元素时应用的窗口高度,这可能会影响诸如媒体查问之类的事件

疏忽元素

options有一个ignoreElements参数能够用来疏忽某些元素,从渲染过程中移除,除了设置该参数外,还有一种疏忽元素的办法,就是在须要疏忽的元素上减少data-html2canvas-ignore属性。

<div data-html2canvas-ignore>Ignore element</div>

2 基本原理

介绍完html2canvas的应用,咱们先来理解下它的基本原理,而后再剖析细节实现。

它的基本原理其实很简略,就是去读取曾经渲染好的DOM元素的构造和款式信息,而后基于这些信息去构建截图,出现在canvas画布中。

它无奈绕过浏览器的内容策略限度,如果要出现跨域图片,须要设置一个代理。

3 主流程 html2canvas办法

基本原理很简略,但源码外面其实货色很多,咱们一步一步来,先找到入口,而后缓缓调试,走一遍大抵的流程。

寻找入口文件

拉取到源码,有很多办法能够找到入口文件:

  • 办法一:最简略的办法是间接全局搜寻html2canvas,这种办法效率很低,而且要碰运气,不举荐
  • 办法二:在我的项目中引入这个库,调用它,跑起来,并在该办法后面打断点进行调试,个别能准确地找到入口文件,举荐
  • 办法三:察看下是否有webpack.config.js或者rollup.config.js的构建工具的配置文件,而后在配置文件中找到准确的入口文件(个别是entryinput之类的属性),举荐
  • 办法四:间接扫一眼目录构造,个别入口文件在src/core/packages之类的目录下,文件名是index或者main,或者是模块的名字,有教训的话能够用这个办法,找起来很快,强烈推荐

办法一:全局搜寻

最简略最容易想到的的办法,就是全局搜寻关键字html2canvas,因为咱们在不理解html2canvas的实现之前,咱们接触到的关键字就只有这一个。

然而全局搜寻运气不好的话,很可能搜进去很多后果,在外面找入口文件费时费力,比方:

42个文件285个后果,找起来很麻烦,不举荐。

办法二:打断点

在调用html2canvas的中央打一个断点。

而后在执行到断点处时,点击向下的小箭头,进入该办法。

因为在开发环境,很快咱们就能发现入口文件和入口办法在哪儿,这里显示的是html2canvas文件,实际上这个文件是构建之后的文件,然而这个文件的上下文给咱们提供了找入口办法的信息,这里咱们发现了renderElement办法。

这时咱们能够尝试全局搜寻这个办法,很侥幸间接找到了????

办法三:找配置文件

寻找配置文件个别也要靠教训,个别配置文件都会带.config后缀常见构建工具的配置文件:

构建工具配置文件
Webpackwebpack.config.js
Rolluprollup.config.js
Gulpglupfile.config.js
GruntGruntfile.js

配置文件找到,入口文件个别很容易就找到

办法四:

办法四个别也要靠教训,咱们扫一眼目录构造,其实很容易就能发现主入口src/index.ts

从主入口登程

咱们曾经找到了入口办法在src/index.ts文件中,先从主入口登程,把大抵的调用关系梳理进去,对全局有个根本的理解,而后再深刻细节。

入口办法简直啥也没做,间接返回了另一个办法renderElement的调用后果。

// 入口办法const html2canvas = (element: HTMLElement, options: Partial<Options> = {}): Promise<HTMLCanvasElement> => {    return renderElement(element, options);};

沿着调用关系往下,很快咱们就梳理出了如下繁难火焰图(带办法正文)

这张繁难的火焰图次要有两点须要留神:

  1. 繁难火焰图只是帮忙咱们对整个流程有个粗略的意识,这种意识既不粗疏也不全面,须要进一步剖析外面的要害办法
  2. renderStackContent这个渲染层叠内容的办法是整个html2canvas最外围的办法,咱们将在4 渲染层叠内容一章中独自剖析

将页面中指定的DOM元素渲染到离屏canvas中 renderElement

通过繁难火焰图,咱们曾经对html2canvas的主流程有了一个根本的意识,接下来咱们一层一层来剖析,先看renderElement办法。

这个办法的次要目标是将页面中指定的DOM元素渲染到一个离屏canvas中,并将渲染好的canvas返回给用户。

它次要做了以下事件:

  1. 解析用户传入的options,将其与默认的options合并,失去用于渲染的配置数据renderOptions
  2. 对传入的DOM元素进行解析,取到节点信息和款式信息,这些节点信息会和上一步的renderOptions配置一起传给canvasRenderer实例,用来绘制离屏canvas
  3. canvasRenderer将根据浏览器渲染层叠内容的规定,将用户传入的DOM元素渲染到一个离屏canvas中,这个离屏canvas咱们能够在then办法的回调中取到

renderElement办法的外围代码如下:

const renderElement = async (element: HTMLElement, opts: Partial<Options>): Promise<HTMLCanvasElement> => {    const renderOptions = {...defaultOptions, ...opts}; // 合并默认配置和用户配置    const renderer = new CanvasRenderer(renderOptions); // 依据渲染的配置数据生成canvasRenderer实例    const root = parseTree(element); // 解析用户传入的DOM元素(为了不影响原始的DOM,实际上会克隆一个新的DOM元素),获取节点信息    return await renderer.render(root); // canvasRenderer实例会依据解析到的节点信息,根据浏览器渲染层叠内容的规定,将DOM元素内容渲染到离屏canvas中};

合并配置的逻辑比较简单,咱们间接略过,重点剖析下解析节点信息(parseTree)和渲染离屏canvas(renderer.render)两个逻辑。

解析节点信息 parseTree

parseTree的入参就是一个一般的DOM元素,返回值是一个ElementContainer对象,该对象次要蕴含DOM元素的地位信息(bounds: width|height|left|top)、款式数据、文本节点数据等(只是节点树的相干信息,不蕴含层叠数据,层叠数据在parseStackingContexts办法中获得)。

解析的办法就是递归整个DOM树,并获得每一层节点的数据。

ElementContainer对象是一颗树状构造,大抵如下:

{  bounds: {height: 1303.6875, left: 8, top: -298.5625, width: 1273},  elements: [    {      bounds: {left: 8, top: -298.5625, width: 1273, height: 1303.6875},      elements: [        {          bounds: {left: 8, top: -298.5625, width: 1273, height: 1303.6875},          elements: [            {styles: CSSParsedDeclaration, textNodes: Array(1), elements: Array(0), bounds: Bounds, flags: 0},            {styles: CSSParsedDeclaration, textNodes: Array(1), elements: Array(0), bounds: Bounds, flags: 0},            {styles: CSSParsedDeclaration, textNodes: Array(1), elements: Array(0), bounds: Bounds, flags: 0},            {styles: CSSParsedDeclaration, textNodes: Array(3), elements: Array(2), bounds: Bounds, flags: 0},            ...          ],          flags: 0,          styles: {backgroundClip: Array(1), backgroundColor: 0, backgroundImage: Array(0), backgroundOrigin: Array(1), backgroundPosition: Array(1), …},          textNodes: []        }      ],      flags: 0,      styles: CSSParsedDeclaration {backgroundClip: Array(1), backgroundColor: 0, backgroundImage: Array(0), backgroundOrigin: Array(1), backgroundPosition: Array(1), …},      textNodes: []    }  ],  flags: 4,  styles: CSSParsedDeclaration {backgroundClip: Array(1), backgroundColor: 0, backgroundImage: Array(0), backgroundOrigin: Array(1), backgroundPosition: Array(1), …},  textNodes: []}

外面蕴含了每一层节点的:

  • bounds - 地位信息(宽/高、横/纵坐标)
  • elements - 子元素信息
  • flags - 用来决定如何渲染的标记
  • styles - 款式形容信息
  • textNodes - 文本节点信息

渲染离屏canvas renderer.render

有了节点树信息,就能够用来渲染离屏canvas了,咱们来看看渲染的逻辑。

渲染的逻辑在CanvasRenderer类的render办法中,该办法次要用来渲染层叠内容:

  1. 应用上一步解析到的节点数据,生成层叠数据
  2. 应用节点的层叠数据,根据浏览器渲染层叠数据的规定,将DOM元素一层一层渲染到离屏canvas中

render办法的外围代码如下:

async render(element: ElementContainer): Promise<HTMLCanvasElement> {  /**   * StackingContext {   *   element: ElementPaint {container: ElementContainer, effects: Array(0), curves: BoundCurves}   *   inlineLevel: []   *   negativeZIndex: []   *   nonInlineLevel: [ElementPaint]   *   nonPositionedFloats: []   *   nonPositionedInlineLevel: []   *   positiveZIndex: [StackingContext]   *   zeroOrAutoZIndexOrTransformedOrOpacity: [StackingContext]   * }   */  const stack = parseStackingContexts(element);  // 渲染层叠内容  await this.renderStack(stack);  return this.canvas;}

其中的

  • inlineLevel - 内联元素
  • negativeZIndex - zIndex为负的元素
  • nonInlineLevel - 非内联元素
  • nonPositionedFloats - 未定位的浮动元素
  • nonPositionedInlineLevel - 内联的非定位元素,蕴含内联表和内联块
  • positiveZIndex - z-index大于等于1的元素
  • zeroOrAutoZIndexOrTransformedOrOpacity - 所有有层叠上下文的(z-index: auto|0)、透明度小于1的(opacity小于1)或变换的(transform不为none)元素

代表的是层叠信息,渲染层叠内容时会依据这些层叠信息来决定渲染的程序,一层一层有序进行渲染。

parseStackingContexts解析层叠信息的形式和parseTree解析节点信息的形式相似,都是递归整棵树,收集树的每一层的信息,造成一颗蕴含层叠信息的层叠树。

而渲染层叠内容的renderStack形式实际上调用的是renderStackContent办法,该办法是整个渲染流程中最为要害的办法,下一章独自剖析。

4 渲染层叠内容 renderStackContent

将DOM元素一层一层得渲染到离屏canvas中,是html2canvas所做的最外围的事件,这件事由renderStackContent办法来实现。

因而有必要重点剖析这个办法的实现原理,这里波及到CSS布局相干的一些常识,我先做一个简略的介绍。

CSS层叠布局规定

默认状况下,CSS是流式布局的,元素与元素之间不会重叠。

流式布局的意思能够了解:在一个矩形的水面上,搁置很多矩形的浮块,浮块会沉没在水面上,且彼此之间顺次排列,不会重叠在一起

这是要绘制它们其实非常简单,一个个按程序绘制即可。

不过有些状况下,这种流式布局会被突破,比方应用了浮动(float)和定位(position)。

因而须要须要辨认出哪些脱离了失常文档流的元素,并记住它们的层叠信息,以便正确地渲染它们。

那些脱离失常文档流的元素会造成一个层叠上下文,能够将层叠上下文简略了解为一个个的薄层(相似Photoshop的图层),薄层中有很多DOM元素,这些薄层叠在一起,最终造成了咱们看到的多彩的页面。

这些不同类型的层的层叠程序规定如下:

这张图很重要,html2canvas渲染DOM元素的规定也是一样的,能够认为html2canvas就是对这张图形容的规定的一个实现。

具体的规定在w3官网文档中有形容,大家能够参考:
https://www.w3.org/TR/css-pos...

renderStackContent就是对CSS层叠布局规定的一个实现

有了这些基础知识,咱们剖析renderStackContent就高深莫测了,它的源码如下:

async renderStackContent(stack: StackingContext) {    // 1. 最底层是background/border    await this.renderNodeBackgroundAndBorders(stack.element);    // 2. 第二层是负z-index    for (const child of stack.negativeZIndex) {        await this.renderStack(child);    }    // 3. 第三层是block块状盒子    await this.renderNodeContent(stack.element);    for (const child of stack.nonInlineLevel) {        await this.renderNode(child);    }    // 4. 第四层是float浮动盒子    for (const child of stack.nonPositionedFloats) {        await this.renderStack(child);    }    // 5. 第五层是inline/inline-block程度盒子    for (const child of stack.nonPositionedInlineLevel) {        await this.renderStack(child);    }    for (const child of stack.inlineLevel) {        await this.renderNode(child);    }    // 6. 第六层是以下三种:    // (1) ‘z-index: auto’或‘z-index: 0’。    // (2) ‘transform: none’    // (3) opacity小于1    for (const child of stack.zeroOrAutoZIndexOrTransformedOrOpacity) {        await this.renderStack(child);    }    // 7. 第七层是正z-index    for (const child of stack.positiveZIndex) {        await this.renderStack(child);    }}

小结

本文次要介绍html2canvas实现浏览器截图的原理。

首先简略介绍html2canvas是做什么的,如何应用它;

而后从主入口登程,剖析html2canvas渲染DOM元素的大抵流程(繁难火焰图);

接着按火焰图的程序,顺次对renderElement办法中执行的parseTree/parseStackingContextrenderer.render三个办法进行剖析,理解这些办法的作用和原理;

最初通过介绍CSS布局规定和7阶层叠程度,天然地引出renderStackContent要害办法实现原理的介绍。

退出咱们

咱们是DevUI团队,欢送来这里和咱们一起打造优雅高效的人机设计/研发体系。招聘邮箱:muyang2@huawei.com。

文/DevUI Kagol

往期文章举荐

《在瀑布下用火焰烤饼:三步法助你疾速定位网站性能问题(超具体)》

《手把手教你应用Rollup打包????并公布本人的工具库????》

《手把手教你搭建一个灰度公布环境》