DevUI是一支兼具設計視角和工程視角的團隊,服務於華爲雲 DevCloud平臺和華爲內部數箇中後臺系統,服務於設計師和前端工程師。
官方網站: devui.design
Ng組件庫: ng-devui(歡迎Star)
官方交流:添加DevUI小助手(devui-official)
DevUIHelper插件:DevUIHelper-LSP(歡迎Star)
有時用戶但願將咱們的報表頁面分享到其餘的渠道,好比郵件、PPT等,每次都須要本身截圖,一是很麻煩,二是截出來的圖大小不一。css
有沒有辦法在頁面提供一個下載報表頁面的功能,用戶只須要點擊按鈕,就自動將當前的報表頁面以圖片形式下載下來呢?html
html2canvas庫就能幫咱們作到,無需後臺支持,純瀏覽器實現截圖,即便頁面有滾動條也是沒問題的,截出來的圖很是清晰。前端
這個庫的維護時間很是長,早在2013年9月8日
它就發佈了第一個版本,比Vue的第一個版本(2013年12月8日)還要早。webpack
截止到今天2020年12月18日,html2canvas庫在github已經有22.3k
star,在npm的周下載量也有506k
,很是了不得!git
上一次提交是在2020年8月9日,可見做者依然在很熱情地維護着這個庫,並且用TypeScript
重構過,不過這個庫的做者很是保守,哪怕已經持續不斷地維護了7年
,他在README裏依然提到這個庫目前還在實驗階段,不建議在生產環境使用。github
事實上我很早就將這個庫用在了生產環境,這篇文章就來分析下這個神奇和了不得的JavaScript庫,看看它是怎麼實現瀏覽器端截圖的。web
在介紹html2canvas的原理以前,先來看看怎麼使用它,使用起來真的很是簡單,幾乎是1分鐘上手。npm
使用html2canvas只要如下3步:canvas
npm i html2canvas
隨便在一個現代框架的工程項目中引入html2canvassegmentfault
import html2canvas from 'html2canvas';
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>
介紹完html2canvas的使用,咱們先來了解下它的基本原理,而後再分析細節實現。
它的基本原理其實很簡單,就是去讀取已經渲染好的DOM元素的結構和樣式信息,而後基於這些信息去構建截圖,呈如今canvas畫布中。
它沒法繞過瀏覽器的內容策略限制,若是要呈現跨域圖片,須要設置一個代理。
基本原理很簡單,但源碼裏面其實東西不少,咱們一步一步來,先找到入口,而後慢慢調試,走一遍大體的流程。
拉取到源碼,有不少方法能夠找到入口文件:
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); };
沿着調用關係往下,很快咱們就梳理出了以下簡易火焰圖(帶方法註釋)
這張簡易的火焰圖主要有兩點須要注意:
4 渲染層疊內容
一章中單獨分析經過簡易火焰圖,咱們已經對html2canvas的主流程有了一個基本的認識,接下來咱們一層一層來分析,先看renderElement方法。
這個方法的主要目的是將頁面中指定的DOM元素渲染到一個離屏canvas中,並將渲染好的canvas返回給用戶。
它主要作了如下事情:
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的入參就是一個普通的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: [] }
裏面包含了每一層節點的:
有了節點樹信息,就能夠用來渲染離屏canvas了,咱們來看看渲染的邏輯。
渲染的邏輯在CanvasRenderer類的render方法中,該方法主要用來渲染層疊內容:
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方法,該方法是整個渲染流程中最爲關鍵的方法,下一章單獨分析。
將DOM元素一層一層得渲染到離屏canvas中,是html2canvas所作的最核心的事情,這件事由renderStackContent方法來實現。
所以有必要重點分析這個方法的實現原理,這裏涉及到CSS佈局相關的一些知識,我先作一個簡單的介紹。
默認狀況下,CSS是流式佈局的,元素與元素之間不會重疊。
流式佈局的意思能夠理解:在一個矩形的水面上,放置不少矩形的浮塊,浮塊會漂浮在水面上,且彼此之間依次排列,不會重疊在一塊兒
這是要繪製它們其實很是簡單,一個個按順序繪製便可。
不過有些狀況下,這種流式佈局會被打破,好比使用了浮動(float)和定位(position)。
所以須要須要識別出哪些脫離了正常文檔流的元素,並記住它們的層疊信息,以便正確地渲染它們。
那些脫離正常文檔流的元素會造成一個層疊上下文,能夠將層疊上下文簡單理解爲一個個的薄層(相似Photoshop的圖層),薄層中有不少DOM元素,這些薄層疊在一塊兒,最終造成了咱們看到的多彩的頁面。
這些不一樣類型的層的層疊順序規則以下:
這張圖很重要,html2canvas渲染DOM元素的規則也是同樣的,能夠認爲html2canvas就是對這張圖描述的規則的一個實現。
詳細的規則在w3官方文檔中有描述,你們能夠參考:
https://www.w3.org/TR/css-pos...
有了這些基礎知識,咱們分析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
往期文章推薦