html2canvas實現瀏覽器截圖的原理(包含源碼分析的通用方法)


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屬性中,將該圖片下載到本地磁盤中。

我們選擇后者。

1 html2canvas(document.querySelector('.main')).then(canvas => {
2   const link = document.createElement('a'); // 創建一個超鏈接對象實例
3   const event = new MouseEvent('click'); // 創建一個鼠標事件的實例
4   link.download = 'Button.png'; // 設置要下載的圖片的名稱
5   link.href = canvas.toDataURL(); // 將圖片的URL設置到超鏈接的href中
6   link.dispatchEvent(event); // 觸發超鏈接的點擊事件
7 });

 

是不是非常簡單?

參數

我們再來大致看一眼它的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對象是一顆樹狀結構,大致如下:

 1 {
 2   bounds: {height: 1303.6875, left: 8, top: -298.5625, width: 1273},
 3   elements: [
 4     {
 5       bounds: {left: 8, top: -298.5625, width: 1273, height: 1303.6875},
 6       elements: [
 7         {
 8           bounds: {left: 8, top: -298.5625, width: 1273, height: 1303.6875},
 9           elements: [
10             {styles: CSSParsedDeclaration, textNodes: Array(1), elements: Array(0), bounds: Bounds, flags: 0},
11             {styles: CSSParsedDeclaration, textNodes: Array(1), elements: Array(0), bounds: Bounds, flags: 0},
12             {styles: CSSParsedDeclaration, textNodes: Array(1), elements: Array(0), bounds: Bounds, flags: 0},
13             {styles: CSSParsedDeclaration, textNodes: Array(3), elements: Array(2), bounds: Bounds, flags: 0},
14             ...
15           ],
16           flags: 0,
17           styles: {backgroundClip: Array(1), backgroundColor: 0, backgroundImage: Array(0), backgroundOrigin: Array(1), backgroundPosition: Array(1), …},
18           textNodes: []
19         }
20       ],
21       flags: 0,
22       styles: CSSParsedDeclaration {backgroundClip: Array(1), backgroundColor: 0, backgroundImage: Array(0), backgroundOrigin: Array(1), backgroundPosition: Array(1), …},
23       textNodes: []
24     }
25   ],
26   flags: 4,
27   styles: CSSParsedDeclaration {backgroundClip: Array(1), backgroundColor: 0, backgroundImage: Array(0), backgroundOrigin: Array(1), backgroundPosition: Array(1), …},
28   textNodes: []
29 }

 

里面包含了每一層節點的:

  • bounds - 位置信息(寬/高、橫/縱坐標)
  • elements - 子元素信息
  • flags - 用來決定如何渲染的標志
  • styles - 樣式描述信息
  • textNodes - 文本節點信息

渲染離屏canvas renderer.render

有了節點樹信息,就可以用來渲染離屏canvas了,我們來看看渲染的邏輯。

渲染的邏輯在CanvasRenderer類的render方法中,該方法主要用來渲染層疊內容:

  1. 使用上一步解析到的節點數據,生成層疊數據
  2. 使用節點的層疊數據,依據瀏覽器渲染層疊數據的規則,將DOM元素一層一層渲染到離屏canvas中

render方法的核心代碼如下:

 1 async render(element: ElementContainer): Promise<HTMLCanvasElement> {
 2   /**
 3    * StackingContext {
 4    *   element: ElementPaint {container: ElementContainer, effects: Array(0), curves: BoundCurves}
 5    *   inlineLevel: []
 6    *   negativeZIndex: []
 7    *   nonInlineLevel: [ElementPaint]
 8    *   nonPositionedFloats: []
 9    *   nonPositionedInlineLevel: []
10    *   positiveZIndex: [StackingContext]
11    *   zeroOrAutoZIndexOrTransformedOrOpacity: [StackingContext]
12    * }
13    */
14   const stack = parseStackingContexts(element);
15 
16   // 渲染層疊內容
17   await this.renderStack(stack);
18   return this.canvas;
19 }

 

其中的

  • 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-position-3/#painting-order

renderStackContent就是對CSS層疊布局規則的一個實現

有了這些基礎知識,我們分析renderStackContent就一目了然了,它的源碼如下:

 1 async renderStackContent(stack: StackingContext) {
 2     // 1. 最底層是background/border
 3     await this.renderNodeBackgroundAndBorders(stack.element);
 4 
 5     // 2. 第二層是負z-index
 6     for (const child of stack.negativeZIndex) {
 7         await this.renderStack(child);
 8     }
 9 
10     // 3. 第三層是block塊狀盒子
11     await this.renderNodeContent(stack.element);
12 
13     for (const child of stack.nonInlineLevel) {
14         await this.renderNode(child);
15     }
16 
17     // 4. 第四層是float浮動盒子
18     for (const child of stack.nonPositionedFloats) {
19         await this.renderStack(child);
20     }
21 
22     // 5. 第五層是inline/inline-block水平盒子
23     for (const child of stack.nonPositionedInlineLevel) {
24         await this.renderStack(child);
25     }
26     for (const child of stack.inlineLevel) {
27         await this.renderNode(child);
28     }
29 
30     // 6. 第六層是以下三種:
31     // (1) ‘z-index: auto’或‘z-index: 0’。
32     // (2) ‘transform: none’
33     // (3) opacity小於1
34     for (const child of stack.zeroOrAutoZIndexOrTransformedOrOpacity) {
35         await this.renderStack(child);
36     }
37 
38     // 7. 第七層是正z-index
39     for (const child of stack.positiveZIndex) {
40         await this.renderStack(child);
41     }
42 }

 

小結

本文主要介紹html2canvas實現瀏覽器截圖的原理。

首先簡單介紹html2canvas是做什么的,如何使用它;

然后從主入口出發,分析html2canvas渲染DOM元素的大致流程(簡易火焰圖);

接着按火焰圖的順序,依次對renderElement方法中執行的parseTree/parseStackingContextrenderer.render三個方法進行分析,了解這些方法的作用和原理;

最后通過介紹CSS布局規則和7階層疊水平,自然地引出renderStackContent關鍵方法實現原理的介紹。

加入我們

我們是DevUI團隊,歡迎來這里和我們一起打造優雅高效的人機設計/研發體系。招聘郵箱:muyang2@huawei.com。

文/DevUI Kagol

往期文章推薦

《在瀑布下用火焰烤餅:三步法助你快速定位網站性能問題(超詳細)》

《大廠是如何用DevCloud流水線實現自動化部署Web應用的? 》

《手把手教你使用Rollup打包📦並發布自己的工具庫🔧》


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM