关于前端:html2canvas实现浏览器截图的原理包含源码分析的通用方法

44次阅读

共计 9498 个字符,预计需要花费 24 分钟才能阅读完成。

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 对象可选的值如下:

Name Default Description
allowTaint false 是否容许跨域图像净化画布
backgroundColor #ffffff 画布背景色彩,如果在 DOM 中没有指定,设置“null”(通明)
canvas null 应用现有的“画布”元素,用来作为绘图的根底
foreignObjectRendering false 是否应用 ForeignObject 渲染(如果浏览器反对的话)
imageTimeout 15000 加载图像的超时工夫(毫秒),设置为“0”以禁用超时
ignoreElements (element) => false 从出现中移除匹配元素
logging true 为调试目标启用日志记录
onclone null 回调函数,当文档被克隆以出现时调用,能够用来批改将要出现的内容,而不影响原始源文档。
proxy null 用来加载跨域图片的代理 URL,如果设置为空(默认),跨域图片将不会被加载
removeContainer true 是否革除 html2canvas 长期创立的克隆 DOM 元素
scale window.devicePixelRatio 用于渲染的缩放比例,默认为浏览器设施像素比
useCORS false 是否尝试应用 CORS 从服务器加载图像
width Element width canvas的宽度
height Element height canvas的高度
x Element x-offset canvas的 x 轴地位
y Element y-offset canvas的 y 轴地位
scrollX Element scrollX 渲染元素时应用的 x 轴地位(例如,如果元素应用position: fixed)
scrollY Element scrollY 渲染元素时应用的 y 轴地位(例如,如果元素应用position: fixed)
windowWidth Window.innerWidth 渲染元素时应用的窗口宽度,这可能会影响诸如媒体查问之类的事件
windowHeight Window.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 后缀常见构建工具的配置文件:

构建工具 配置文件
Webpack webpack.config.js
Rollup rollup.config.js
Gulp glupfile.config.js
Grunt Gruntfile.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 打包???? 并公布本人的工具库????》

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

正文完
 0