摘要: 深刻JS系列17。javascript
Fundebug經受權轉載,版權歸原做者全部。css
這是專門探索 JavaScript 及其所構建的組件的系列文章的第 17 篇。html
若是你錯過了前面的章節,能夠在這裏找到它們:前端
Web Components 是一套不一樣的技術,容許你建立可重用的定製元素,它們的功能封裝在你的代碼以外,你能夠在 Web 應用中使用它們。java
Web組件由四部分組成:node
在本文中主要講解 Shadow DOM(影子DOM)git
Shadow DOM 這款工具旨在構建基於組件的應用。所以,可爲網絡開發中的常見問題提供解決方案:github
本文假設你已經熟悉 DOM 及其它的 Api 的概念。若是不熟悉,能夠在這裏閱讀關於它的詳細文章—— developer.mozilla.org...。web
陰影 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>
)。<img>
)。例如,如下方法行不通:
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 中,因此能夠在模板中使用 元素的形式包含一些樣式信息,而後將其封裝在自定義元素中。若是隻是將其追加到標準 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 自定義屬性的形式)讓用戶替換默認值。
代碼部署後可能存在的BUG無法實時知道,過後爲了解決這些BUG,花了大量的時間進行log 調試,這邊順便給你們推薦一個好用的BUG監控工具 Fundebug。
做用域 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的本地樣式。使用元素在#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>)
或其任意父級與 匹配,它將與組件匹配。 例如,在文檔的元素上可能有一個用於表示樣式主題 (theme) 的 CSS 類,而咱們應當基於它來決定組件的樣式。 好比,不少人都經過將類應用到 或 進行主題化:
<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>
等陳舊的東西。
Fundebug專一於JavaScript、微信小程序、微信小遊戲、支付寶小程序、React Native、Node.js和Java線上應用實時BUG監控。 自從2016年雙十一正式上線,Fundebug累計處理了9億+錯誤事件,付費客戶有Google、360、金山軟件、百姓網等衆多品牌企業。歡迎你們免費試用!