<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          避坑指南!手把手帶你解讀html2canvas的實現(xiàn)原理

          共 10145字,需瀏覽 21分鐘

           ·

          2021-10-01 09:32


          導語 | html2canvas在前端通常用于合成海報、生成截圖等場景。本文從一次蒙層截圖失敗對html2canvas的實現(xiàn)原理展開詳細探討,帶你完美避坑!


          一、問題背景


          在一個前端項目中,有對當前頁面進行截屏并上傳的需求。安裝了html2canvas的npm包后,實現(xiàn)頁面截圖時,發(fā)現(xiàn)html2canvas將原本有透明度的蒙層截圖為了沒有透明度的蒙層,如下面兩張圖所示:




          顯然這并不能滿足前端截屏的需求,于是進行google,終于查到了相關問題。原來html2canvas渲染opacity失敗的問題自2015年起就已存在,雖然niklasvh在2020年12月修復了該問題,但是并沒有合并入npm包中。所以當使用html2canvas的npm包實現(xiàn)截圖時,仍然存在opacity渲染失敗的問題。


          為了徹底搞明白html2canvas渲染opacity失敗的問題,我們先對html2canvas的實現(xiàn)原理進行剖析。



          二、html2canvas原理剖析


          (一)流程圖


          如下圖所示,將html2canvas原理圖形化,主要分成出口供用戶使用的主要流程和兩部分核心邏輯:克隆并解析DOM節(jié)點、渲染DOM節(jié)點。




          (二)html2canvas方法


          html2canvas是出口方法,主要將用戶選擇的DOM節(jié)點和自定義配置項傳遞給renderElement方法。簡要邏輯代碼如下:


          const html2canvas = (element: HTMLElement, options: Partial<Options> = {}): Promise<HTMLCanvasElement> => {    return renderElement(element, options);};


          renderElement方法,主要把用戶自定義配置與默認配置進行合并,生成CanvasRenderer實例,克隆、解析并渲染用戶選擇的DOM節(jié)點。簡要邏輯代碼如下:


          const renderElement = async (element: HTMLElement, opts: Partial<Options>): Promise<HTMLCanvasElement> => {    const renderOptions = {...defaultOptions, ...opts}; // 合并默認配置與用戶自定義配置    const renderer = new CanvasRenderer(renderOptions); // 根據(jù)渲染配置數(shù)據(jù)生成CanvasRenderer實例    const documentCloner = new DocumentCloner(element, options);  // 生成DocumentCloner實例    const clonedElement = documentCloner.clonedReferenceElement; // createNewHtml層層遞歸查找用戶選擇的DOM元素,并克隆    const root = parseTree(clonedElement); // 解析克隆的DOM元素,獲取節(jié)點信息    const canvas = await renderer.render(root);  // CanvasRenderer實例將克隆的DOM元素內(nèi)容渲染到離屏canvas中    return canvas;};



          (三)克隆并解析DOM節(jié)點


          CanvasRenderer是canvas渲染類,后續(xù)使用的渲染方法均是該類的方法。在克隆并解析DOM節(jié)點部分,主要是將renderOptions傳給canvasRenderer實例,調(diào)用render方法來繪制canvas。


          DocumentCloner是DOM克隆類,主要是生成documentCloner實例,克隆用戶所選擇的DOM節(jié)點。其核心方法cloneNode通過遞歸整個DOM結構樹,匹配查詢用戶選擇的DOM節(jié)點并進行克隆,簡要邏輯代碼如下:


          cloneNode(node: Node): Node {    const window = node.ownerDocument.defaultView;    if (window && isElementNode(node) && (isHTMLElementNode(node) || isSVGElementNode(node))) {        const clone = this.createElementClone(node);        if (this.referenceElement === node && isHTMLElementNode(clone)) {            this.clonedReferenceElement = clone;        }        ...        for (let child = node.firstChild; child; child = child.nextSibling) {            if (!isElementNode(child) || (!isScriptElement(child) && !child.hasAttribute(IGNORE_ATTRIBUTE) && (typeof this.options.ignoreElements !== 'function' || !this.options.ignoreElements(child)))) {                if (!this.options.copyStyles || !isElementNode(child) || !isStyleElement(child)) {                    clone.appendChild(this.cloneNode(child));                }            }        } // 層層遞歸DOM樹,查找匹配并克隆用戶所選擇的DOM節(jié)點        ...        return clone;    }    return node.cloneNode(false);} // 輸出格式為DOM節(jié)點格式


          parseTree方法是解析克隆DOM節(jié)點,獲取節(jié)點的相關信息。parseTree層層遞歸克隆DOM節(jié)點,獲取DOM節(jié)點的位置、寬高、樣式等信息,簡要邏輯代碼如下:


          export const parseTree = (element: HTMLElement): ElementContainer => {    const container = createContainer(element);    container.flags |= FLAGS.CREATES_REAL_STACKING_CONTEXT;    parseNodeTree(element, container, container);    return container;};const parseNodeTree = (node: Node, parent: ElementContainer, root: ElementContainer) => {    for (let childNode = node.firstChild, nextNode; childNode; childNode = nextNode) {        nextNode = childNode.nextSibling;        if (isTextNode(childNode) && childNode.data.trim().length > 0) {            parent.textNodes.push(new TextContainer(childNode, parent.styles));        } else if (isElementNode(childNode)) {            const container = createContainer(childNode);            if (container.styles.isVisible()) {                ...                parent.elements.push(container);                if (!isTextareaElement(childNode) && !isSVGElement(childNode) && !isSelectElement(childNode)) {                    parseNodeTree(childNode, container, root);                }            }        }    }// 層層遞歸克隆DOM節(jié)點,解析獲取節(jié)點信息};


          parseTree輸出的格式如下:


          const ElementContainer = {    bounds: Bounds {left: 8, top: 8, width: 389, height: 313.34375},    elements: [        {            bounds: Bounds {left: 33, top: 33, width: 339, height: 263.34375}            elements: [],            flags: 0,            style: CSSParsedDeclaration {backgroundClip: Array(1), backgroundColor: 4289003775, …},            textNodes: [],        },        ...    ],    flags: 4,    style: styles: CSSParsedDeclaration {backgroundClip: Array(1), backgroundColor: 4278190335, …},    textNodes: [],}// bounds:位置、寬高// elements:子元素// flags:如何渲染的標志// style:樣式// textNodes:文本節(jié)點



          (四)層疊上下文


          在探討html2canvas渲染DOM節(jié)點的實現(xiàn)原理之前,先來闡明一下什么是層疊上下文。


          層疊上下文(stacking content),是HTML中的一種三維概念。如果一個節(jié)點含有層疊上下文,那么在下圖的Z軸中距離用戶更近。



          當一個節(jié)點滿足以下條件中的任意一個,則該節(jié)點含有層疊上下文。


          • 文檔根元素<html>

          • position為absolute或relative,且z-index不為auto

          • position為fixed或sticky

          • flex容器的子元素,且z-index不為auto

          • grid容器的子元素,且z-index不為auto

          • opacity小于1

          • mix-blend-mode不為normal

          • transform、filter、perspective、clip-path、mask/mask-imag/mask-border不為none

          • isolation為isolate

          • -webkit-overflow-scrolling為touch

          • will-change為任意屬性值

          • contain為layout、paint、strict、content


          著名的7階層疊水平對DOM節(jié)點進行分層,如下圖所示:



          通過以下html結構對7階層疊水平進行驗證時,發(fā)現(xiàn)層疊水平為:z-index為負的節(jié)點在background/border的下面,與7階層疊水平有所出入。


          <div style="width: 300px; height: 120px;background: #ccc; border: 20px solid #F56C6C">    <span style="color: #fff;margin-left: -20px;">內(nèi)聯(lián)元素內(nèi)聯(lián)元素內(nèi)聯(lián)元素內(nèi)聯(lián)元素內(nèi)聯(lián)元素</span>    <div style="width: 200px;height: 100px;background: #67C23A; margin-left: -20px; margin-top: -10px;"></div>    <div style="float: left; width: 150px; height: 100px; background: #409EFF; margin-top: -110px;"></div>    <div style="position: relative; background: #E6A23C; width: 100px; height: 100px; margin-top: -100px;"></div>    <div style="position: absolute; z-index: 1; background: yellow; width: 50px; height: 50px; top: 110px;"></div>    <div style="position: absolute; z-index: -1; background: #000; height: 200px; width: 100px; top: 90px"></div></div>



          但是,當父元素具有定位和z-index屬性時,z-index為負的節(jié)點在background/border上面,與7階層疊水平相印證。


          <div style="width: 300px; height: 120px; background: #ccc; border: 20px solid #F56C6C; position: relative; z-index: 0; ">  <span style="color: #fff; margin-left: -20px;">內(nèi)聯(lián)元素內(nèi)聯(lián)元素內(nèi)聯(lián)元素內(nèi)聯(lián)元素內(nèi)聯(lián)元素</span>  <div style="width: 200px; height: 100px; background: #67C23A; margin-left: -20px; margin-top: -10px;"></div>  <div style="float: left; width: 150px; height: 100px; background: #409EFF; margin-top: -110px;"></div>  <div style="position: relative; width: 100px; height: 100px; background: #E6A23C; margin-top: -100px;"></div>  <div style="position: absolute; width: 50px; height: 50px;  z-index: 1; background: yellow; top: -10px;"></div>  <div style="position: absolute; height: 200px; width: 100px; z-index: -1; background: #000; top: -30px"></div></div>




          (五)渲染DOM節(jié)點


          html2canvas是依據(jù)層疊上下文對DOM節(jié)點進行渲染。所以,在渲染DOM節(jié)點之前,需要先獲取DOM節(jié)點的層疊上下文。parseStackingContexts方法對克隆的DOM節(jié)點進行解析,獲取了克隆DOM節(jié)點的層疊上下文關系,其輸出的格式如下:


          const StackingContext = {    element: ElementPaint {container: ElementContainer, effects: Array(0), curves: BoundCurves},    inlineLevel: [],    negativeZIndex: [],    nonInlineLevel: [ElementPaint],    nonPositionedFloats: [],    nonPositionedInlineLevel: [],    positiveZIndex: [],    zeroOrAutoZIndexOrTransformedOrOpacity: [],};// element: parseTree輸出的ElementContainer、DOM節(jié)點邊界信息、特殊渲染效果// inlineLevel:內(nèi)聯(lián)元素// negativeZIndex:z-index為負的元素// nonInlineLevel:非內(nèi)聯(lián)元素// nonPositionedFloats:未定位的浮動元素// nonPositionedInlineLevel:未定位的內(nèi)聯(lián)元素// positiveZIndex:z-index為正的元素// zeroOrAutoZIndexOrTransformedOrOpacity:z-index: auto|0、opacity小于1,transform不為none的元素


          然后,renderStack方法調(diào)用renderStackContent方法遵循層疊上下文,自底層向上層層渲染DOM節(jié)點,簡要邏輯代碼如下:


          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. 第六層z-index: auto 或 z-index: 0, transform: none, 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); }}


          最后,在方法renderNodeBackgroundAndBorders和方法renderNodeContent內(nèi)部,調(diào)用了方法applyeffects的特殊效果進行渲染。而html2canvas的npm包中,缺少了透明度渲染效果的處理邏輯。這正是文章開頭出現(xiàn)的透明蒙層截圖失敗的根源所在。



          三、問題定位與解決


          通過對比niklasvh提交的版本記錄fix: opacity with overflow hidden #2450,發(fā)現(xiàn)新增了一個透明度渲染效果的處理邏輯,簡要代碼邏輯如下:

          export class OpacityEffect implements IElementEffect {    readonly type: EffectType = EffectType.OPACITY;    readonly target: number = EffectTarget.BACKGROUND_BORDERS | EffectTarget.CONTENT;    readonly opacity: number;
          constructor(opacity: number) { this.opacity = opacity; }}export const isOpacityEffect = (effect: IElementEffect): effect is OpacityEffect => effect.type === EffectType.OPACITY;


          在parseStackingContexts解析DOM節(jié)點層疊上下文,輸出StackingContext時,在element的ElementContainer中新增了記錄節(jié)點透明度的邏輯,簡要代碼邏輯如下:


          if (element.styles.opacity < 1) {    this.effects.push(new OpacityEffect(element.styles.opacity));}


          最后在applyEffects方法中,對DOM節(jié)點的透明度進行渲染,簡要代碼邏輯如下:


          if (isOpacityEffect(effect)) {    this.ctx.globalAlpha = effect.opacity;}


          至此,將上述邏輯融合進html2canvas的npm包后,可解決透明蒙層截圖失敗的問題。


          參考資料
          1.深入理解CSS中的層疊上下文和層疊順序

          2.css的層疊上下文

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



           作者簡介


          劉孟

          騰訊前端開發(fā)工程師

          劉孟,騰訊前端開發(fā)工程師,畢業(yè)于上海大學。目前負責騰訊優(yōu)聯(lián)項目的前端開發(fā)工作,有豐富的系統(tǒng)平臺及游戲營銷活動前端開發(fā)經(jīng)驗。



           推薦閱讀


          10分鐘了解Flutter跨平臺運行原理!

          如何在C++20中實現(xiàn)Coroutine及相關任務調(diào)度器?(實例教學)

          拒絕千篇一律,這套Go錯誤處理的完整解決方案值得一看!

          10個技巧!實現(xiàn)Vue.js極致性能優(yōu)化(建議收藏)






          瀏覽 392
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  人成视频在线观看一区二区 | 免费日屄| 亚洲骚货 | 精品国产美女 | 久久伊人大|