摘要: 深入JS系列17涎跨。
Fundebug經(jīng)授權(quán)轉(zhuǎn)載崭歧,版權(quán)歸原作者所有隅很。
這是專門探索 JavaScript 及其所構(gòu)建的組件的系列文章的第 17 篇。
如果你錯(cuò)過了前面的章節(jié)率碾,可以在這里找到它們:
- JavaScript 是如何工作的:引擎叔营,運(yùn)行時(shí)和調(diào)用堆棧的概述!
- JavaScript 是如何工作的:深入V8引擎&編寫優(yōu)化代碼的5個(gè)技巧所宰!
- JavaScript 是如何工作的:內(nèi)存管理+如何處理4個(gè)常見的內(nèi)存泄漏审编!
- JavaScript 是如何工作的:事件循環(huán)和異步編程的崛起+ 5種使用 async/await 更好地編碼方式!
- JavaScript 是如何工作的:深入探索 websocket 和HTTP/2與SSE +如何選擇正確的路徑歧匈!
- JavaScript 是如何工作的:與 WebAssembly比較 及其使用場景!
- JavaScript 是如何工作的:Web Workers的構(gòu)建塊+ 5個(gè)使用他們的場景!
- JavaScript 是如何工作的:Service Worker 的生命周期及使用場景!
- JavaScript 是如何工作的:Web 推送通知的機(jī)制!
- JavaScript是如何工作的:使用 MutationObserver 跟蹤 DOM 的變化!
- JavaScript是如何工作的:渲染引擎和優(yōu)化其性能的技巧!
- JavaScript是如何工作的:深入網(wǎng)絡(luò)層 + 如何優(yōu)化性能和安全!
- JavaScript是如何工作的:CSS 和 JS 動(dòng)畫底層原理及如何優(yōu)化它們的性能砰嘁!
- JavaScript的如何工作的:解析件炉、抽象語法樹(AST)+ 提升編譯速度5個(gè)技巧!
- JavaScript是如何工作的:深入類和繼承內(nèi)部原理+Babel和 TypeScript 之間轉(zhuǎn)換!
- JavaScript是如何工作的:存儲(chǔ)引擎+如何選擇合適的存儲(chǔ)API!
概述
Web Components 是一套不同的技術(shù),允許你創(chuàng)建可重用的定制元素矮湘,它們的功能封裝在你的代碼之外斟冕,你可以在 Web 應(yīng)用中使用它們。
Web組件由四部分組成:
- Shadow DOM(影子DOM)
- HTML templates(HTML模板)
- Custom elements(自定義元素)
- HTML Imports(HTML導(dǎo)入)
在本文中主要講解 Shadow DOM(影子DOM)
Shadow DOM 這款工具旨在構(gòu)建基于組件的應(yīng)用缅阳。因此磕蛇,可為網(wǎng)絡(luò)開發(fā)中的常見問題提供解決方案:
- 隔離 DOM:組件的 DOM 是獨(dú)立的(例如,document.querySelector() 不會(huì)返回組件 shadow DOM 中的節(jié)點(diǎn))十办。
- 作用域 CSS:shadow DOM 內(nèi)部定義的 CSS 在其作用域內(nèi)秀撇。樣式規(guī)則不會(huì)泄漏,頁面樣式也不會(huì)滲入向族。
- 組合:為組件設(shè)計(jì)一個(gè)聲明性呵燕、基于標(biāo)記的 API。
- 簡化 CSS - 作用域 DOM 意味著您可以使用簡單的 CSS 選擇器件相,更通用的 id/類名稱再扭,而無需擔(dān)心命名沖突。
Shadow DOM
本文假設(shè)你已經(jīng)熟悉 DOM 及其它的 Api 的概念夜矗。如果不熟悉泛范,可以在這里閱讀關(guān)于它的詳細(xì)文章—— https://developer.mozilla.org...。
陰影 DOM 只是一個(gè)普通的 DOM紊撕,除了兩個(gè)區(qū)別:
創(chuàng)建/使用的方式
-
與頁面其他部分有關(guān)的行為方式
通常罢荡,你創(chuàng)建 DOM 節(jié)點(diǎn)并將其附加至其他元素作為子項(xiàng)。 借助于 shadow DOM,您可以創(chuàng)建作用域 DOM 樹柠傍,該 DOM 樹附加至該元素上麸俘,但與其自身真正的子項(xiàng)分離開來。這一作用域子樹稱為影子樹惧笛。被附著的元素稱為影子宿主从媚。 您在影子中添加的任何項(xiàng)均將成為宿主元素的本地項(xiàng),包括
<style>
患整。 這就是 shadow DOM 實(shí)現(xiàn) CSS 樣式作用域的方式
通常拜效,創(chuàng)建 DOM 節(jié)點(diǎn)并將它們作為子元素追加到另一個(gè)元素中。借助于 shadow DOM各谚,創(chuàng)建一個(gè)作用域 DOM 樹紧憾,附該 DOM 樹附加到元素上,但它與實(shí)際的子元素是分離的昌渤。這個(gè)作用域的子樹稱為 影子樹赴穗,被附著的元素稱為影子宿主。向影子樹添加的任何內(nèi)容都將成為宿主元素的本地元素膀息,包括 <style>
般眉,這就是 影子DOM 實(shí)現(xiàn) CSS 樣式作用域的方式。
創(chuàng)建 shadow DOM
影子根是附加到“宿主”元素的文檔片段潜支,元素通過附加影子根來獲取其 shadow DOM甸赃。要為元素創(chuàng)建陰影 DOM,調(diào)用 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);
規(guī)范定義了元素列表冗酿,這些元素?zé)o法托管影子樹埠对,元素之所以在所選之列,其原因如下:
- 瀏覽器已為該元素托管其自身的內(nèi)部 shadow DOM(
<textarea>
裁替、<input>
)项玛。 - 讓元素托管 shadow DOM 毫無意義 (
<img>
)。
例如弱判,以下方法行不通:
document.createElement('input').attachShadow({mode: 'open'});
// Error. `<input>` cannot host shadow dom.
Light DOM
這是組件用戶寫入的標(biāo)記稍计。該 DOM 不在組件 shadow DOM 之內(nèi),它是元素的實(shí)際孩子裕循。假設(shè)已經(jīng)創(chuàng)建了一個(gè)名為<extended-button>
的定制組件臣嚣,它擴(kuò)展了原生 HTML 按鈕組件,此時(shí)希望在其中添加圖像和一些文本剥哑。代碼如下:
<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 是你創(chuàng)建的組件 extension-button
怎虫。Shadow DOM是 組件的本地組件暑认,它定義了組件的內(nèi)部結(jié)構(gòu)、作用域 CSS 和 封裝實(shí)現(xiàn)細(xì)節(jié)大审。
扁平 DOM 樹
瀏覽器將用戶創(chuàng)建的 Light DOM 分發(fā)到 Shadow DOM蘸际,并對(duì)最終產(chǎn)品進(jìn)行渲染。扁平樹是最終在 DevTools 中看到的以及頁面上呈渲染的對(duì)象徒扶。
<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>
模板 (Templates)
如果需要 Web 頁面上重復(fù)使用相同的標(biāo)簽結(jié)構(gòu)時(shí)粮彤,最好使用某種類型的模板,而不是一遍又一遍地重復(fù)相同的結(jié)構(gòu)姜骡。這在以前也是可以實(shí)現(xiàn)导坟,但是 HTML <template>
元素(在現(xiàn)代瀏覽器中得到了很好的支持)使它變得容易得多。此元素及其內(nèi)容不在 DOM 中渲染圈澈,但可以使用 JavaScript 引用它惫周。
一個(gè)簡單的例子:
<template id="my-paragraph">
<p> Paragraph content. </p>
</template>
這不會(huì)出現(xiàn)在頁面中,直到使用 JavaScrip t引用它康栈,然后使用如下方式將其追加到 DOM 中:
var template = document.getElementById('my-paragraph');
var templateContent = template.content;
document.body.appendChild(templateContent);
到目前為止递递,已經(jīng)有其他技術(shù)可以實(shí)現(xiàn)類似的行為,但是啥么,正如前面提到的登舞,將其原生封裝起來是非常好的,Templates 也有相當(dāng)不錯(cuò)的瀏覽器支持:
模板本身是有用的饥臂,但它們與自定義元素配合會(huì)更好。 可以 customElement
Api 能定義一個(gè)自定義元素似踱,并且告知 HTML 解析器如何正確地構(gòu)造一個(gè)元素隅熙,以及在該元素的屬性變化時(shí)執(zhí)行相應(yīng)的處理。
讓我們定義一個(gè) Web 組件名為 <my-paragraph>
核芽,該組件使用之前模板作為它的 Shadow DOM 的內(nèi)容:
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));
}
});
這里需要注意的關(guān)鍵點(diǎn)是囚戚,我們向影子根添加了模板內(nèi)容的克隆,影子根是使用 Node.cloneNode() 方法創(chuàng)建的轧简。
因?yàn)閷⑵鋬?nèi)容追加到一個(gè) Shadow DOM 中驰坊,所以可以在模板中使用 元素的形式包含一些樣式信息,然后將其封裝在自定義元素中哮独。如果只是將其追加到標(biāo)準(zhǔn) DOM 中拳芙,它是無法工作。
例如皮璧,可以將模板更改為:
<template id="my-paragraph">
<style>
p {
color: white;
background-color: #666;
padding: 5px;
}
</style>
<p>Paragraph content. </p>
</template>
現(xiàn)在自定義組件可以這樣使用:
<my-paragraph></my-paragraph>
<slot> 元素
模板有一些缺點(diǎn)舟扎,主要是靜態(tài)內(nèi)容,它不允許我們渲染變量/數(shù)據(jù)悴务,好可以讓我們按照一般使用的標(biāo)準(zhǔn) HTML 模板的習(xí)慣來編寫代碼睹限。Slot 是組件內(nèi)部的占位符,用戶可以使用自己的標(biāo)記來填充。讓我們看看上面的模板怎么使用 slot
:
<template id="my-paragraph">
<p>
<slot name="my-text">Default text</slot>
</p>
</template>
如果在標(biāo)記中包含元素時(shí)沒有定義插槽的內(nèi)容羡疗,或者瀏覽器不支持插槽染服,<my-paragraph>
就只展示文本 “Default text”。
為了定義插槽的內(nèi)容叨恨,應(yīng)該在 <my-paragraph>
元素中包含一個(gè) HTML 結(jié)構(gòu)柳刮,其中的 slot 屬性的值為我們定義插槽的名稱:
<my-paragraph>
<span slot="my-text">Let's have some different text!</span>
</my-paragraph>
可以插入插槽的元素稱為 Slotable; 當(dāng)一個(gè)元素插入一個(gè)插槽時(shí),它被稱為開槽 (slotted)特碳。
注意诚亚,在上面的例子中,插入了一個(gè) <span>
元素午乓,它是一個(gè)開槽元素站宗,它有一個(gè)屬性 slot
,它等于 my-text
益愈,與模板中的 slot
定義中的 name
屬性的值相同梢灭。
在瀏覽器中渲染后,上面的代碼將構(gòu)建以下扁平 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>
設(shè)定樣式
使用 shadow DOM 的組件可通過主頁來設(shè)定樣式蒸其,定義其自己的樣式或提供鉤子(以 CSS 自定義屬性的形式)讓用戶替換默認(rèn)值敏释。
代碼部署后可能存在的BUG沒法實(shí)時(shí)知道,事后為了解決這些BUG摸袁,花了大量的時(shí)間進(jìn)行l(wèi)og 調(diào)試钥顽,這邊順便給大家推薦一個(gè)好用的BUG監(jiān)控工具 Fundebug。
組件定義的樣式
作用域 CSS 是 Shadow DOM 最大的特性之一:
- 外部頁面的 CSS 選擇器不應(yīng)用于組件內(nèi)部
- 組件內(nèi)定義的樣式不會(huì)影響頁面的其他元素靠汁,它們的作用域是宿主元素
shadow DOM 內(nèi)部使用的 CSS 選擇器在本地應(yīng)用于組件實(shí)際上蜂大,這意味著我們可以再次使用公共vid/類名,而不用擔(dān)心頁面上其他地方的沖突,最佳做法是在 Shadow DOM 內(nèi)使用更簡單的 CSS 選擇器,它們?cè)谛阅苌弦膊诲e(cuò)蝶怔。
看看在 #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 偽類選擇器
使用 :host
偽類選擇器澳叉,用來選擇組件宿主元素中的元素 (相對(duì)于組件模板內(nèi)部的元素)。
<style>
:host {
display: block; /* by default, custom elements are display: inline */
}
</style>
當(dāng)涉及到 :host
選擇器時(shí)沐悦,應(yīng)該小心一件事:父頁面中的規(guī)則具有比元素中定義的 :host
規(guī)則具有更高的優(yōu)先級(jí)成洗,這允許用戶從外部覆蓋頂級(jí)樣式。而且 :host
只在影子根目錄下工作藏否,所以你不能在Shadow DOM 之外使用它泌枪。
如果 :host(<selector>)
的函數(shù)形式與 <selector>
匹配,你可以指定宿主秕岛,對(duì)于你的組件而言碌燕,這是一個(gè)很好的方法误证,它可讓你基于宿主將對(duì)用戶互動(dòng)或狀態(tài)的反應(yīng)行為進(jìn)行封裝,或?qū)?nèi)部節(jié)點(diǎn)進(jìn)行樣式設(shè)定:
<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>)
:host-context(<selector>)
或其任意父級(jí)與 <selector> 匹配修壕,它將與組件匹配愈捅。 例如,在文檔的元素上可能有一個(gè)用于表示樣式主題 (theme) 的 CSS 類慈鸠,而我們應(yīng)當(dāng)基于它來決定組件的樣式蓝谨。
比如,很多人都通過將類應(yīng)用到 <html> 或 <body> 進(jìn)行主題化:
<body class="lightheme">
<custom-container>
…
</custom-container>
</body>
在下面的例子中青团,只有當(dāng)某個(gè)祖先元素有 CSS 類theme-light時(shí)譬巫,我們才會(huì)把background-color樣式應(yīng)用到組件內(nèi)部的所有元素中:
:host-context(.theme-light) h2 {
background-color: #eef;
}
/deep/
組件樣式通常只會(huì)作用于組件自身的 HTML 上,我們可以使用 /deep/
選擇器督笆,來強(qiáng)制一個(gè)樣式對(duì)各級(jí)子組件的視圖也生效芦昔,它不但作用于組件的子視圖师郑,也會(huì)作用于組件的內(nèi)容泊窘。
在下面例子中,我們以所有的元素為目標(biāo)泥畅,從宿主元素到當(dāng)前元素再到 DOM 中的所有子元素:
:host /deep/ h3 {
font-style: italic;
}
/deep/
選擇器還有一個(gè)別名 >>>
料扰,可以任意交替使用它們凭豪。
/deep/
和>>>
選擇器只能被用在仿真 (emulated)模式下。 這種方式是默認(rèn)值晒杈,也是用得最多的方式嫂伞。
從外部為組件設(shè)定樣式
有幾種方法可從外部為組件設(shè)定樣式:最簡單的方法是使用標(biāo)記名稱作為選擇器,如下
custom-container {
color: red;
}
外部樣式比在 Shadow DOM 中定義的樣式具有更高的優(yōu)先級(jí)拯钻。
例如帖努,如果用戶編寫選擇器:
custom-container {
width: 500px;
}
它將覆蓋組件的樣式:
:host {
width: 300px;
}
對(duì)組件本身進(jìn)行樣式化只能到此為止。但是如果人想要對(duì)組件的內(nèi)部進(jìn)行樣式化说庭,會(huì)發(fā)生什么情況呢然磷?為此郑趁,我們需要 CSS 自定義屬性刊驴。
使用 CSS 自定義屬性創(chuàng)建樣式鉤子
如果組件的開發(fā)者通過 CSS 自定義屬性提供樣式鉤子,則用戶可調(diào)整內(nèi)部樣式寡润。其思想類似于<slot>
捆憎,但適用于樣式。
看看下面的例子:
<!--?main page -->
<style>
custom-container {
margin-bottom: 60px;
-?custom-container-bg: black;
}
</style>
<custom-container background>…</custom-container>
在其 shadow DOM 內(nèi)部:
:host([background]) {
background: var(?-?custom-container-bg, #CECECE);
border-radius: 10px;
padding: 10px;
}
在本例中梭纹,該組件將使用 black 作為背景值躲惰,因?yàn)橛脩糁付嗽撝担駝t变抽,背景顏色將采用默認(rèn)值 #CECECE
础拨。
作為組件的作者氮块,是有責(zé)任讓開發(fā)人員了解他們可以使用的 CSS 定制屬性,將其視為組件的公共接口的一部分诡宗。
在 JS 中使用 slot
Shadow DOM API 提供了使用 slot 和分布式節(jié)點(diǎn)的實(shí)用程序滔蝉,這些實(shí)用程序在編寫自定義元素時(shí)遲早派得上用場。
slotchange 事件
當(dāng) slot
的分布式節(jié)點(diǎn)發(fā)生變化時(shí)塔沃,slotchange
事件將觸發(fā)蝠引。例如,如果用戶從 light DOM 中添加/刪除子元素蛀柴。
var slot = this.shadowRoot.querySelector('#some_slot');
slot.addEventListener('slotchange', function(e) {
console.log('Light DOM change');
});
要監(jiān)視對(duì) light DOM 的其他類型的更改,可以在元素的構(gòu)造函數(shù)中使用 MutationObserver
鸽疾。以前討論過 MutationObserver 的內(nèi)部結(jié)構(gòu)以及如何使用它。
assignedNodes() 方法
有時(shí)候肮韧,了解哪些元素與 slot 相關(guān)聯(lián)非常有用。調(diào)用 slot.assignedNodes()
可查看 slot 正在渲染哪些元素弄企。 {flatten: true}
選項(xiàng)將返回 slot 的備用內(nèi)容(前提是沒有分布任何節(jié)點(diǎn))。
讓我們看看下面的例子:
<slot name=’slot1’><p>Default content</p></slot>
假設(shè)這是在一個(gè)名為 <my-container>
的組件中拘领。
看看這個(gè)組件的不同用法,以及調(diào)用 assignedNodes()
的結(jié)果是什么:
在第一種情況下约素,我們將向 slot
中添加我們自己的內(nèi)容:
<my-container>
<span slot="slot1"> container text </span>
</my-container>
調(diào)用 assignedNodes()
會(huì)得到 [<span slot= " slot1 " > container text </span>]
,注意士葫,結(jié)果是一個(gè)節(jié)點(diǎn)數(shù)組。
在第二種情況下慢显,將內(nèi)容置空:
<my-container> </my-container>
調(diào)用 assignedNodes()
的結(jié)果將返回一個(gè)空數(shù)組 []
。
在第三種情況下荚藻,調(diào)用 slot.assignedNodes({flatten: true})
,得到結(jié)果是: [<p>默認(rèn)內(nèi)容</p>]
应狱。
此外,要訪問 slot
中的元素祠丝,可以調(diào)用 assignedNodes()
來查看元素分配給哪個(gè)組件 slot
疾呻。
事件模型
值得注意的是除嘹,當(dāng)發(fā)生在 Shadow DOM 中的事件冒泡時(shí),會(huì)發(fā)生什么岸蜗。
當(dāng)事件從 Shadow DOM 中觸發(fā)時(shí)憾赁,其目標(biāo)將會(huì)調(diào)整為維持 Shadow DOM 提供的封裝。也就是說散吵,事件的目標(biāo)重新進(jìn)行了設(shè)定龙考,因此這些事件看起來像是來自組件,而不是來自 Shadow DOM 中的內(nèi)部元素矾睦。
下面是從 Shadow DOM 傳播出去的事件列表(有些沒有):
- 聚焦事件:blur晦款、focus、focusin枚冗、focusout
- 鼠標(biāo)事件:click缓溅、dblclick、mousedown赁温、mouseenter坛怪、mousemove,等等
- 滾輪事件:wheel
- 輸入事件:beforeinput股囊、input
- 鍵盤事件:keydown袜匿、keyup
- 組合事件:compositionstart、compositionupdate稚疹、compositionend
- 拖放事件:dragstart居灯、drag、dragend内狗、drop怪嫌,等等
自定義事件
默認(rèn)情況下,自定義事件不會(huì)傳播到 Shadow DOM 之外柳沙。如果希望分派自定義事件并使其傳播岩灭,則需要添加 bubbles: true
和 composed: true
選項(xiàng)。
讓我們看看派發(fā)這樣的事件是什么樣的:
var container = this.shadowRoot.querySelector('#container');
container.dispatchEvent(new Event('containerchanged', {bubbles: true, composed: true}));
瀏覽器支持
如希望獲得 shadow DOM 檢測功能赂鲤,請(qǐng)查看是否存在 attachShadow:
const supportsShadowDOMV1 = !!HTMLElement.prototype.attachShadow;
有史以來第一次噪径,我們擁有了實(shí)施適當(dāng) CSS 作用域蛤袒、DOM 作用域的 API 原語妙真,并且有真正意義上的組合荚守。 與自定義元素等其他網(wǎng)絡(luò)組件 API 組合后,shadow DOM 提供了一種編寫真正封裝組件的方法锈候,無需花多大的功夫或使用如 <iframe>
等陳舊的東西泵琳。
關(guān)于Fundebug
Fundebug專注于JavaScript、微信小程序谷市、微信小游戲迫悠、支付寶小程序创泄、React Native括蝠、Node.js和Java線上應(yīng)用實(shí)時(shí)BUG監(jiān)控忌警。 自從2016年雙十一正式上線,F(xiàn)undebug累計(jì)處理了9億+錯(cuò)誤事件感混,付費(fèi)客戶有Google弧满、360庭呜、金山軟件犀忱、百姓網(wǎng)等眾多品牌企業(yè)阴汇。歡迎大家免費(fèi)試用!
[圖片上傳失敗...(image-a6bff-1548725834164)]