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打包????并公布本人的工具库????》
《手把手教你搭建一个灰度公布环境》