網(wǎng)絡(luò)技術(shù)的快速發(fā)展,帶來(lái)了層出不窮的新概念和框架,尤其是在前端開發(fā)領(lǐng)域,新技術(shù)的出現(xiàn)如同浪潮般一波接一波,例如 Vue3 和 Vite 的組合。而在這種技術(shù)快速更新的環(huán)境中, Web Components 作為一項(xiàng)已經(jīng)存在一段時(shí)間的技術(shù),為什么仍然值得我們深入學(xué)習(xí)和探討呢?
---- 文章的篇幅可能較長(zhǎng),借助目錄效果更好。
Web Components
是由 W3C 推動(dòng)的標(biāo)準(zhǔn)化技術(shù),它得到了包括 Chrome、Firefox、Safari 和 Edge 在內(nèi)的主流瀏覽器的廣泛支持。不僅 Vue3 的更新就包括了對(duì) Web Components
的原生支持,現(xiàn)在面試也問(wèn) Web Components
話題,尤其是頻頻出現(xiàn) Shadow DOM
。
這項(xiàng)技術(shù)的魅力在于,它允許開發(fā)者創(chuàng)建自定義、可重用的元素,這些元素可以在任何符合標(biāo)準(zhǔn)的 Web 應(yīng)用中無(wú)縫使用,而不受限于特定的框架(React、Vue)。如果你還對(duì) Web Components 比較陌生,那么現(xiàn)在是時(shí)候開始了解這項(xiàng)技術(shù)了。
Web Components 核心概念 Web Components 是一種瀏覽器原生支持的 Web 組件化技術(shù),它允許開發(fā)者創(chuàng)建可重用的自定義元素 ,并且可以在任何支持 Web Components 的瀏覽器中使用。
image.png
Web Components 包括以下幾個(gè)核心概念:
Custom Elements (自定義元素):允許開發(fā)者創(chuàng)建新的 HTML 元素,并且可以定義它的行為和樣式。Shadow DOM (影子 DOM):允許開發(fā)者封裝組件的內(nèi)部結(jié)構(gòu)和樣式,避免全局命名空間的污染。Templates (模板):允許開發(fā)者定義一個(gè)可以在多個(gè)組件中重用的 HTML 結(jié)構(gòu)。Slots (插槽):允許開發(fā)者創(chuàng)建一個(gè)可插入內(nèi)容的占位符,以便在不同的組件中使用。今天將圍繞這 4 個(gè)核心概念以及相關(guān)拓展,通過(guò)例子演示重點(diǎn)說(shuō)一下 Web Components 是如何創(chuàng)建可重用的自定義元素的。
Custom Elements(自定義元素) Web Components 最大的特性之一就是能將 HTML 封裝成 Custom Elements(自定義元素)。下面我們通過(guò)一個(gè)簡(jiǎn)單的按鈕例子,看下它是怎么實(shí)現(xiàn)的。
創(chuàng)建自定義元素 首先,我們需要定義一個(gè)自定義元素。這可以通過(guò)使用 customElements.define()
方法來(lái)實(shí)現(xiàn)。在這個(gè)例子中,我們將創(chuàng)建一個(gè)名為 my-button
的自定義元素。
// main.js class MyButton extends HTMLElement { constructor () { super (); const shadowRoot = this .attachShadow({ mode : 'open' }); shadowRoot.innerHTML = ` <style> button { background-color: #4CAF50; border: none; color: white; text-align: center; text-decoration: none; display: inline-block; font-size: 16px; margin: 4px 2px; cursor: pointer; padding: 10px 24px; border-radius: 4px; } </style> <button>Click Me!</button> ` ; } } customElements.define('my-button' , MyButton);
現(xiàn)在我們已經(jīng)定義了一個(gè)名為 my-button
的自定義元素,我們可以在 HTML 文件中直接使用它。
<!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <meta name ="viewport" content ="width=device-width, initial-scale=1.0" > <title > Web Components Example</title > </head > <body > <my-button > </my-button > <script src ="./main.js" > </script > </body > </html >
在這個(gè)例子中,我們創(chuàng)建了一個(gè)名為 my-button
的自定義元素,并在 HTML 文件中直接使用它。這個(gè)自定義元素將渲染為一個(gè)綠色的按鈕,上面寫著“Click Me!”。
不止如此,CustomElements 還支持自定義元素行為(如添加點(diǎn)擊事件),也就是說(shuō)既能封裝 UI 樣式,也是封裝 UI 交互。
const shadowRoot = this .attachShadow({ mode : 'open' }); shadowRoot.querySelector('button' ).addEventListener('click' , () => { alert('按鈕被點(diǎn)擊了!' ); });
到這里為止,便實(shí)現(xiàn)了一個(gè)簡(jiǎn)單的 Web Components,詳細(xì)代碼見CustomElements [1] 。
生命周期回調(diào)方法 Custom Elements 也有一組生命周期回調(diào)方法(到這里是不是感覺(jué) Web Component 就像 Vue、React似得,怎么還有生命周期?
),這些方法在元素的不同生命周期階段被調(diào)用。這些生命周期方法允許你在元素的創(chuàng)建、插入文檔、更新和刪除等時(shí)刻執(zhí)行操作。
以下是自定義元素的一些主要生命周期回調(diào)方法:
constructor() : 構(gòu)造函數(shù),在創(chuàng)建元素實(shí)例時(shí)調(diào)用。適用于執(zhí)行初始化操作,例如設(shè)置初始屬性、添加事件監(jiān)聽器或創(chuàng)建 Shadow DOM。connectedCallback() : 當(dāng)自定義元素被插入到上下文時(shí)調(diào)用。適用于元素被插入到 DOM 時(shí)執(zhí)行的操作,例如獲取數(shù)據(jù)、渲染內(nèi)容或啟動(dòng)定時(shí)器。disconnectedCallback() : 當(dāng)自定義元素從文檔中移除時(shí)調(diào)用。適用于元素從 DOM 中移除時(shí)執(zhí)行的操作,例如移除事件監(jiān)聽器或停止定時(shí)器。attributeChangedCallback(attributeName, oldValue, newValue) : 當(dāng)自定義元素的屬性被添加、移除或更改時(shí)調(diào)用。要使用這個(gè)回調(diào),你需要在類中定義一個(gè)靜態(tài)的 observedAttributes
屬性,列出你想要監(jiān)聽的屬性。下面是一個(gè)簡(jiǎn)單的例子,展示了如何在自定義元素中使用這些生命周期方法:
class MyCustomElement extends HTMLElement { constructor () { super (); // 初始化操作,例如創(chuàng)建 Shadow DOM const shadowRoot = this .attachShadow({ mode : 'open' }); shadowRoot.innerHTML = '<p>這是一個(gè)自定義元素</p>' ; } connectedCallback() { // 元素被插入到 DOM 時(shí)執(zhí)行的操作 console .log('Custom element connected to the DOM' ); } disconnectedCallback() { // 元素從 DOM 中移除時(shí)執(zhí)行的操作 console .log('Custom element disconnected from the DOM' ); } attributeChangedCallback(name, oldValue, newValue) { // 監(jiān)聽的屬性發(fā)生變化時(shí)執(zhí)行的操作 console .log(`Attribute ${name} changed from ${oldValue} to ${newValue} ` ); } static get observedAttributes() { // 返回一個(gè)數(shù)組,包含需要監(jiān)聽的屬性 return ['my-attribute' ]; } } customElements.define('my-custom-element' , MyCustomElement);
在 HTML 中使用這個(gè)自定義元素:
<my-custom-element my-attribute ="value" > </my-custom-element >
當(dāng) my-custom-element
被插入到 DOM 中時(shí),connectedCallback
會(huì)被調(diào)用。如果元素被從 DOM 中移除,disconnectedCallback
會(huì)被調(diào)用。如果元素的 my-attribute
屬性發(fā)生變化,attributeChangedCallback
會(huì)被調(diào)用。
注意 :監(jiān)聽的同時(shí),也記得停止監(jiān)聽。比如說(shuō)你可能需要在元素連接到 DOM 時(shí)開始監(jiān)聽事件,但是在元素?cái)嚅_連接時(shí)停止監(jiān)聽,避免內(nèi)存泄漏。
Shadow DOM(影子 DOM) 下面我們將繼續(xù)探討 Shadow DOM,它是 Web Components 的核心特性之一。
Shadow DOM Shadow DOM 允許開發(fā)者創(chuàng)建一個(gè)封閉的 DOM 子樹,這個(gè)子樹與主文檔的 DOM 分離,這意味著 Shadow DOM 內(nèi)部的樣式和結(jié)構(gòu)不會(huì)受到外部的影響,也不會(huì)影響到外部。
在“Custom Elements(自定義元素)”的例子中,我們已經(jīng)簡(jiǎn)單使用了 Shadow DOM。
1、使用 innerHTML
通過(guò)設(shè)置 Shadow DOM 的 innerHTML 屬性,可以直接添加一個(gè)或多個(gè)元素。這種方式適用于從字符串模板快速填充 Shadow DOM。
class MyElementInnerHTML extends HTMLElement { constructor () { super (); const shadowRoot = this .attachShadow({ mode : 'open' }); shadowRoot.innerHTML = ` <style> p { color: black; } </style> <p>使用 innerHTML</p> ` ; } } customElements.define('my-element-inner' , MyElementInnerHTML);
2、使用 createElement 和 appendChild
也可以使用 document.createElement 方法創(chuàng)建一個(gè)新元素,然后使用 appendChild 方法將其添加到 Shadow DOM 中。
const wrapper = document .createElement('p' ); wrapper.textContent = '使用 createElement 和 appendChild' ;var style = document .createElement('style' ); style.textContent = ` p { color: gray; } ` ;// 引入外部樣式同樣可以使用 appendChild // const linkElement = document.createElement('link'); // linkElement.setAttribute('rel', 'stylesheet'); // linkElement.setAttribute('href', 'style.css'); class MyElementAppend extends HTMLElement { constructor () { super (); const shadowRoot = this .attachShadow({ mode : 'open' }); shadowRoot.appendChild(wrapper); shadowRoot.appendChild(style); // shadowRoot.appendChild(linkElement); } } customElements.define('my-element-append' , MyElementAppend);
3、template 方式
除上面兩種方式外,還可以使用模板元素 (<template>
)添加,具體見下方 “Templates(模版)” 。
Shadow Mode 其中在自定義元素的構(gòu)造函數(shù)中,我們調(diào)用了 attachShadow()
方法,并傳入了一個(gè)對(duì)象 { mode: 'open' }
。這里的 mode
屬性決定了 Shadow DOM 的封裝模式,它有兩個(gè)可能的值:
open
:允許外部訪問(wèn) Shadow DOM 的 API。closed
:不允許外部訪問(wèn) Shadow DOM 的 API。在這個(gè)例子中,我們創(chuàng)建了一個(gè) Shadow DOM,并向其中添加了一行文字和相關(guān)的樣式。由于 Shadow DOM 的封裝性,這些樣式只會(huì)在 my-element
元素內(nèi)部生效,不會(huì)影響到頁(yè)面上的其他元素(樣式隔離)。
下面我們更詳細(xì)地探討 Shadow DOM 是否允許外部訪問(wèn),的兩種封裝模式:open
和 closed
。
1、Shadow Mode:open 模式
當(dāng)使用 open
模式創(chuàng)建 Shadow DOM 時(shí),外部腳本可以通過(guò) Element.shadowRoot
屬性訪問(wèn) Shadow DOM 的根節(jié)點(diǎn)。
這意味著你可以從外部查詢、修改 Shadow DOM 內(nèi)部的元素和樣式。下面是一個(gè)使用 open
模式的例子:
class OpenMyElement extends HTMLElement { constructor () { super (); // 創(chuàng)建一個(gè) open 模式的 Shadow DOM const shadowRoot = this .attachShadow({ mode : 'open' }); shadowRoot.innerHTML = ` <style> p { color: red; } </style> <p>這是一個(gè) open 模式的 Shadow DOM</p> ` ; } } customElements.define('open-my-element' , OpenMyElement);// 在外部訪問(wèn) Shadow DOM const element = document .querySelector('open-my-element' );console .log(element.shadowRoot); // 輸出 ShadowRoot 對(duì)象
在這個(gè)例子中,我們創(chuàng)建了一個(gè)自定義元素 open-my-element
,它有一個(gè) open
模式的 Shadow DOM。由于模式是 open
,我們可以在外部通過(guò) element.shadowRoot
訪問(wèn) Shadow DOM 的根節(jié)點(diǎn),并進(jìn)行進(jìn)一步的操作,比如添加或刪除子元素,修改樣式等。
image.png
2、Shadow Mode:closed 模式
當(dāng)使用 closed
模式創(chuàng)建 Shadow DOM 時(shí),外部腳本無(wú)法通過(guò) Element.shadowRoot
屬性訪問(wèn) Shadow DOM 的根節(jié)點(diǎn)。
這意味著 Shadow DOM 內(nèi)部的元素和樣式對(duì)外部是完全隱藏的,無(wú)法從外部直接訪問(wèn)或修改。 下面是一個(gè)使用 closed
模式的例子:
class ClosedMyElement extends HTMLElement { constructor () { super (); // 創(chuàng)建一個(gè) closed 模式的 Shadow DOM const shadowRoot = this .attachShadow({ mode : 'closed' }); shadowRoot.innerHTML = ` <style> p { color: blue; } </style> <p>這是一個(gè) closed 模式的 Shadow DOM</p> ` ; } } customElements.define('closed-my-element' , ClosedMyElement);// 在外部嘗試訪問(wèn) Shadow DOM const element = document .querySelector('closed-my-element' );console .log(element.shadowRoot); // 輸出 null
在這個(gè)例子中,我們創(chuàng)建了一個(gè)自定義元素 closed-mode-element
,它有一個(gè) closed
模式的 Shadow DOM。由于模式是 closed
,當(dāng)我們嘗試在外部通過(guò) element.shadowRoot
訪問(wèn) Shadow DOM 的根節(jié)點(diǎn)時(shí),將得到 null
。
image.png
open
和 closed
模式?jīng)Q定了 Shadow DOM 的封裝程度:
open
模式允許外部訪問(wèn) Shadow DOM 的 API,這意味著你可以從外部查詢和修改 Shadow DOM 內(nèi)部的元素和樣式。closed
模式不允許外部訪問(wèn) Shadow DOM 的 API,這意味著 Shadow DOM 內(nèi)部的元素和樣式對(duì)外部是完全隱藏的,無(wú)法從外部直接訪問(wèn)或修改。選擇哪種模式取決于你的具體需求。如果你希望組件的內(nèi)部結(jié)構(gòu)和樣式完全對(duì)外部隱藏,使用 closed
模式是更好的選擇。如果你需要從外部訪問(wèn)和修改組件的內(nèi)部結(jié)構(gòu)和樣式,使用 open
模式會(huì)更合適。
完整代碼,詳見 ShadowDOM [2] 。
其外,Shadow DOM 還支持更高級(jí)的用法,比如可以將 Shadow DOM 分割成多個(gè) Shadow Trees,使用 slots(插槽)來(lái)插入內(nèi)容,以及使用 template(模板)來(lái)定義可重用的 HTML 結(jié)構(gòu)。
Slots(插槽) Slots 是一種特殊類型的元素,它允許你將內(nèi)容從組件的一個(gè)部分傳遞到另一個(gè)部分,增加了組件的靈活性。它使得 Web Components 自定義元素,更加的靈活。
基礎(chǔ)使用 例如,我們可以修改 my-button
組件,使其允許用戶自定義按鈕文本:
class MyButton extends HTMLElement { constructor () { super (); const shadowRoot = this .attachShadow({ mode : 'open' }); shadowRoot.innerHTML = ` <style> /* ...樣式代碼保持不變... */ </style> <button> <slot>Click Me!</slot> </button> ` ; } } customElements.define('my-button' , MyButton);
現(xiàn)在,當(dāng)我們?cè)?HTML 中使用 my-button
時(shí),我們可以向其中插入任何內(nèi)容,它會(huì)替換掉 <slot>
標(biāo)簽:
<my-button > Slots Custom Text</my-button >
image.png
命名插槽 在開發(fā)中,我們更多的還會(huì)遇到不同情況下,選擇插入的內(nèi)容,這里就用到了命名插槽,使用起來(lái)非常方便。
class MyButtonName extends HTMLElement { constructor () { super (); const shadowRoot = this .attachShadow({ mode : 'open' }); shadowRoot.innerHTML = ` <style> /* ...樣式代碼保持不變... */ </style> <button> <slot name="element-name"></slot> <slot name="element-age"></slot> <slot name="element-email"></slot> </button> ` ; } } customElements.define('my-button-name' , MyButtonName);
<my-button-name > <span slot ="element-name" > element-name</span > </my-button-name > <my-button-name > <span slot ="element-age" > element-age</span > </my-button-name > <my-button-name > <span slot ="element-email" > element-email</span > </my-button-name >
image.png
是不是很方便,很靈活??!具體代碼詳見Web Components Slots [3] 。
Templates(模板) Templates 允許你定義一個(gè)可以在多個(gè)組件中重用的 HTML 結(jié)構(gòu)。你可以將模板放在 HTML 文件中的任何位置,并通過(guò) JavaScript 動(dòng)態(tài)地實(shí)例化它們:
<my-button > </my-button > <template id ="my-button-template" > <style > /* ...樣式代碼保持不變... */ </style > <button > <slot > Click Me!</slot > </button > </template >
在 JavaScript 中,你可以這樣使用模板:
class MyButton extends HTMLElement { constructor () { super (); const shadowRoot = this .attachShadow({ mode : 'open' }); const template = document .getElementById('my-button-template' ); // 使用`cloneNode()` 方法添加了拷貝到 Shadow root 根節(jié)點(diǎn)上。 shadowRoot.appendChild(template.content.cloneNode(true )); } } customElements.define('my-button' , MyButton);
image.png
這樣,你就可以在不同的組件中重用同一個(gè)模板,從而提高代碼的可維護(hù)性和重用性。具體代碼下詳見Web Components Templates [4] 。
相關(guān)拓展 Web Components 兼容性 Web Components 是一組用于構(gòu)建可復(fù)用組件的技術(shù),包括 Custom Elements, Shadow DOM, HTML Templates 等。這些技術(shù)的出現(xiàn),使得開發(fā)者能夠更好地組織,去開發(fā)復(fù)雜的網(wǎng)頁(yè)應(yīng)用。然而,由于這些技術(shù)相對(duì)較新,不同瀏覽器的支持情況不盡相同,因此兼容性問(wèn)題也是我們需要重點(diǎn)關(guān)注的方向。
Custom Elements
image.png
Shadow DOM
image.png
HTML Templates
image.png
從上面可以看出,現(xiàn)階段市場(chǎng)上大部分的瀏覽器已經(jīng)都原生支持了 Web Components 的規(guī)范標(biāo)準(zhǔn)。但是如果說(shuō)出現(xiàn)了兼容性問(wèn)題,我們應(yīng)該怎么處理?
Polyfills 對(duì)于舊版瀏覽器不支持的兼容性情況,可以考慮使用 polyfill 來(lái)實(shí)現(xiàn)兼容性。Polyfills 是一種代碼注入技術(shù),使得瀏覽器可以支持新的標(biāo)準(zhǔn) API。對(duì)于不支持 Web Components 的瀏覽器,我們可以用 Polyfills 讓這些瀏覽器可以支持 Web Components。
這里我們可以用到 webcomponents.js [5] 庫(kù),它可以實(shí)現(xiàn)兼容 Custom Elements、Shadow DOM 和 HTML Templates 標(biāo)準(zhǔn),讓我們?cè)陂_發(fā)時(shí)不必考慮兼容性問(wèn)題。
npm install @webcomponents/webcomponentsjs
<!-- load webcomponents bundle, which includes all the necessary polyfills --><script src ="node_modules/@webcomponents/webcomponentsjs/webcomponents-bundle.js" > </script > <!-- load the element --><script type ="module" src ="my-element.js" > </script > <!-- use the element --><my-element > </my-element >
具體配置詳情,見polyfills webcomponents [6] 。
React 與 Vue 相信大家也比較關(guān)心 Web Components 與現(xiàn)有框架(如 React、Vue)相比有哪些優(yōu)勢(shì)?以及各自適用場(chǎng)景?
首先,Web Components 是一組 Web 平臺(tái) API,允許開發(fā)者創(chuàng)建可重用的自定義元素,而無(wú)需依賴于任何特定的框架。與現(xiàn)有的前端框架,Web Components 有以下幾個(gè)優(yōu)勢(shì):
標(biāo)準(zhǔn)化 :Web Components 是基于 Web 標(biāo)準(zhǔn)(如 Custom Elements、Shadow DOM 和 HTML Templates)構(gòu)建的,這意味著它們得到了瀏覽器廠商的直接支持,而不依賴于任何特定的庫(kù)或框架。輕量級(jí) :Web Components 不需要額外的庫(kù)或框架即可工作,這可以減少應(yīng)用程序的依賴性和大小,特別是在不需要框架其他功能的情況下。封裝性 :通過(guò) Shadow DOM,Web Components 可以將標(biāo)記結(jié)構(gòu)、樣式和腳本封裝在一起,避免全局樣式和腳本的沖突,保證了組件的獨(dú)立性和重用性。易于集成 :Web Components 可以與現(xiàn)有的框架(如 React 和 Vue)集成,開發(fā)者可以在這些框架中使用 Web Components,或者將現(xiàn)有的框架組件封裝成 Web Components 以供其他項(xiàng)目使用。然而,Web Components 也有其局限性,例如:
生態(tài)系統(tǒng) :與 React 和 Vue 等成熟框架相比,Web Components 的生態(tài)系統(tǒng)較小,社區(qū)支持和資源可能不如這些框架豐富。功能限制 :Web Components 本身不提供狀態(tài)管理、路由等高級(jí)功能,這些通常需要額外的庫(kù)或框架來(lái)實(shí)現(xiàn)。性能 :對(duì)于復(fù)雜的應(yīng)用程序,一些框架(如 React)通過(guò)虛擬 DOM 等技術(shù)提供了更高的性能優(yōu)化,而 Web Components 需要開發(fā)者手動(dòng)優(yōu)化。總的來(lái)說(shuō),Web Components 提供了一種標(biāo)準(zhǔn)化且框架無(wú)關(guān)的方式來(lái)構(gòu)建組件,適合組件庫(kù)的開發(fā) 。而框架如 React、Vue 則在生態(tài)系統(tǒng)支持、開發(fā)體驗(yàn)和數(shù)據(jù)處理方面有明顯優(yōu)勢(shì),適合快速開發(fā)復(fù)雜的應(yīng)用程序 。
實(shí)際應(yīng)用案例 Vue3 : Vue3 引入了對(duì) Web Components 的原生支持,通過(guò)所謂的 “Vue Components”,它允許將 Vue 組件轉(zhuǎn)換為 Web Components。MicroApp [7] :基于 Web Components 的一款簡(jiǎn)約、高效、功能強(qiáng)大的微前端框架。Twitter [8] :Twitter 2016 年開始將自己的嵌入式推文 從 iframe 切換成 ShadowDOM,減少了內(nèi)存消耗、加快了渲染速度,并批量渲染的時(shí)候保持絲滑。svelte + vite 開發(fā) Web Components [9] :通過(guò) svelte + vite 快速搭建 web components 的項(xiàng)目。使用 Polymer 構(gòu)建 Web Components [10] :用于構(gòu)建 Web Component,它提供了一套工具和 API,能夠更容易地創(chuàng)建自定義元素。參考資料 **MDN Web Docs - Web Components 入門** [11] 你不知道的 Web Components - 現(xiàn)狀 [12] 自定義元素 v1 - 可重復(fù)使用的網(wǎng)絡(luò)組件 [13] Web Components Tutorial for Beginners \[2019\] [14] 總結(jié) Web Components 是 W3C 推動(dòng)的標(biāo)準(zhǔn)化技術(shù),它通過(guò)自定義元素的方式,允許開發(fā)者在瀏覽器中直接使用。這種技術(shù)通過(guò) Shadow DOM 實(shí)現(xiàn)了組件化 DOM 隔離和樣式隔離,確保了組件的獨(dú)立性和可重用性,這些特性被現(xiàn)有很多借鑒和使用。
該文章在 2024/3/27 16:37:49 編輯過(guò)