這是專門探索 JavaScript 及其所構建的組件的系列文章的第 17 篇。html
想閱讀更多優質文章請猛戳GitHub博客,一年百來篇優質文章等着你!前端
若是你錯過了前面的章節,能夠在這裏找到它們:node
Web Components 是一套不一樣的技術,容許你建立可重用的定製元素,它們的功能封裝在你的代碼以外,你能夠在 Web 應用中使用它們。git
Web組件由四部分組成:github
在本文中主要講解 Shadow DOM(影子DOM)web
Shadow DOM 這款工具旨在構建基於組件的應用。所以,可爲網絡開發中的常見問題提供解決方案:編程
本文假設你已經熟悉 DOM 及其它的 Api 的概念。若是不熟悉,能夠在這裏閱讀關於它的詳細文章—— https://developer.mozilla.org...。segmentfault
陰影 DOM 只是一個普通的 DOM,除了兩個區別:數組
一般,你建立 DOM 節點並將其附加至其餘元素做爲子項。 藉助於 shadow DOM,您能夠建立做用域 DOM 樹,該 DOM 樹附加至該元素上,但與其自身真正的子項分離開來。這一做用域子樹稱爲影子樹。被附着的元素稱爲影子宿主。 您在影子中添加的任何項均將成爲宿主元素的本地項,包括 <style>。 這就是 shadow DOM 實現 CSS 樣式做用域的方式瀏覽器
一般,建立 DOM 節點並將它們做爲子元素追加到另外一個元素中。藉助於 shadow DOM,建立一個做用域 DOM 樹,附該 DOM 樹附加到元素上,但它與實際的子元素是分離的。這個做用域的子樹稱爲 影子樹,被附着的元素稱爲影子宿主。向影子樹添加的任何內容都將成爲宿主元素的本地元素,包括 <style>
,這就是 影子DOM 實現 CSS 樣式做用域的方式。
影子根是附加到「宿主」元素的文檔片斷,元素經過附加影子根來獲取其 shadow DOM。要爲元素建立陰影 DOM,調用 element.attachShadow()
:
var header = document.createElement('header'); var shadowRoot = header.attachShadow({mode: 'open'}); var paragraphElement = document.createElement('p'); paragraphElement.innerText = 'Shadow DOM'; shadowRoot.appendChild(paragraphElement);
規範定義了元素列表,這些元素沒法託管影子樹,元素之因此在所選之列,其緣由以下:
<textarea>
、<input>
)。例如,如下方法行不通:
document.createElement('input').attachShadow({mode: 'open'}); // Error. `<input>` cannot host shadow dom.
這是組件用戶寫入的標記。該 DOM 不在組件 shadow DOM 以內,它是元素的實際孩子。假設已經建立了一個名爲<extended-button>
的定製組件,它擴展了原生 HTML 按鈕組件,此時但願在其中添加圖像和一些文本。代碼以下:
<extended-button> <!-- the image and span are extended-button's light DOM --> <img src="boot.png" slot="image"> <span>Launch</span> </extended-button>
「extension -button」 是定義的定製組件,其中的 HTML 稱爲 Light DOM,該組件由用戶本身添加。
這裏的 Shadow DOM 是你建立的組件 extension-button
。Shadow DOM是 組件的本地組件,它定義了組件的內部結構、做用域 CSS 和 封裝實現細節。
瀏覽器將用戶建立的 Light DOM 分發到 Shadow DOM,並對最終產品進行渲染。扁平樹是最終在 DevTools 中看到的以及頁面上呈渲染的對象。
<extended-button> #shadow-root <style>…</style> <slot name="image"> <img src="boot.png" slot="image"> </slot> <span id="container"> <slot> <span>Launch</span> </slot> </span> </extended-button>
若是須要 Web 頁面上重複使用相同的標籤結構時,最好使用某種類型的模板,而不是一遍又一遍地重複相同的結構。這在之前也是能夠實現,可是 HTML <template> 元素(在現代瀏覽器中獲得了很好的支持)使它變得容易得多。此元素及其內容不在 DOM 中渲染,但可使用 JavaScript 引用它。
一個簡單的例子:
<template id="my-paragraph"> <p> Paragraph content. </p> </template>
這不會出如今頁面中,直到使用 JavaScrip t引用它,而後使用以下方式將其追加到 DOM 中:
var template = document.getElementById('my-paragraph'); var templateContent = template.content; document.body.appendChild(templateContent);
到目前爲止,已經有其餘技術能夠實現相似的行爲,可是,正如前面提到的,將其原生封裝起來是很是好的,Templates 也有至關不錯的瀏覽器支持:
模板自己是有用的,但它們與自定義元素配合會更好。 能夠 customElement
Api 能定義一個自定義元素,而且告知 HTML 解析器如何正確地構造一個元素,以及在該元素的屬性變化時執行相應的處理。
讓咱們定義一個 Web 組件名爲 <my-paragraph>
,該組件使用以前模板做爲它的 Shadow DOM 的內容:
customElements.define('my-paragraph', class extends HTMLElement { constructor() { super(); let template = document.getElementById('my-paragraph'); let templateContent = template.content; const shadowRoot = this.attachShadow({mode: 'open'}).appendChild(templateContent.cloneNode(true)); } });
這裏須要注意的關鍵點是,咱們向影子根添加了模板內容的克隆,影子根是使用 Node.cloneNode() 方法建立的。
由於將其內容追加到一個 Shadow DOM 中,因此能夠在模板中使用 <style> 元素的形式包含一些樣式信息,而後將其封裝在自定義元素中。若是隻是將其追加到標準 DOM 中,它是沒法工做。
例如,能夠將模板更改成:
<template id="my-paragraph"> <style> p { color: white; background-color: #666; padding: 5px; } </style> <p>Paragraph content. </p> </template>
如今自定義組件能夠這樣使用:
<my-paragraph></my-paragraph>
模板有一些缺點,主要是靜態內容,它不容許咱們渲染變量/數據,好可讓咱們按照通常使用的標準 HTML 模板的習慣來編寫代碼。Slot 是組件內部的佔位符,用戶可使用本身的標記來填充。讓咱們看看上面的模板怎麼使用 slot
:
<template id="my-paragraph"> <p> <slot name="my-text">Default text</slot> </p> </template>
若是在標記中包含元素時沒有定義插槽的內容,或者瀏覽器不支持插槽,<my-paragraph>
就只展現文本 「Default text」。
爲了定義插槽的內容,應該在 <my-paragraph>
元素中包含一個 HTML 結構,其中的 slot 屬性的值爲咱們定義插槽的名稱:
<my-paragraph> <span slot="my-text">Let's have some different text!</span> </my-paragraph>
能夠插入插槽的元素稱爲 Slotable; 當一個元素插入一個插槽時,它被稱爲開槽 (slotted)。
注意,在上面的例子中,插入了一個 <span>
元素,它是一個開槽元素,它有一個屬性 slot
,它等於 my-text
,與模板中的 slot
定義中的 name
屬性的值相同。
在瀏覽器中渲染後,上面的代碼將構建如下扁平 DOM 樹:
<my-paragraph> #shadow-root <p> <slot name="my-text"> <span slot="my-text">Let's have some different text!</span> </slot> </p> </my-paragraph>
使用 shadow DOM 的組件可經過主頁來設定樣式,定義其本身的樣式或提供鉤子(以 CSS 自定義屬性的形式)讓用戶替換默認值。
做用域 CSS 是 Shadow DOM 最大的特性之一:
shadow DOM 內部使用的 CSS 選擇器在本地應用於組件實際上,這意味着咱們能夠再次使用公共vid/類名,而不用擔憂頁面上其餘地方的衝突,最佳作法是在 Shadow DOM 內使用更簡單的 CSS 選擇器,它們在性能上也不錯。
看看在 #shadow-root 定義了一些樣式的:
#shadow-root <style> #container { background: white; } #container-items { display: inline-flex; } </style> <div id="container"></div> <div id="container-items"></div>
上面例子中的全部樣式都是#shadow-root的本地樣式。使用<link>元素在#shadow-root中引入樣式表,這些樣式表也都屬於本地的。
使用 :host
僞類選擇器,用來選擇組件宿主元素中的元素 (相對於組件模板內部的元素)。
<style> :host { display: block; /* by default, custom elements are display: inline */ } </style>
當涉及到 :host
選擇器時,應該當心一件事:父頁面中的規則具備比元素中定義的 :host
規則具備更高的優先級,這容許用戶從外部覆蓋頂級樣式。並且 :host
只在影子根目錄下工做,因此你不能在Shadow DOM 以外使用它。
若是 :host(<selector>)
的函數形式與 <selector>
匹配,你能夠指定宿主,對於你的組件而言,這是一個很好的方法,它可以讓你基於宿主將對用戶互動或狀態的反應行爲進行封裝,或對內部節點進行樣式設定:
<style> :host { opacity: 0.4; } :host(:hover) { opacity: 1; } :host([disabled]) { /* style when host has disabled attribute. */ background: grey; pointer-events: none; opacity: 0.4; } :host(.pink) > #tabs { color: pink; /* color internal #tabs node when host has class="pink". */ } </style>
:host-context(<selector>)
或其任意父級與 <selector> 匹配,它將與組件匹配。 例如,在文檔的元素上可能有一個用於表示樣式主題 (theme) 的 CSS 類,而咱們應當基於它來決定組件的樣式。
好比,不少人都經過將類應用到 <html> 或 <body> 進行主題化:
<body class="lightheme"> <custom-container> … </custom-container> </body>
在下面的例子中,只有當某個祖先元素有 CSS 類theme-light時,咱們纔會把background-color樣式應用到組件內部的全部元素中:
:host-context(.theme-light) h2 { background-color: #eef; }
組件樣式一般只會做用於組件自身的 HTML 上,咱們可使用 /deep/
選擇器,來強制一個樣式對各級子組件的視圖也生效,它不但做用於組件的子視圖,也會做用於組件的內容。
在下面例子中,咱們以全部的元素爲目標,從宿主元素到當前元素再到 DOM 中的全部子元素:
:host /deep/ h3 { font-style: italic; }
/deep/
選擇器還有一個別名 >>>
,能夠任意交替使用它們。
/deep/
和>>>
選擇器只能被用在 仿真 (emulated)模式下。 這種方式是默認值,也是用得最多的方式。
有幾種方法可從外部爲組件設定樣式:最簡單的方法是使用標記名稱做爲選擇器,以下
custom-container { color: red; }
外部樣式比在 Shadow DOM 中定義的樣式具備更高的優先級。
例如,若是用戶編寫選擇器:
custom-container { width: 500px; }
它將覆蓋組件的樣式:
:host { width: 300px; }
對組件自己進行樣式化只能到此爲止。可是若是人想要對組件的內部進行樣式化,會發生什麼狀況呢?爲此,咱們須要 CSS 自定義屬性。
若是組件的開發者經過 CSS 自定義屬性提供樣式鉤子,則用戶可調整內部樣式。其思想相似於<slot>
,但適用於樣式。
看看下面的例子:
<!-- main page --> <style> custom-container { margin-bottom: 60px; - custom-container-bg: black; } </style> <custom-container background>…</custom-container>
在其 shadow DOM 內部:
:host([background]) { background: var( - custom-container-bg, #CECECE); border-radius: 10px; padding: 10px; }
在本例中,該組件將使用 black 做爲背景值,由於用戶指定了該值,不然,背景顏色將採用默認值 #CECECE
。
做爲組件的做者,是有責任讓開發人員瞭解他們可使用的 CSS 定製屬性,將其視爲組件的公共接口的一部分。
Shadow DOM API 提供了使用 slot 和分佈式節點的實用程序,這些實用程序在編寫自定義元素時早晚派得上用場。
當 slot
的分佈式節點發生變化時,slotchange
事件將觸發。例如,若是用戶從 light DOM 中添加/刪除子元素。
var slot = this.shadowRoot.querySelector('#some_slot'); slot.addEventListener('slotchange', function(e) { console.log('Light DOM change'); });
要監視對 light DOM 的其餘類型的更改,能夠在元素的構造函數中使用 MutationObserver
。之前討論過 MutationObserver 的內部結構以及如何使用它。
有時候,瞭解哪些元素與 slot 相關聯很是有用。調用 slot.assignedNodes()
可查看 slot 正在渲染哪些元素。 {flatten: true}
選項將返回 slot 的備用內容(前提是沒有分佈任何節點)。
讓咱們看看下面的例子:
<slot name=’slot1’><p>Default content</p></slot>
假設這是在一個名爲 <my-container>
的組件中。
看看這個組件的不一樣用法,以及調用 assignedNodes()
的結果是什麼:
在第一種狀況下,咱們將向 slot
中添加咱們本身的內容:
<my-container> <span slot="slot1"> container text </span> </my-container>
調用 assignedNodes()
會獲得 [<span slot= " slot1 " > container text </span>]
,注意,結果是一個節點數組。
在第二種狀況下,將內容置空:
<my-container> </my-container>
調用 assignedNodes()
的結果將返回一個空數組 []
。
在第三種狀況下,調用 slot.assignedNodes({flatten: true})
,獲得結果是: [<p>默認內容</p>]
。
此外,要訪問 slot
中的元素,能夠調用 assignedNodes()
來查看元素分配給哪一個組件 slot
。
值得注意的是,當發生在 Shadow DOM 中的事件冒泡時,會發生什麼。
當事件從 Shadow DOM 中觸發時,其目標將會調整爲維持 Shadow DOM 提供的封裝。也就是說,事件的目標從新進行了設定,所以這些事件看起來像是來自組件,而不是來自 Shadow DOM 中的內部元素。
下面是從 Shadow DOM 傳播出去的事件列表(有些沒有):
默認狀況下,自定義事件不會傳播到 Shadow DOM 以外。若是但願分派自定義事件並使其傳播,則須要添加 bubbles: true
和 composed: true
選項。
讓咱們看看派發這樣的事件是什麼樣的:
var container = this.shadowRoot.querySelector('#container'); container.dispatchEvent(new Event('containerchanged', {bubbles: true, composed: true}));
如但願得到 shadow DOM 檢測功能,請查看是否存在 attachShadow:
const supportsShadowDOMV1 = !!HTMLElement.prototype.attachShadow;
有史以來第一次,咱們擁有了實施適當 CSS 做用域、DOM 做用域的 API 原語,而且有真正意義上的組合。 與自定義元素等其餘網絡組件 API 組合後,shadow DOM 提供了一種編寫真正封裝組件的方法,無需花多大的功夫或使用如 <iframe> 等陳舊的東西。
代碼部署後可能存在的BUG無法實時知道,過後爲了解決這些BUG,花了大量的時間進行log 調試,這邊順便給你們推薦一個好用的BUG監控工具 Fundebug。
你的點贊是我持續分享好東西的動力,歡迎點贊!