共计 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 步:
- 装置
- 引入
- 调用
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 之后,咱们能够
- 将其放到
<img>
标签的 src 属性中,让其显示在网页中; - 也能够将其放到
<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
的构建工具的配置文件,而后在配置文件中找到准确的入口文件(个别是entry
或input
之类的属性),举荐 - 办法四:间接扫一眼目录构造,个别入口文件在
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);
};
沿着调用关系往下,很快咱们就梳理出了如下繁难火焰图(带办法正文)
这张繁难的火焰图次要有两点须要留神:
- 繁难火焰图只是帮忙咱们对整个流程有个粗略的意识,这种意识既不粗疏也不全面,须要进一步剖析外面的要害办法
- renderStackContent 这个渲染层叠内容的办法是整个 html2canvas 最外围的办法,咱们将在
4 渲染层叠内容
一章中独自剖析
将页面中指定的 DOM 元素渲染到离屏 canvas 中 renderElement
通过繁难火焰图,咱们曾经对 html2canvas 的主流程有了一个根本的意识,接下来咱们一层一层来剖析,先看 renderElement 办法。
这个办法的次要目标是将页面中指定的 DOM 元素渲染到一个离屏 canvas 中,并将渲染好的 canvas 返回给用户。
它次要做了以下事件:
- 解析用户传入的 options,将其与默认的 options 合并,失去用于渲染的配置数据 renderOptions
- 对传入的 DOM 元素进行解析,取到节点信息和款式信息,这些节点信息会和上一步的 renderOptions 配置一起传给 canvasRenderer 实例,用来绘制离屏 canvas
- 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 办法中,该办法次要用来渲染层叠内容:
- 应用上一步解析到的节点数据,生成层叠数据
- 应用节点的层叠数据,根据浏览器渲染层叠数据的规定,将 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 打包???? 并公布本人的工具库????》
《手把手教你搭建一个灰度公布环境》