你真的瞭解 Web Component 嗎?
爲什麼使用框架?
對框架的理解
作爲現代前端開發者,擁抱框架是生存的不二法則,有些人一入場便投身框架的海洋,有些人則有幸見證過變革,從原生,到 jq,到各種框架大行其道的今天。而當前,國內佔領市場份額最多的要數 vue、react 和 angular,他們都有着各自的特點,這也是它們一路走來的立足之本。
那麼作爲使用者的我們,在使用框架高效處理業務的同時,對框架本身也是需要一定程度的理解,以此來輔助我們更好的學習、瞭解和應用框架。下面有一個表格,內容提煉自尤雨溪本人對三大框架的對比看法,也許可以一定程度提升我們對框架的認知。
職責範圍的意義:
-
大的職責範圍讓開發者習慣把問題拋給框架,
-
小的職責範圍讓開發者習慣把問題拋給社區。
框架的優勢
基於上述框架間的差異化,我們可以看出框架各自不同的設計、發展和其衍生出的生態其實都是源自於最初各自對於職責範圍界定的不同而來。但儘管差異不小,它們依然存在着共性,而共性,正是源於框架本身存在的意義和目標。
回頭審視,你會發現所有的框架其實都有共同的特點和目標,就是基於原生,然後更高的效率,更棒的性能,更好的差異抹平。
但我們需要正確理解這句話,這並不意味着框架的指標就優於原生,而是說,因爲有了框架,我們不用再手寫不依賴業務場景的數據 - 視圖的綁定,不用再手動抹平平臺或瀏覽器之間的差異,不用再陷入操作 dom 的同時還要兼顧性能苦惱。可以說框架提高了開發者開發和實現功能的各項下限,讓快速開發和基礎性能之間更好的平衡。我們以 react 和 vue 爲例,這兩大框架所帶來的優勢包括但不限於:
-
數據綁定 (單 / 雙向)
-
組件化開發 (各種鉤子 / 生命週期 / 作用域隔離)
-
虛擬 dom(diff 算法) 以及路由等。
-
......
但這些優勢不是憑空而來,就像 vue 的雙向綁定,從使用 object.defineProperty 轉爲使用 proxy,這種類似的實現或者說轉變,核心之處都需要 js 語法以及瀏覽器的原生支持。因爲 web 應用最終都是要運行在宿主 -- 瀏覽器上的,所以制定規範的各大瀏覽器廠商以及提供原生 api 支持的瀏覽器環境纔是王道,而框架不是。我們之所以需要引入各類的框架、工具庫去實現各種優秀的設計與思想,比如組件化,本質上是因爲原生未直接提供對應的方式或是 api,所以才需要框架去構建棋盤之上的又一層規則體系,來實現開發者的訴求。
而框架這種在瀏覽器原生規則之上又一層較高程度的封裝,在帶來便利高效的同時,不可避免的帶來兩個缺陷:
- 性能的下降,這也是爲什麼上面說有時原生的直接操作指標要優於框架。下面是一些關於處理 dom 的 react vs js 的對比:
(圖 1: 桌面 chrome; 圖 2: 平板 chrome; 圖 3: 移動端 chrome;)
- 框架環境的隔離,例如 vue 的組件庫沒辦法很好的銜接在 react 的項目中 (也許你會說 vuera 或微前端,但事實上 ROI 和性能並不好,開發和維護的成本較高)。
那麼如果原生可以提供某些 api,是不是就可以一定程度上替代框架的某些功能,在擁有便利高效的同時,跨平臺、跨框架的使用,還能較大限度的保持原生的性能?
這就是接下來要聊到的是 web component 和其所能帶來的可能甚至是變革。
認識 web component
web component
回看上文中,我們對框架優勢的分析羅列,可以發現解決這些開發痛點的方案早已存在,也就是與之對應的框架優勢中的組件化。那麼根據上面的分析,既然原生支持了,是不是意味着可以顛覆框架?這種想法是有些衝動的,單純依靠原生的 api 去顛覆框架是不現實的,能顛覆框架的也必須是框架,因爲每一個框架都意味着對應的生態 (路由管理、狀態管理、dom 性能優化管理等)。如果有一天,當前框架中的大部分優秀的設計與思想被原生環境所吸收並支持,那麼在此基礎上衍生的框架,才能真正具備替代當前三大框架的能力,成爲前端唯一一類框架。
而現在,我們雖然還是無法捨棄框架擁抱原生,但是我們可以將其中的一部分進行替代,使之擁有框架提供的優勢,又能避免因框架而導致的缺陷。
原生組件化能否替代框架組件化?
我們先來看看組件化的特點:
-
高內聚,低耦合
-
標記鮮明易維護
-
塊狀接口易擴展
再看看依據組件化的規範,框架組件化提供給我們最直觀的體驗:
-
高效複用
-
作用域及樣式隔離
-
自定義開發
-
鉤子函數 (生命週期)
-
......
最後我們來看看 web component 給我們提供了什麼:
-
Custom elements:自定義元素,通過使用對應的 api,用戶可以在不依賴框架的情況下,開發原生層面的自定義元素,最關鍵的是,它將包含獨立的生命週期,以及提供了自定義屬性的監聽。這就意味着它也同樣具備了較高的可操作性。
-
Shadow DOM:影子 dom(最大的特點是不暴露給全局),你可以通過對應的 api,將 shadow dom 附加給你的自定義元素,並控制其相關功能。利用 shadow dom 的特性,起到隔離的作用,使特性保密,不用再擔心所編寫的腳本及樣式與文檔其他部分衝突。
-
HTML 模版:通過
<template/>
、<slot/>
去實現內容分發。或者你可以回憶一下 vue 的插槽 (slot) 和 react 的 props.children。但事實上,真的是 vue 最先創立的 slot 嗎?看下面~
那麼至少從理論的角度上說,web component 是完全有能力替代框架組件化的,這意味着開發者可以在不使用的框架的前提下進行組件化開發,而且開發出的組件可以無縫嵌入使用了框架的項目中。有趣的是在最新發布的 vue3.2 中,也初步引入了對於 web component 的使用:
兼容性
作爲開發者,面對新的強大的 api,在充滿熱情的同時,更需要關注其可用性和普及範圍。我們可以通過 can i use 去查看它的兼容性:https://caniuse.com/?search=web%20component。從中我們可以看到:
1. Custom elements 兼容性
2. Shadow DOM 兼容性
3. HTML templates 兼容性
自主定製元素和自定義內置元素
在 Custom elements 兼容性的描述中,我們看到兩個概念,如下:
-
自主定製元素:獨立元素;它們不繼承自內置的 HTML 元素。
-
自定義內置元素:這些元素繼承並擴展了內置的 HTML 元素。
那麼這裏怎麼去理解自主定製元素和自定義內置元素?我們可以從具體的 code 實現上進行觀察:
- 自主定製元素
js:
...
customElements.define('custom-elements', class);
...
html:
<body>
...
<custom-elements></custom-elements>
...
</body>
- 自定義內置元素
js:
...
customElements.define('custom-elements', class, { extends: 'p' });
...
html:
<body>
...
<p is="custom-elements"></p>
...
</body>
可以看到從聲明上是沒有太大區別的,都是通過 customElements.define 去定義聲明,並且需要一個 class 去構建內部的生命週期與屬性監聽。區別之處在於自定義內置元素需要在後面的配置項中設置要繼承的內置 HTML 元素 (指原生的元素)。
而最大的區別是在於使用上,自主定製元素其實就是一個完整的自定義組件,可以讓我們在不依賴任何框架的前提下實現組件化。而自定義內置組件,可以理解爲是對所繼承的原生元素的改造(如上述 code 呈現,聲明定義自定義組件時,指定繼承的原生元素,後續使用該原生元素時,通過 is 屬性引用聲明的自定義組件,就可以改造該原生元素,使其擁有生命週期、自定義組件和作用域隔離的功能)。
web component api 的使用
自定義組件的聲明和使用
所依賴的主要接口是 CustomElementRegistry,該接口提供了,用作支持自定義組件的使用和聲明:
- window.customElements.define。
該方法用來聲明自定義組件,接受 3 個參數,無返回值:
-
name:將要全局註冊的自定義組件名字 (必須是中劃線的形式)。
-
constructor:一個類,如果聲明的是自主定製元素,則必須繼承自 HTMLElement;如果聲明的是自定義內置元素,則必須繼承它將要擴展的原生元素所屬的類 (如要擴展 div,那就必須繼承 HTMLDivElement)。並且類的構造函數中,必須執行 super。
-
options:一個可選的配置對象,只有在聲明自定義內置元素時使用,且當前只有一個配置項 extends,值爲將要擴展的原生元素的標籤名。
聲明示例:
//自主定製元素
class CustomEle extends HTMLElement {
constructor() {
super();
...
}
}
customElements.define('custom-ele', CustomEle);
//自定義內置元素,如果要擴展div的話
class CustomEleBuiltIn extends HTMLDivElement {
constructor() {
super();
...
}
}
customElements.define('custom-ele-build-in', CustomEleBuiltIn, { extends: 'div' });
使用的方式也是多樣的。可以通過 document.createElement 的方式使用,也可以直接書寫在 html 中。使用示例:
//自主定製元素
const customEle = document.createElement('custom-ele');
customEle.setAttribute('img', 'https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fcdn.duitang.com%2Fuploads%2Fitem%2F201409%2F06%2F20140906020558_h4VfY.png');
customEle.setAttribute('text', '我是一段懸停說明');
document.querySelector('#app').appendChild(customEle);
customElements.define('custom-ele', CustomEle);
//或
customElements.define('custom-ele', CustomEle);
const customEle = new CustomEle();
customEle.setAttribute('img', 'https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fcdn.duitang.com%2Fuploads%2Fitem%2F201409%2F06%2F20140906020558_h4VfY.png');
customEle.setAttribute('text', '我是一段懸停說明');
document.querySelector('#app').appendChild(customEle);
//或
<custom-ele img="https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fcdn.duitang.com%2Fuploads%2Fitem%2F201409%2F06%2F20140906020558_h4VfY.png" text="我是一段懸停說明">
//自定義內置元素,如果要擴展div的話
customElements.define('custom-ele-build-in', CustomEleBuiltIn, { extends: 'div' });
const div = document.createElement('div', { is: 'custom-ele-build-in' });
div.setAttribute('img', 'https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fcdn.duitang.com%2Fuploads%2Fitem%2F201409%2F06%2F20140906020558_h4VfY.png');
div.setAttribute('text', '我是一段懸停說明');
document.querySelector('#app').appNode.appendChild(div);
//或
<div is="custom-ele-build-in" img="https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fcdn.duitang.com%2Fuploads%2Fitem%2F201409%2F06%2F20140906020558_h4VfY.png" text="我是一段懸停說明" />
這裏的幾 種使用方式其實還是有差異的,在初始化的時候,直接引用的方式可以在構造階段就拿到掛載的各個屬性;但是採用 create 的方式時,構造階段無法第一時間獲取屬性,當然,利用生命週期的鉤子函數,也是解決該問題的。
- window.customElements.get。
該方法用來獲取自定義組件的構造函數,接受一個參數,即聲明過的自定義組件的 name,返回構造函數。
const getCustomConstructorBefore = customElements.get('custom-ele');
console.log('getCustomConstructor-before', getCustomConstructorBefore);//undefined
customElements.define('custom-ele', CustomEle);
const getCustomConstructorAfter = customElements.get('custom-ele');
console.log('getCustomConstructor-after', getCustomConstructorAfter);//CustomEle
- window.customElements.upgrade。
該方法是用來更新掛載主文檔之前的包含 shadow dom 的自定義組件的,接受一個參數,即包含了 shadow dom 的自定義組件節點,無返回值。(自定義組件在被 append 到主文檔的時候,會觸發自動更新)。
//先創建了自定義元素
const customEle = document.createElement('custom-ele');
customEle.setAttribute('img', 'https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fcdn.duitang.com%2Fuploads%2Fitem%2F201409%2F06%2F20140906020558_h4VfY.png');
customEle.setAttribute('text', '我是一段懸停說明');
//後聲明自定義元素
customElements.define('custom-ele', CustomEle);
//結果爲false,null
console.log(customEle instanceof CustomEle, customEle.shadowRoot);
//進行更新節點
customElements.upgrade(customEle);//或document.querySelector('#app').appendChild(customEle);
//true,#document-fragment
console.log(customEle instanceof CustomEle, customEle.shadowRoot);
- window.customElements.whenDefined。
該方法是用來檢測並提供自定義組件被定義聲明完畢的時機得,接受一個參數,即自定義元素的 name,返回值是一個 promise(只檢測自定義組件是否被 defined,不檢測是否被掛載於主文檔)。若提供的 name 無效,則觸發 promise 的 catch。
//創建了自定義元素dom
const customEle = document.createElement('custom-ele');
customEle.setAttribute('img', 'https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fcdn.duitang.com%2Fuploads%2Fitem%2F201409%2F06%2F20140906020558_h4VfY.png');
customEle.setAttribute('text', '我是一段懸停說明');
//用來判斷關閉定時器得標識
let isStop = false;
//獲取自定義組件定義完畢的時機
customElements.whenDefined('custom-ele').then(() => {
console.log('定義完畢');
isStop = true;
});
//一個用於觀察得計時器
const timer = setInterval(() => {
if (isStop) {
clearInterval(timer);
return;
}
console.log(Math.floor(Date.now() / 1000));
}, 1000);
//延遲3秒進行自定義組件的定義及聲明
setTimeout(() => {
customElements.define('custom-ele', CustomEle);
}, 3000)
自定義組件的生命週期
- constructor
自定義組件的第一個生命週期,用來初始化自定義組件本身。觸發的時機在自定義組件被 document.createElement 的時候 (前提是組件已經被 customElements.define 過,如果組件是先 create,後 defined,那麼 constructor 的執行時機在 append 到主文檔裏時)。
class CustomEle extends HTMLElement {
constructor() {
super();
console.log('constructor被執行');
......
}
}
customElements.define('custom-ele', CustomEle);
const customEle = document.createElement('custom-ele');
- connectedCallback
在組件被成功添加到主文檔時觸發的生命週期,在 constructor 之後。
class CustomEle extends HTMLElement {
constructor() {
super();
console.log('constructor被執行');
......
}
connectedCallback () {
console.log('connectedCallback被執行');
}
}
customElements.define('custom-ele', CustomEle);
const customEle = document.createElement('custom-ele');
document.querySelector('#app').appendChild(customEle);
- attributeChangedCallback
自定義組件最關鍵的一個生命週期。觸發時機在組件屬性被增加、刪除或修改的時候。如果你是在組件被 append 之前設置了屬性,那麼就會在 connectedCallback 之前觸發;反之,則在 connectedCallback 之後觸發。需要配合靜態方法 observedAttributes 來使用,只有註冊在 observedAttributes 中的屬性纔會被監聽。
class CustomEle extends HTMLElement {
constructor() {
super();
console.log('constructor被執行');
......
}
connectedCallback () {
console.log('connectedCallback被執行');
}
static get observedAttributes () { return [ 'img', 'text' ]; }
attributeChangedCallback (name, oldValue, newValue) {
console.log('attributeChangedCallback', name)
if (name === 'img') {
this.shadowRoot.querySelector('img').src = this.getAttribute('img');
}
if (name === 'text') {
this.shadowRoot.querySelector('.info').textContent = this.getAttribute('text');
}
}
}
customElements.define('custom-ele', CustomEle);
customEle.setAttribute('img', 'https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fcdn.duitang.com%2Fuploads%2Fitem%2F201409%2F06%2F20140906020558_h4VfY.png');
customEle.setAttribute('text', '我是一段懸停說明');
const customEle = document.createElement('custom-ele');
document.querySelector('#app').appendChild(customEle);
- adoptedCallback
當元素被移動到新的文檔時,被調用。即元素是另一個文檔的元素,而 adoptedCallback 是新文檔下的自定義組件的回調。
//聲明自定義組件的類
class CustomEle extends HTMLElement {
constructor() {
super();
......
}
adoptedCallback () {
console.log('adoptedCallback被執行');
}
}
//創造場景,增加iframe,即舊文檔
appNode.innerHTML = '<iframe></iframe>';
const p = document.createElement('p');
p.innerHTML = 'iframe';
appNode.querySelector('iframe').contentWindow.document.body.appendChild(p);
//新文檔中創建自定義組件
const customEle = document.createElement('custom-ele');
customEle.setAttribute('img', 'https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fcdn.duitang.com%2Fuploads%2Fitem%2F201409%2F06%2F20140906020558_h4VfY.png');
customEle.setAttribute('text', '我是一段懸停說明');
customElements.define('custom-ele', CustomEle);
appNode.appendChild(customEle);
//將元素從舊文檔遷移到新文檔
setTimeout(() => {
console.log('開始對元素進行adoptNode操作')
const node = appNode.querySelector('iframe').contentWindow.document.body.firstElementChild;
appNode.appendChild(document.adoptNode(node))
}, 2000);
- disconnectedCallback
自定義組件的最後一個生命週期,觸發的時機在組件被成功從主文檔移除時。
class CustomEle extends HTMLElement {
constructor() {
super();
......
}
disconnectedCallback () {
console.log('disconnectedCallback被執行');
}
}
customElements.define('custom-ele', CustomEle);
const customEle = document.createElement('custom-ele');
document.querySelector('#app').appendChild(customEle);
setTimeout(() => {
appNode.removeChild(customEle);
}, 2000)
Shadow DOM 的使用
其作用是將標記結構、樣式和行爲隱藏起來,並與頁面上的其他代碼相隔離。Shadow DOM 都不是一個新事物,在過去的很長一段時間裏,瀏覽器用它來封裝一些元素的內部結構,回憶一下 video 標籤內部被隱藏起來的控制按鈕們。
- 爲元素附加 Shadow DOM:ele.attachShadow
attachShadow 接受一個對象參數,只需關注一個配置屬性 mode,如果設置爲 open,表示可以從外部獲取 Shadow DOM 內部的元素;如果設置爲 closed,則表示隱藏 Shadow DOM 內部,例如 。
class CustomEle extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
......
}
}
customElements.define('custom-ele', CustomEle);
const customEle = document.createElement('custom-ele');
document.querySelector('#app').appendChild(customEle);
console.log(customEle.shadowRoot)
-
操作元素的 Shadow DOM 並添加樣式
當爲一個元素附加了 Shadow DOM 後,就可以使用同操作正常 dom 一樣的方法去操作了。示例如下:
class CustomEle extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
const wrapper = document.createElement('span');
wrapper.setAttribute('class', 'wrapper');
const icon = document.createElement('span');
icon.setAttribute('class', 'icon');
const info = document.createElement('span');
info.setAttribute('class', 'info');
const text = this.getAttribute('text');
info.textContent = text;
const img = document.createElement('img');
img.src = this.getAttribute('img');
icon.appendChild(img);
const style = document.createElement('style');
// console.log('CustomEle', style.isConnected);
style.textContent = `
.wrapper {
position: relative;
}
.info {
font-size: 0.8rem;
width: 200px;
display: inline-block;
border: 1px solid black;
padding: 10px;
background: white;
border-radius: 10px;
opacity: 0;
transition: 0.6s all;
position: absolute;
bottom: 20px;
left: 10px;
z-index: 3;
}
img {
width: 1.2rem;
}
.icon:hover + .info, .icon:focus + .info {
opacity: 1;
}
`;
shadow.appendChild(style);
// console.log('CustomEle', style.isConnected);
shadow.appendChild(wrapper);
wrapper.appendChild(icon);
wrapper.appendChild(info);
}
}
const customEle = document.createElement('custom-ele');
customEle.setAttribute('img', 'https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fcdn.duitang.com%2Fuploads%2Fitem%2F201409%2F06%2F20140906020558_h4VfY.png');
customEle.setAttribute('text', '我是一段懸停說明');
customElements.define('custom-ele', CustomEle);
document.querySelector('#app').appendChild(customEle);
const style = document.createElement('style');
// console.log('CustomEle', style.isConnected);
style.textContent = `
.wrapper {
position: relative;
}
.info {
font-size: 0.8rem;
width: 200px;
display: inline-block;
border: 1px solid black;
padding: 10px;
background: white;
border-radius: 10px;
opacity: 0;
transition: 0.6s all;
position: absolute;
bottom: 20px;
left: 10px;
z-index: 3;
}
img {
width: 1.2rem;
}
.icon:hover + .info, .icon:focus + .info {
opacity: 1;
}
`;
shadow.appendChild(style);
替換爲:
const linkElem = document.createElement('link');
linkElem.setAttribute('rel', 'stylesheet');
linkElem.setAttribute('href', 'style.css');//樣式的地址
shadow.appendChild(linkElem);
需要注意的是:由於 link 元素不會打斷 shadow root 的繪製, 因此在加載樣式表時可能會出現未添加樣式內容(FOUC),導致閃爍。
模版
- template
使用包裹的內容不會在頁面上顯示,但是卻可以被 js 引用到。這就意味着有些內容我們不用重複構建多遍,使用 構建一遍,然後多次引用處理就好了。
class CustomEle extends HTMLElement {
constructor() {
super();
console.log('constructor被執行');
const shadow = this.attachShadow({ mode: 'open' });
let template = document.getElementById('my-paragraph');
if (template) {
let templateContent = template.content;
shadow.appendChild(templateContent.cloneNode(true));
}
......
}
}
appNode.innerHTML = '<template id="my-paragraph"><style>p {color: white;background-color: #666;padding: 5px;}</style><p>My paragraph</p></template>';
const customEle = document.createElement('custom-ele');
customEle.setAttribute('img', 'https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fcdn.duitang.com%2Fuploads%2Fitem%2F201409%2F06%2F20140906020558_h4VfY.png');
customEle.setAttribute('text', '我是一段懸停說明');
customElements.define('custom-ele', CustomEle);
appNode.appendChild(customEle);
-
slot
-
在 template 的基礎上,更加靈活的內容分發,可以配合 template 使用 (在 template 中定義佔位符,然後將 template 的內容 clone 到 shadow DOM 中)。也可以直接在 shadow DOM 中添加佔位符。
然後在自定義組件的 innerhtml 中使用即可。
class CustomEle extends HTMLElement {
constructor() {
super();
console.log('constructor被執行');
const shadow = this.attachShadow({ mode: 'open' });
let template = document.getElementById('my-paragraph');
if (template) {
let templateContent = template.content;
shadow.appendChild(templateContent.cloneNode(true));
}
const slot2 = document.createElement('slot');
slot2.setAttribute('name', 'newText2');
shadow.appendChild(slot2);
......
}
}
appNode.innerHTML = '<template id="my-paragraph"><style>p {color: white;background-color: #666;padding: 5px;}</style><slot >newText1</p></custom-ele>';
const customEle = document.createElement('custom-ele');
customEle.innerHTML = '<p slot="newText2">newText2</p>';
customElements.define('custom-ele', CustomEle);
appNode.appendChild(customEle);
- slotchange:用於監聽 shadow DOM 中的 slot 插入或移除的事件。
class CustomEle extends HTMLElement {
constructor() {
super();
let template = document.getElementById('my-paragraph');
if (template) {
let templateContent = template.content;
shadow.appendChild(templateContent.cloneNode(true));
}
const slots = shadow.querySelectorAll('slot');
slots.forEach(slot => {
slot.addEventListener('slotchange', function (e) {
console.log('slotchange', slot.name, e);
});
});
......
}
}
appNode.innerHTML = '<template id="my-paragraph">' +
'<style>p {color: white;background-color: #666;padding: 5px;}</style>' +
'<slot ></slot>' +
'<slot ></slot>' +
'</template>' +
'<h3>' +
'<custom-ele class="newText1Box">' +
'<p slot="newText1">newText1</p>' +
'<span slot="spanText">spanText</span>' +
'</custom-ele>' +
'</h3>';
setTimeout(() => {
document.querySelector('.newText1Box').removeChild(document.querySelector('.newText1Box p'));
//或
document.querySelector('.newText1Box p').removeAttribute('slot');
}, 2000)
在添加slot時(直接插入包含slot屬性的元素或給已插入的元素增加slot屬性)或刪除slot時(直接remove包含slot屬性的元素或給已插入的元素removeAttribute slot屬性),都會觸發slotchange事件。
相關的其他 api
- element.attachShadow(opt):用來給指定元素掛載 shadow DOM。
opt 的配置項:
-
mode:如果爲 open,表示可以在外部通過 element.shadowRoot 獲取 shadow DOM 節點。並且方法會返回 shadow DOM 對象。如果爲 closed,表示不允許外部訪問 shadow DOM 節點,並且方法返回 null。
-
delegatesFocus:表示是否減輕自定義元素的聚焦性能問題。當 shadow DOM 中不可聚焦的部分被點擊時, 讓第一個可聚焦的部分成爲焦點, 並且 shadow host 將提供所有可用的 :focus 樣式.
-
css 僞類:
-
:defined:表示所有內置元素及已經通過 customElements.define 註冊的元素。
-
:host:只能在 shadow DOM 的樣式表內書寫。表示當前所在的自定義組件的所有實例及 shadow DOM 下所有的元素。
-
:host([選擇器]):只能在 shadow DOM 的樣式表內書寫。是: host 的增強,表示: host() 所在的自定義組件的所有實例中選擇器符合括號中名稱的實例及其包含的 shadow DOM 下屬所有元素。
-
:host-context([選擇器]):只能在 shadow DOM 的樣式表內書寫。是: host 的增強,表示: host()-context 所在的自定義組件的所有實例的父元素中選擇器符合括號中名稱的實例及其包含的 shadow DOM 下屬所有元素。
-
:slotted([選擇器]): 只能在 shadow DOM 的樣式表內書寫。表示: slotted() 所在的自定義組件的所有實例中選擇器符合括號中名稱的 slot 元素,若選擇器爲 *,則表示命中所有 slot。
-
節點相關拓展
-
getRootNode: 使用方式爲 ele. getRootNode(opt),opt 中包含一個屬性 composed,爲 true 時,檢索到的根元素爲 document;爲 false 時,如果 ele 是屬於 shadow DOM,那麼檢索到 shadow DOM,否則檢索到 document。
-
isConnected: 是元素的一個只讀屬性接口。返回元素是否與 dom 樹連接的 boolean 值。即是否被 append 到主文檔中。
-
event 擴展
-
composed 屬性:用來指示該事件是否可以從 Shadow DOM 傳遞到一般的 DOM(測試後發現不論是普通 DOM 還是 shadow DOM 均爲 true)。
-
path 屬性:返回事件的路徑。如果 shadow root 是使用 mode 爲 closed 創建的,則不包括 shadow 樹中的節點 (測試後發現儘管 shadowdom 設置了 mode 爲 closed,依然能獲取完整的 path)。
-
關於 slot
-
ele.assignedSlot:用來獲取 ele 元素上代表插入 slot 的元素。但如果 ele.attachShadow 中的 mode 是 closed 爲 closed 時,返回 null。
-
ele.slot:用來獲取元素上 slot 的 name 值。
-
......
相關的庫及網站
-
webcomponents.org — site featuring web components examples, tutorials, and other information.
-
Hybrids — Open source web components library, which favors plain objects and pure functions over class and this syntax. It provides a simple and functional API for creating custom elements.
-
Polymer — Google's web components framework — a set of polyfills, enhancements, and examples. Currently the easiest way to use web components cross-browser.
-
Snuggsi.es — Easy Web Components in ~1kB Including polyfill — All you need is a browser and basic understanding of HTML, CSS, and JavaScript classes to be productive.
-
Slim.js — Open source web components library — a high-performant library for rapid and easy component authoring; extensible and pluggable and cross-framework compatible.
-
Smart.js — Web Components library with simple API for creating cross-browser custom elements.
-
Stencil — Toolchain for building reusable, scalable design systems in web components.
參考
-
https://developer.mozilla.org/zh-CN/docs/Web/Web_Components
-
https://medium.com/jspoint/the-anatomy-of-web-components-d6afedb81b37
-
https://www.ruanyifeng.com/blog/2019/08/web_components.html
-
https://html.spec.whatwg.org/multipage/custom-elements.html#custom-elements
-
https://developers.google.cn/web/fundamentals/web-components
-
https://objectpartners.com/2015/11/19/comparing-react-js-performance-vs-native-dom/
-
https://bugs.webkit.org/show_bug.cgi?id=182671
❤️ 謝謝支持
以上便是本次分享的全部內容,希望對你有所幫助 ^_^
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/o79-iOAJMKX_7a__gmSX2A