Stories

Detail Return Return

如何複製由自定義元素組成的網頁的 HTML 代碼 - Stories Detail

  • React Hook 深入淺出
  • CSS技巧與案例詳解
  • vue2與vue3技巧合集
  • VueUse源碼解讀

image.png

有時我們需要獲取某個網頁HTML的本地副本,例如作為測試的輸入。

但複製網頁或元素的HTML並不總是直截了當的。現代網站往往由自定義元素構建。自定義元素通常是影子宿主。影子宿主的 innerHTMLouterHTML 屬性只返回直接子元素的HTML,而忽略了包含的影子DOM的HTML。

同樣,開發者工具中Elements面板的"複製outerHTML"操作目前還無法生成帶有聲明式影子根的HTML。例如,面向開發者的流行網站https://chromestatus.com/是由嵌套的自定義元素構建的。body 的第一個子元素chromedash-app託管了一個包含多個嵌套元素的影子DOM:

image.png

嘗試複製 https://chromestatus.com/feature/5084403030818816頁面body的HTML:

image.png

將複製的HTML粘貼到文本編輯器後,我看到chromedash-app是空的,它的影子DOM沒有被複制:

image.png

所以開發者工具還不支持複製影子DOM。

如何複製包含影子根的頁面DOM的HTML

可以使用一個小型輔助腳本,利用新的getHTML()方法來實現。該方法需要引用元素中嵌套的所有影子根才能正常工作。

為了獲取影子根,我使用了基於el.shadowRoot的函數childRoots(root)。但是,如果DOM包含封閉的影子根,只能通過Chrome擴展API提供的方法來獲取它們。

用於獲取元素的inner或outer HTML的函數很簡單。outerHTML()將僅定義父元素的HTML添加到innerHTML()的結果中:

// html.js

import { childRoots } from "./dom.js";

export function innerHTML(parent) {
    return parent.getHTML({ shadowRoots: childRoots(parent) });
}

export function outerHTML(parent) {
    return parent.cloneNode(false).outerHTML.replace('><', `>${innerHTML(parent)}<`);
}

要查看函數的結果,從一個額外的main.js中調用它們:

// main.js

import { outerHTML } from "./html.js";
 
console.log(outerHTML(document.body)); 

JavaScript模塊可以方便地在開發者工具控制枱中執行。main.js生成一個完整的HTML,其中自定義元素內包含聲明式影子根:

image.png

生成的HTML長度為139 kB。如何知道這是一個精確的副本,它是否被準確複製了?有兩個簡單的選項。

可以保存HTML,在瀏覽器中打開,並目視比較源頁面的內容與副本。不過,樣式可能會不同。樣式可能直接在HTML中指定,或在自定義元素的構造函數中指定。如果腳本被包含在本地副本中,那些沒有失敗的腳本可能會修改頁面內容。要重現原始樣式,需要一些編碼工作。

複製的body的HTML(不包括head中聲明的腳本和樣式)很好地反映了原始頁面的文本內容:

image.png

如果將生成的HTML轉換為DOM,並與源DOM逐節點比較,結論會更有説服力。

如何測試兩個帶有影子根和插槽的DOM是否是克隆

innerHTML不能正確解析帶有新的shadowrootmode屬性的template元素。但它的現代替代品setHTMLUnsafe()允許將帶有聲明式影子根的HTML插入到元素中。

main2.js使用setHTMLUnsafe()創建源body元素的副本,然後調用函數compare(source, copy)比較源DOM和副本DOM中的所有元素:

// main2.js

import { innerHTML } from "./html.js";
import { compare } from "./compare.js";

let source = document.body;
const copy = document.createElement('body');
copy.setHTMLUnsafe(innerHTML(source));
compare(source, copy); 

compare(source, copy)將結果打印到控制枱:

image.png

compare(source, copy)獲取源和副本DOM的所有元素,並通過將它們連接成一個字符串來比較它們的順序和文本內容。沒有差異,所以不需要更詳細的比較:

// compare.js

import { allElements } from "./distributed.js";

function joinNames(els) {
    return els.map(el => el.localName).join('');
}

function joinText(els) {
    return els.map(el => el.textContent).join('');
}
 
export function compare(source, copy) {
    const els1 = allElements(source);
    const els2 = allElements(copy);
    console.log(copy, source);
    console.log('元素數量', els1.length, els2.length);
    console.log('元素順序相同', joinNames(els1) === joinNames(els2));
    console.log('元素文本相同', joinText(els1) === joinText(els2));
 }

allElements(el)從allChildNodes(parent)返回的所有類型的節點中選擇元素,它複製了瀏覽器在DOM渲染期間的行為 — 它返回分配的元素、影子根的子元素和普通子節點。忽略影子根的兄弟節點或具有分配元素的插槽的子節點:

// distributed.js

export function allChildNodes(parent) {
    let children = [];

    if (parent.assignedNodes && parent.assignedNodes().length)
        children.push(...parent.assignedNodes());
    else {
        if (parent.shadowRoot) {
            parent = parent.shadowRoot;
        }

        children.push(...parent.childNodes);
    }
 
    return [...children, ...children.flatMap(allChildNodes)];
}

export function allElements(parent) {
    return allChildNodes(parent).filter(n => Node.ELEMENT_NODE === n.nodeType);
}

只比較了文本節點。似乎在通過getHTML()將DOM轉換為HTML或通過setHTMLUnsafe()將HTML轉換為新DOM的過程中,多個換行和空格被刪除,新的被添加。因此,源元素和副本元素中的文本節點數量不同。但如果去掉它們內容中的換行和空格,它們是相同的。

完整的示例代碼可從https://github.com/marianc000/compareShadowDOM下載。

本文 GitHub https://github.com/qq449245884/xiaozhi 已收錄,有一線大廠面試完整考點、資料以及我的系列文章。
user avatar toopoo Avatar alibabawenyujishu Avatar yinzhixiaxue Avatar jingdongkeji Avatar qingzhan Avatar kobe_fans_zxc Avatar dirackeeko Avatar anchen_5c17815319fb5 Avatar zero_dev Avatar jiavan Avatar woniuseo Avatar yqyx36 Avatar
Favorites 96 users favorite the story!
Favorites

Add a new Comments

Some HTML is okay.