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