
Web Components 核心技術一覽
Web Components 是由瀏覽器原生支援的一組技術,讓開發者可以創建可重用的客製化元素,並封裝其功能與介面,使其封裝性和獨立性達到類似框架元件的效果。Web Components 核心由三部分組成:Custom Elements(自訂元素)、Shadow DOM(影子 DOM)以及HTML Template(HTML 範本)。下面讓我們逐一簡介這三大技術,了解它們如何為封裝外部嵌入元件奠定基礎。
Custom Elements(自訂元素)
Custom Elements 提供了定義自訂 HTML 元素的能力。我們可以透過 JavaScript 建立一個類別繼承 HTMLElement,並使用 customElements.define(名稱, 類別) 將其註冊為新的元素。這個名稱通常是兩個單字組成(中間必須有連字號),例如 <youtube-player> 或 <google-map>,以避免與原生 HTML 標籤衝突。註冊後,我們就能像使用內建標籤一樣,在 HTML 中使用這個自訂元素。
自訂元素的類別可以定義多種生命週期回呼方法,例如:
- connectedCallback():元素被插入 DOM 時觸發,適合在此做初始化工作(如載入外部資源、渲染內容)。
- disconnectedCallback():元素從 DOM 中移除時觸發,可用於清理資源。
- attributeChangedCallback(name, oldValue, newValue):監聽指定屬性的變化,對應地更新元件狀態。
透過 Custom Elements,我們可以將外部元件的初始化與行為邏輯封裝在類別中。例如封裝一個 YouTube 播放器時,可以在 connectedCallback 裡生成對應的 iframe。使用者引入這個自訂元素 <youtube-player> 時,不需要關心內部實作細節,只需像使用普通標籤一樣使用它。這種抽象讓我們在頁面上多次使用嵌入元件時不會「一團亂」,而是有條理地管理每個獨立的元素實例。
Shadow DOM(影子 DOM)
Shadow DOM 是 Web Components 的核心封裝機制。簡而言之,它允許我們將自訂元素的內部 DOM 結構附加一個「影子樹」,使其與主文件的 DOM 分離。當我們在自訂元素的類別中呼叫 this.attachShadow({ mode: 'open' }),就建立了一個 shadow root 作為該元素的隱藏 DOM 根節點。我們可以把內部的子元素、樣式都插入這個 shadow root 中。
影子 DOM 帶來幾項關鍵好處:
- 樣式封裝:Shadow DOM 內部的樣式不會影響到外部頁面,外部的 CSS 也預設無法穿透進 shadow DOM(除非透過特殊 CSS 自訂屬性或 ::part 等機制)。這意味著你可以在封裝的元件內部放心地書寫 CSS,而不用擔心破壞頁面其他部分的樣式,也不怕頁面上其他框架或樣式干擾到元件內部。對於嵌入第三方內容而言,這種隔離特別寶貴——例如外部 widget 的結構或樣式再複雜,也被關在元件的影子樹內,不會與你的應用產生衝突。
- DOM 隔離:類似地,shadow DOM 的結構對外部是不可見的(除了透過 element.shadowRoot 可取得 open mode 的影子樹外,外部 JavaScript 無法直接訪問裡面的元素)。這讓自訂元素內部的實作細節對外部腳本是私有的,避免外部意外干涉。對封裝外部嵌入元件來說,我們可以確保第三方內容被束縛在元件內,例如內部產生的 <iframe> 或 <canvas> 不會被頁面其他 script 隨意取得或修改。
總而言之,Shadow DOM 提供了類似「沙盒」的環境,用來構築元件的 UI 和行為。當使用 <youtube-player> 這樣的自訂元素時,頁面只會看到一個 <youtube-player> 標籤本身,至於其內實際包含哪些子節點(iframe、按鈕、樣式等等)都是隱藏在影子 DOM 裡,彷彿黑盒子一般運作。這種封裝非常適合拿來裝載「外部」內容,讓它們看起來就像原生的一部分。
HTML Template 與 Slots(範本與插槽)
HTML Template (<template>) 元素提供了一種定義可重複使用的 HTML 結構的方式。<template> 內的內容在頁面載入時不會被渲染,而是處於靜默狀態,等待透過 JavaScript 複製和插入 DOM 時才會生效。搭配 Web Components,我們常將 <template> 用來定義自訂元素的結構藍圖。典型做法是:在 HTML 中寫好 <template id="my-component-template">...</template>,裡面包含元件的內部 HTML 結構(甚至可以內含 <style> 樣式)。在自訂元素的類別中,我們可以取得這個模板並將其內容 clone 下來附加到 shadow DOM。這樣可以方便地定義元件的初始 DOM 結構,讓多人協作或維護時一目了然。更棒的是,模板內嵌的 <style> 也會跟隨內容進入 shadow DOM 中,被封裝起來。
然而,一個靜態的模板往往不足以應付各種情境。因此 Web Components 還提供了 <slot> 插槽機制,讓元件可以預留可插入的佔位。在 shadow DOM 的模板中使用 <slot name="content">,即可在元件中定義一個名為 "content" 的插槽。使用者在頁面上使用自訂元素時,可以在元素標籤內放置子節點並標記對應的 slot,例如:
<my-component>
<div slot="content">這段內容會被插入元件內部指定的位置</div>
</my-component>
這樣,元件在渲染時就會將提供的內容投射到 shadow DOM 中對應的 <slot> 位置上。Slots 大大提升了元件的靈活性和延展性 —— 我們可以封裝外部元件的主要框架,同時允許使用者傳入客製化的部分。例如封裝一個對話框元件時,提供 <slot name="header">、<slot name="body"> 等讓開發者插入自訂的抬頭和內容。同理,在封裝外部嵌入時,也可以設計插槽以便使用者調整或添加額外元素(如額外的說明文字、載入指示器等)。
總結而言,Custom Elements 定義了行為,Shadow DOM 提供封裝,而 Template 和 Slots 則賦予結構和彈性。掌握這三大核心,我們就有能力構築起自己的 Web Components,從零開始封裝各種外部嵌入元件,而無需任何第三方框架。
為什麼要用 Web Components 封裝外部元件?
在進入實作之前,我們有必要理解:為什麼 Web Components 特別適合用來封裝「外部嵌入元件」?直接使用 <iframe> 或第三方提供的 <script> snippet 難道不行嗎?以下是傳統方式可能遇到的挑戰,以及 Web Components 能帶來的改進:
首先,傳統 <iframe> 雖然簡單,把外部內容隔離在自己的子畫面中,但也因此帶來整合性問題。大量的 iframe 會像在頁面中開了許多小窗,每個窗裡各自載入完整的 HTML/JS/CSS,導致頁面載入負擔變重,也無法與主頁溝通。例如你嵌入三個不同的內容(影片、地圖、圖表),三個 iframe 就是三個獨立的小世界,彼此和與主頁都缺乏互動。另一方面,若直接在頁面引入第三方 <script>,外部腳本會在全域作用域下執行,污染你的頁面:可能造成命名衝突、安全風險,而且外部腳本通常會自行在 DOM 塞入元素、樣式,難以控制其行為或移除。不僅如此,傳統嵌入方式往往缺乏重用性——每當你在不同頁面想嵌入相同的東西,都得複製同樣的一段 iframe/snippet,無法模組化管理。
Web Components 則提供了一個更現代且模組化的解法:
- 封裝複雜度:透過自訂元素,我們把嵌入元件的所有建立、初始化邏輯都封裝起來。頁面只需寫上一個簡潔的 <my-widget> 標籤即可完成嵌入。這降低了使用門檻,也避免每次嵌入都重寫繁瑣的程式碼或配置。
- 隔離與安全:Shadow DOM 確保外部元件的內容在樣式和結構上是獨立的,不會影響你的應用程式。同時,我們可以在元件內部使用 sandbox iframe 或其他安全措施,讓不受信任的第三方內容依然安全地呈現。在不使用 iframe 的情況下,Web Components 本身不是安全沙箱——外部程式碼仍在同個頁面執行——但因為我們有控制權,可以選擇性地限制或包裝它的行為。例如,只暴露必要的介面給外部、使用嚴格模式或 Content Security Policy 等。另外,如果確實需要 iframe 的安全隔離,也完全可以在 Web Component 裡頭再嵌入一個 iframe,用組合的方式達到安全與封裝並存。
- 互動與通信:自訂元素作為頁面 DOM 的一員,可以和其他元素、應用邏輯自然地交互。我們可以透過屬性或方法來傳入設定,並透過自訂事件將訊息傳出給父頁面。相比之下,傳統 iframe 要和主頁通信得依賴 postMessage 等較麻煩的方式。使用 Web Components,假設我們有兩個嵌入的圖表元件,完全可以用事件機制讓一個圖表的更新去影響另一個,因為它們都處於同一個 DOM 上下文中,這是 iframe 隔離模式下難以做到的。
- 一致的介面:透過 Web Components,我們可以為不同的外部服務設計一致的使用介面。例如,也許 YouTube 和 Vimeo 都提供影片播放器嵌入,我們可以分別封裝 <video-player platform="youtube"> 和 <video-player platform="vimeo">,讓用法和屬性介面相似,減少學習成本。相較之下,每個第三方提供的 snippet 形式可能各異、API 不同,開發者得記住多套用法。
綜上所述,使用 Web Components 封裝外部嵌入元件,能同時兼顧封裝性與靈活度:外部內容被當作獨立元件管理,不會失控地散落在應用中;而應用與元件之間仍有順暢的橋樑,彼此可以傳遞資料、互相影響。理解了這些優點後,我們就能更有方向地投入實作。下面,我們將透過一系列範例,一步步展示如何封裝各種常見的外部嵌入元件。
實戰範例:封裝常見的外部嵌入元件
本章節我們將針對幾種常見的外部內容,示範如何以 Web Component 形式封裝。所有範例都僅使用原生 JavaScript,不依賴任何框架。每個範例會提供元件的程式碼、用法範例,並附上簡要的說明與實現細節。你可以直接將這些範例代碼應用於自己的項目或進一步擴充。
封裝 YouTube 影片播放器
情境:想在頁面中嵌入 YouTube 影片,以往我們可能直接貼上 YouTube 提供的 <iframe> 代碼。但透過 Web Component,我們可以封裝一個更靈活的影片播放器元件,例如 <youtube-player>。使用者只需提供影片的ID或網址,元件就會自動呈現影片。
先看我們的元件程式碼:
class YouTubePlayer extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
const videoId = this.getAttribute('video'); // 從屬性取得影片ID
// 建立iframe元素
const iframe = document.createElement('iframe');
iframe.width = this.getAttribute('width') || '560';
iframe.height = this.getAttribute('height') || '315';
iframe.src = `https://www.youtube.com/embed/${videoId}`;
iframe.allow = 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture';
iframe.allowFullscreen = true;
// 將iframe插入shadow DOM
this.shadowRoot.appendChild(iframe);
}
}
customElements.define('youtube-player', YouTubePlayer);
用法範例:
<!-- 在HTML中直接使用自訂元素來嵌入影片,只需提供影片的ID -->
<youtube-player video="dQw4w9WgXcQ" width="560" height="315"></youtube-player>
說明:上述代碼定義了一個 YouTubePlayer 類別並將其註冊為 <youtube-player> 元素。它在 connectedCallback 中執行嵌入邏輯:從元素的 video 屬性取得影片ID,創建一個 <iframe> 元素,並設定好寬度、高度、YouTube 的嵌入網址以及必要的 allow 權限(允許影片使用全螢幕、播放等功能)。這個 iframe 然後被附加到元件的 shadow DOM 中。由於使用了 shadow DOM,頁面上的其他 CSS 不會影響到這個 iframe,例如父頁面若有針對 iframe 標籤的樣式設定,也不會污染我們的 YouTube 影片內嵌區塊。同時,我們將 allowFullscreen 設為 true,使使用者可以點擊全螢幕觀看。
使用者在HTML中只需要寫一行 <youtube-player video="..."> 就能嵌入影片,無需關心 iframe 的詳細設定。寬度和高度屬性是可選的,若不指定則預設為560x315(YouTube 標準嵌入尺寸)。這個元件可以被多次重複使用,每個 <youtube-player> 元素互相獨立,各自管理自己的 iframe,不會互相干擾。
封裝 Google 地圖
情境:需要在頁面中嵌入地圖,例如顯示某個地點的 Google Maps。傳統做法可能使用 Google 提供的 iframe 嵌入代碼,或動態載入 Google Maps API。但透過封裝 <google-map> 元素,可以更方便地重複使用地圖嵌入,並對使用者隱藏繁瑣的設定(如 API Key 或各種參數)。
這裡我們將示範使用iframe嵌入 Google地圖的方式封裝(相對簡單且無需額外 API 金鑰)。元件程式碼如下:
class GoogleMap extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
// 從屬性取得地圖位置或查詢參數
const locationQuery = this.getAttribute('query') || '';
// 建立iframe並設定Google Maps的嵌入網址
const iframe = document.createElement('iframe');
iframe.width = this.getAttribute('width') || '600';
iframe.height = this.getAttribute('height') || '450';
iframe.style.border = '0';
// 使用 Google Maps 的地圖嵌入服務,輸入查詢參數
iframe.src = `https://www.google.com/maps/embed/v1/place?key=YOUR_API_KEY&q=${encodeURIComponent(locationQuery)}`;
iframe.allowFullscreen = true;
iframe.referrerPolicy = "no-referrer-when-downgrade";
this.shadowRoot.appendChild(iframe);
}
}
customElements.define('google-map', GoogleMap);
用法範例:
<!-- 將地圖嵌入,提供 query 屬性作為地點名稱(需要有效的 API 金鑰) -->
<google-map query="台北101" width="600" height="450"></google-map>
說明:以上我們定義了 <google-map> 元件,在 connectedCallback 中生成一個 Google地圖的 iframe。src 使用的是 Google Maps Embed API(必須提供你自己的API Key)。這個 API 允許透過 URL 查詢參數來指定地點,例如 q=台北101。我們從元素的 query 屬性讀取使用者想顯示的地點字串,經過 encodeURIComponent 處理後拼入地圖的網址。如果沒提供 query,則預設為空字串(此時 Google Maps 可能顯示一個空白地圖或世界地圖)。同時我們允許地圖全螢幕,並設定了 referrerPolicy 以確保地圖正常載入。
使用 <google-map> 的時候,開發者只需給出查詢地點名,不用記憶繁雜的 Google Maps 嵌入網址格式,也不用每次複製巨長的 <iframe> snippet。所有地圖渲染的邏輯由元件接管。如果需要更進階的用法,例如使用 Google Maps JavaScript API 進行互動或標記,自訂元素的封裝也能擴充:我們可以讓元件在初始化時載入 Google Maps JS SDK,將地圖繪製在 shadow DOM 中的一個 <div> 中。但那樣實作相對複雜且需要 API Key 設定,此處選擇 iframe 方式已足以示範封裝概念。讀者在實務中可依需求選擇具體方案。
封裝 Chart.js 圖表
情境:網頁中嵌入圖表通常需要引入一套圖表庫,例如 Chart.js,並撰寫初始化代碼來繪製圖表。如果有多處需要圖表,我們希望避免重複樣板代碼。這時可以封裝一個 <chart-widget> 元素,讓它內部自動處理圖表的繪製。我們將使用 Chart.js 這個流行的開源圖表庫作為例子。
以下是 <chart-widget> 元件的程式碼:
class ChartWidget extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
// 建立 canvas 作為圖表繪製區
this.canvas = document.createElement('canvas');
this.shadowRoot.appendChild(this.canvas);
}
connectedCallback() {
// 取得圖表配置資料(從屬性中擷取 JSON 字串並解析)
const configText = this.getAttribute('data');
this.chartConfig = configText ? JSON.parse(configText) : {
type: 'bar',
data: {
labels: ['範例A', '範例B', '範例C'],
datasets: [{ label: '範例數據', data: [30, 55, 42], backgroundColor: '#4e79a7' }]
},
options: {}
};
// 確認 Chart.js 是否已載入,若無則動態載入 CDN 腳本
if (!window.Chart) {
const script = document.createElement('script');
script.src = "https://cdn.jsdelivr.net/npm/chart.js";
script.onload = () => this._renderChart();
document.head.appendChild(script);
} else {
this._renderChart();
}
}
_renderChart() {
const ctx = this.canvas.getContext('2d');
new Chart(ctx, this.chartConfig);
}
}
customElements.define('chart-widget', ChartWidget);
用法範例:
<!-- 使用自訂圖表元件,透過 data 屬性傳入 Chart.js 的配置 (JSON 格式) -->
<chart-widget data='{
"type":"bar",
"data":{
"labels":["Q1","Q2","Q3"],
"datasets":[{ "label":"營收", "data":[150, 200, 170], "backgroundColor":"#71B37C" }]
}
}'></chart-widget>
說明:這段代碼有點長,但值得細看。<chart-widget> 元件的構造函式中,我們建立了一個 <canvas> 並附加到 shadow DOM 作為圖表的繪製區域。在 connectedCallback,元件會從 data 屬性中讀取圖表設定(這裡我們期望傳入的是一個 JSON 字串)。如果使用者沒有提供 data,我們就使用預設的一組範例資料。接著,程式檢查全域的 window.Chart 是否存在,以判斷 Chart.js 是否已載入。如果尚未載入,我們動態創建一個 <script> 元素指向 Chart.js 的 CDN,當腳本載入完成時(onload 回呼),呼叫私有方法 _renderChart() 來繪製圖表;如果 Chart.js 已存在(例如頁面中某處已經引入過,或者另一個 chart-widget 元件剛好載入完腳本),則直接呼叫 _renderChart()。
_renderChart() 方法中,我們取得前面建立的 canvas 的 2D 繪圖 context,然後使用 new Chart(ctx, this.chartConfig) 初始化圖表。這行運用了 Chart.js 提供的全域 Chart 類別,將我們準備好的配置物件傳入,Chart.js 會在 canvas 上繪製出對應的圖表。
這個元件的用法非常直觀:在 HTML 中放一個 <chart-widget>,並透過 data 屬性提供圖表的設定(包含資料集、標籤、類型等)。由於 JSON 在 HTML 屬性中需要注意引號問題,我們在範例中使用單引號包住整個 JSON 字串,內部使用雙引號,以避免衝突。元件封裝了載入 Chart.js 庫和繪製圖表的所有細節,所以頁面上無需再寫任何 Chart.js 初始化相關的 JS 程式。
需要注意,這種做法在單頁多圖表的情況下可能需要優化:例如,如果出現多個 <chart-widget> 同時初始化,各自都檢查到 Chart.js 未載入而重複載入腳本。我們可以改進元件的實作,使用靜態旗標避免重覆載入,或將 Chart.js 模組打包進元件。但為了簡化範例,我們在這裡著重說明封裝思路,而不深入優化。實務上,當有多個圖表元件時,可考慮在頁面提前引入 Chart.js,或者修改元件讓第一個圖表載入庫後通知後續圖表使用已載入的資源。
封裝通用第三方 iframe
情境:有時我們會需要嵌入一些第三方提供的小工具或頁面,例如天氣預報 Widget、社群媒體貼文、或其他網站的一部分。一般這類嵌入可能也是透過 <iframe src="..."> 完成。我們可以撰寫一個通用的 <embed-frame> 元素,用來包裝任意來源的 iframe。藉由這個元件,我們可以將所有 iframe 的樣式與行為控制集中化,例如設定默認大小、Lazy-load(延遲載入)等。
以下是 <embed-frame> 元件的程式碼:
class EmbedFrame extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
// 插入樣式,讓主機元素變成區塊並控制iframe默認大小
const style = document.createElement('style');
style.textContent = `
:host { display: block; width: 100%; max-width: 100%; height: 400px; }
iframe { width: 100%; height: 100%; border: none; }
`;
this.shadowRoot.appendChild(style);
}
connectedCallback() {
const url = this.getAttribute('src');
const iframe = document.createElement('iframe');
iframe.src = url;
iframe.loading = this.getAttribute('loading') || 'lazy'; // 預設使用 lazy 加載
// allow 屬性可視需要加入,這裡開放常見功能
iframe.allow = 'accelerometer; encrypted-media; picture-in-picture';
this.shadowRoot.appendChild(iframe);
}
}
customElements.define('embed-frame', EmbedFrame);
用法範例:
<!-- 封裝通用iframe的元件,可嵌入任意URL -->
<embed-frame src="https://www.example.com/some-widget" style="height:500px;"></embed-frame>
說明:這個 <embed-frame> 元件相對簡單但非常實用。我們讓元件接收一個 src 屬性,即目標頁面的 URL。connectedCallback 中創建 <iframe> 並將 src 設為這個 URL。此外,我們將 iframe.loading 屬性設為 'lazy'(除非使用者特別指定 loading="eager"),以便默認對 iframe 啟用延遲載入(只有接近視窗時才載入內容),這對於大量 iframe 的頁面能提升效能。
在 constructor 中,我們插入了一段 <style> 到 shadow DOM,設定 :host 為區塊元素且寬度100%,高度預設400px。也定義了 iframe 子元素的寬高為100%且無邊框。這意味著 <embed-frame> 元件在頁面上會擁有自己的尺寸,我們也允許開發者通過行內樣式或CSS覆蓋 height 來改變高度(例如上面的用法示例透過 style 指定高度500px)。外部寬度則預設充滿可用空間,也可由容器環境控制。透過封裝,我們確保無論嵌入什麼來源,都套用了統一的樣式約束。
安全性方面,因為這元件主要用途是嵌入外部內容,如果該內容不受信任,使用者可自行在 iframe 的 src 中加入例如 sandbox 屬性來約束外部內容的權限(可在元件中擴充以接受 sandbox 屬性)。在此示例中,我們簡單開啟了一些常見允許(像影片播放等),開發者可按需調整。
<embed-frame> 元件提高了我們嵌入任意第三方內容的一致性。日後如果想對所有嵌入內容增加某些功能,例如在載入前顯示一個 spinner、或點擊按鈕才載入(交互式 lazy load),都可以在這個元件內部實現,而無需修改每一處嵌入代碼。這正是 Web Components 強大的地方:一次封裝,多處重用。
Web Components 與傳統嵌入方式的比較
在掌握了上述實作範例後,我們來討論使用 Web Components 進行封裝,與傳統嵌入方式相比有哪些優勢和權衡。我們將分別對比 iframe、直接引入 script、以及微前端架構三種常見方案,理解何時該選擇 Web Components,何時其他方案可能更適合。
iframe 的優劣比較
優點:iframe 能提供與主頁完全隔離的環境。外部內容在自己的子頁面中運行,樣式和腳本都不會影響父頁面。同時,透過 sandbox 等屬性,iframe 可以在安全上限制第三方內容的權限,保護主站資料。此外,使用 iframe 實作簡單,只要一行 HTML snippet,就能嵌入影片、地圖等常見內容,開發者不需要了解內部細節。
缺點:iframe 和主頁之間缺乏互動,默認無法與父頁面或其他iframe通信(需要額外使用 window.postMessage 等方式,實作複雜)。在無障礙方面,iframe 內容對螢幕閱讀器和鍵盤導航並不友善,使用上可能有障礙。此外,多個 iframe 會增加頁面負載——每個 iframe 都是獨立的渲染進程,效能較差,在行動裝置上尤其明顯。如果嵌入內容需要與頁面高度融合(例如內嵌的互動部件),iframe 則顯得笨重而不靈活。SEO 方面,iframe 中的內容通常不會被索引到父頁面上,對於希望提升主站內容豐富度的情境來說並不理想。
直接引入 <script> 腳本的優劣比較
優點:通過 <script> 引入第三方元件(如提供一段JS讓你插入),可以將外部內容直接渲染到當前頁面 DOM 中。相對於 iframe,它更容易整合到頁面佈局,外部內容可以自適應父頁面的大小或樣式,響應式表現較佳。不存在跨窗口通信問題,因為外部代碼就在同一個上下文,可以直接操作 DOM。對於SEO,直接插入的內容(若是文本或DOM)有機會被搜索引擎索引,也沒有iframe多餘的請求開銷。
缺點:直接引入外部腳本存在風險——該腳本擁有與你網站相同的權限,可能改變全域變數、修改頁面 DOM,若未妥善控制,易引發衝突或安全漏洞。它也會增大主頁腳本負擔,某些第三方腳本體積龐大或執行緩慢,導致主頁面變慢。另外,因為所有樣式和DOM都混在同一層,容易造成樣式衝突(第三方 widget 的CSS影響你的頁面,或相反)。測試和偵錯上也較困難,因為外部腳本的錯誤可能干擾主應用。總的來說,直接插入 script 缺乏封裝,需要開發者自行確保隔離,這正是 Web Components 所著力改善的痛點。
微前端架構(Module Federation 等)比較
優點:微前端是一種架構模式,允許大型應用拆分為多個獨立部署的小應用。在 Webpack Module Federation 等實現下,不同團隊甚至可以使用不同框架,各部分構建後動態加載整合。與 Web Components 相比,微前端更側重部署和團隊邊界上的解耦:每個微前端有自己的代碼庫與部署管線。使用 Module Federation,可以共享部分程式碼(例如共用某些函式庫以避免重覆載入)並在 runtime 載入遠端模組。這在大型項目或多團隊協作下特別有用,每個子應用可以獨立發布更新。
缺點:微前端的複雜度較高。建置 Module Federation 需要設定打包器,管理版本相容、公共依賴,以及跨微前端的路由與通信。如果只是嵌入幾個簡單的第三方功能,引入微前端架構明顯過重。此外,微前端通常並不保證樣式隔離——若不同微前端使用全域CSS,仍可能互相影響,仍需要搭配像 Web Components 或css-in-js等方案。相比之下,Web Components 較輕量,不需要特定建構工具支援,只依賴瀏覽器標準。對於小型嵌入或UI 級別的重用,Web Components 上手更容易,維護成本更低。微前端更適合整個應用層級的拆分,而 Web Components 則可以作為微前端之間溝通的橋樑(例如每個微前端導出一系列 Web Components 供主應用使用)。
總結:iframe 注重隔離但缺乏融合,script 注重融合但缺乏隔離,而 Web Components 設法兩者兼得:在同一頁面上下文中提供元件級的隔離與重用。微前端則是更高層級的拆解方式,可與 Web Components 互補而非衝突。選擇何種方案應取決於你的應用需求規模與安全/整合考量。不過,隨著瀏覽器對 Web Components 的支援日趨成熟,它正成為嵌入各種前端模組的一項新標準解法。
測試、相容性與最佳實踐
封裝 Web Components 來嵌入外部元件,在開發過程中還需考慮一些實際應用層面的問題。我們將從測試、瀏覽器相容、無障礙、SEO,以及元件延伸性幾個方面來討論最佳實踐和建議。
測試策略
為自訂 Web Component 撰寫測試時,需要注意其生命週期和 Shadow DOM 特性。單元測試方面,可以直接透過 JavaScript 建立元件實例,調用其方法或模擬屬性變更,然後驗證 shadow DOM 結構是否符合預期。例如使用 Jest + Testing Library,可以掛載 <youtube-player> 元件到 DOM 中,然後檢查它的 shadowRoot 裡是否生成了 iframe,src 屬性是否正確等。需要注意一般的 DOM 模擬(如 JSDOM)對 Shadow DOM 支援有限,如果測試涉及 shadowRoot 的操作,可能需要在測試環境載入 @webcomponents/webcomponentsjs 的 polyfill,或者直接使用真實瀏覽器環境。
端到端測試(E2E)對於 Web Components 更為直接可靠,可以使用 Cypress 或 Puppeteer 等工具,在瀏覽器中真的渲染頁面,再檢查元件的行為。例如可以測試 <chart-widget> 是否最終渲染出了圖表的 canvas,或點擊某個嵌入內容是否正確反應。由於 Web Components 本質上是在瀏覽器中運行的原生元素,使用瀏覽器進行測試可以覆蓋大部分情況。
撰寫測試時也要留意非同步行為。像 <chart-widget> 會動態載入 Chart.js,在測試中需要等待圖表載入完成再斷言結果,可以透過監聽相關事件或 Promise 來實現。總之,在Web Components測試上,保持元件的低耦合會更容易測試——例如把核心邏輯封裝成純函式,元件只負責調用,這樣可以用純JS測試函式邏輯,而UI部分用簡單的DOM檢查即可。
瀏覽器相容性
好消息是,2025 年的今天,所有現代瀏覽器(Chrome、Firefox、Safari、Edge 等)都已經對 Web Components 提供了良好支援。 Custom Elements v1、Shadow DOM v1、HTML Template 等規範在幾年前已定稿並實作。然而,如果你需要支援較舊的瀏覽器(例如 Internet Explorer 11 或更舊),則必須引入官方提供的 polyfill。@webcomponents/webcomponentsjs 是一套常用的 polyfill,可以讓不支援原生 Web Components 的瀏覽器具備基本能力。不過這些 polyfill 會增加額外的腳本負擔,且IE等老舊瀏覽器本身性能欠佳,使用 Shadow DOM 等可能較吃力,因此需謹慎評估支援範圍。
Edge 情況:舊版的微軟 Edge(Edge Legacy,非Chromium內核)只部分支援 Web Components,但該版本已停止更新且市場佔比極低,可以視情況放棄支援。現代 Edge 基於 Chromium,與 Chrome 有相同的相容性。Safari 曾經在 Shadow DOM 實作上有些 bug(例如某些 CSS 選擇器支援問題),但隨著版本更新,大體已穩定。如果你的受眾主要包含蘋果設備,用最新的 Safari 測試一下你的元件仍是好習慣,確保沒有漏網問題。
總而言之,在大多數情況下,你不需要為 Web Components 的瀏覽器相容性太過擔憂——可以放心地運用這項技術於生產環境。但也建議在應用初始化時檢查 window.customElements 是否存在等方式,必要時載入 polyfill,以提升健壯性。
無障礙(Accessibility, a11y)
Web Components 封裝外部內容時,千萬不要忽視無障礙考量。雖然 Shadow DOM 封裝了內部實現,但輔助技術(如螢幕閱讀器)依然可以存取 shadow DOM 內的可見內容。因此,在元件內我們應當使用適當的語義化元素和 ARIA 標籤,確保元件對所有使用者友善。例如:
- 如果封裝影片播放器,務必給內部的 iframe 添加 title 屬性,以提供簡要描述(如 title="YouTube 影片播放器"),讓使用者知道這是什麼內容。
- 封裝圖表時,由於 Canvas 圖形對螢幕閱讀器而言是不可見的,有必要提供替代資訊。例如可以在 <chart-widget> 元件內或者附近加入隱藏的表格或文字摘要,說明圖表的數據重點。此外,Chart.js 本身不會自動處理無障礙,因此開發者要主動補充。
- 若元件需要用戶互動(例如點擊、輸入),請使用原生可聚焦的元素或者透過 tabindex 讓自訂元素可聚焦,並處理鍵盤事件。舉例來說,封裝一個自訂按鈕元件時,應在 constructor 裡設定 this.setAttribute('role', 'button') 以及必要的 tabindex,並監聽鍵盤空白鍵或 Enter 鍵來觸發點擊行為,模仿原生按鈕的無障礙行為。
- 使用 <slot> 投影內容時,也要留心無障礙:插槽內容應與周圍內容有正確的語意關聯。例如對話框元件的 <slot name="header"> 內容,應透過ARIA屬性關聯到對話框本體,以利輔助技術理解結構。
總之,封裝並不代表忽視可及性。我們要像對待平常的 HTML 元素一樣,確保自訂元件遵循無障礙規範。在這方面,MDN 等網站提供的 Web Component 無障礙指南值得參考。在實踐中,多用常規的 HTML 元素(如 <button>、<label> 等)放入 shadow DOM,而非用 <div> 貌似一切,將令你的元件在無障礙上贏在起跑點。
SEO 考量
關於 SEO,有一個早期的誤區是"Web Components 裡的內容搜尋引擎看不到"。這或許在幾年前部分正確,但如今情況已有改善。Google 等主流搜尋引擎已經可以執行頁面上的 JavaScript,並索引動態生成的內容。這意味著,只要你的 Web Component 在載入時將外部內容插入了 DOM(不論是在主DOM或影子DOM),Googlebot 理論上都能看見並索引其中的文字。舉例來說,如果你的 <embed-frame> 元件載入了一段含文字的內容,Google 的爬蟲在渲染階段也能取得那些文字。因此,Web Components 本身不會成為 SEO 障礙。
然而,需要注意幾點:
- 索引時間:搜尋引擎處理 JavaScript 內容需要額外的渲染時間,可能不如純靜態內容即時。因此重要的內容儘量在頁面初始載入時就渲染(或使用 Declarative Shadow DOM 等進階技術在 HTML 中直述元件內容)。如果外部嵌入內容非常關鍵,考慮在服務器端預取並渲染,以保證搜尋引擎一定能抓到。
- 不同引擎差異:Google 對 Web Components 支援最好,Bing、DuckDuckGo 等也陸續跟進。但仍建議測試各主要搜尋引擎的表現。例如曾經 Bing 的工具顯示 shadow DOM 未被渲染,不過官方表示實際索引有處理。因此最好查看實際的索引結果或使用搜尋引擎提供的站長工具檢查。如果發現某些引擎確實忽略了自訂元素的內容,可能需要備用方案。
- 語義與結構:確保你的自訂元件產生的內容有正確的 HTML 結構(heading 用 <h1>-<h6>、段落 <p> 等),搜尋引擎才能正確解讀。不要因為內容在 shadow DOM 就隨意使用不恰當的標籤。
- 鏈接可追蹤:如果嵌入內容包含連結,希望爬蟲跟蹤,務必使用真正的 <a href="...">,而不只是JavaScript 綁定點擊事件。確認連結在渲染後存在於 DOM 中,搜尋引擎才能抓取。
總的來說,使用 Web Components 不會讓你的網站自動喪失 SEO 能力。只要確保重要內容最後都有出現在頁面的 DOM 中(哪怕在 shadow DOM 裡),並遵循良好HTML結構,搜尋引擎大都能處理。如果仍有顧慮,可以考慮在構建時做預渲染或伺服端渲染(SSR)——例如使用一個 Node.js 腳本去跑過你的頁面,把 Web Components 渲染好的結果輸出為靜態 HTML,提供給爬蟲抓取。當然,這只有在非常必要時才需要這麼做。
延伸性與客製化
Web Components 作為獨立的 UI 元件,也需要考慮重用與延伸的能力。這裡提供幾項最佳實踐:
屬性與 Properties(Props):善用自訂元素的屬性,讓使用者可以透過 HTML 屬性或 DOM Properties 來配置元件行為。在元件類別中定義 static get observedAttributes() 監聽需要的屬性,實作 attributeChangedCallback 以在屬性變化時更新 UI。例如 <youtube-player> 就透過 video 屬性傳入影片ID。如果元件有複雜設定,可以考慮接受一個 JSON 字串(如 <chart-widget> 的做法)或多個獨立屬性。保持屬性介面的一致性和語義化,能使元件更易用、更容易被他人理解。除了 HTML 屬性之外,也可以提供 JavaScript API,例如 document.querySelector('youtube-player').play() 這樣的方法,以豐富元件操作方式——不過請務必在文檔中說明,並確保方法執行前元件已經初始化完成(或方法內部做好保護)。
Slots 插槽:前面已討論過,插槽提供了內容客製化的機制。如果你的元件需要用戶提供部分內容,那就使用 <slot>。例如一個封裝的對話框元件,讓開發者決定對話框內容,或一個 <user-card> 元件,可以允許插入 <img slot="avatar"> 來自定頭像。設計 slot 時要命好名、規劃好預設行為。當然,假如嵌入的外部元件本身不需要使用者傳入內容(比如 <google-map> 元件大多全由內部生成),則不強求 slot。但 slot 也可以用於模板擴充:例如我們的 <embed-frame> 可以在 shadow DOM 中預留一個 <slot name="loader"> 位置,用戶可以放入自定義的載入中提示動畫,在 iframe 加載時顯示。這種靈活性會讓元件更具專業水準。
樣式客製化:Shadow DOM 封閉了樣式,但有時我們希望開放某些造型讓使用者調整。一種方式是利用CSS Custom Properties(CSS變數)。例如 <user-card> 元件的 shadow DOM CSS 可以使用變數 --card-bg-color 來決定背景色,並給 :host 設定 background-color: var(--card-bg-color, #fff);,那麼使用者就可以在外部元素上透過行內樣式或CSS設定 user-card { --card-bg-color: #f0f0f0; } 改變其外觀。另一種方式是透過 ::part 或 ::theme 等標準,將 shadow DOM 某些元素的樣式開放為「部件」。需要在元件內元素上設置 part 屬性,例如 <button part="close-btn">,然後外部就可以寫 user-card::part(close-btn) { color: red; } 來定義關閉按鈕的樣式。這些機制能在保留封裝優點的同時,提供可樣式化的掛鉤,讓你的元件不會顯得太封閉僵硬。
元件擴充:Web Components 也支援透過 ES6 class 的繼承來擴充。比如你已經有一個 <base-modal> 元件,處理了對話框的通用邏輯,你可以繼承它創建 <alert-dialog> 元件,專門用於簡訊息提示。這樣可以重用大部分實作,只改動部分行為。雖然繼承在 Web Components 不如在類別庫/框架裡常見,但對於一些通用模式,是可以考慮的。同時,由於自訂元素本質上也是 HTMLElement,理論上也可以擴充內建元素(所謂 Customized built-in elements),但瀏覽器支援度不如 Autonomous Custom Element,應用相對少,我們此處不深入討論。
最後提醒,設計一個優秀的 Web Component 和設計傳統 UI 元件一樣,需要平衡封裝與彈性。過度封裝會導致無法應對特殊需求,而過度暴露細節又失去封裝意義。在實踐中,多與使用者(團隊成員或開放社群)交流,了解他們使用你的元件時希望可以調整哪些方面,進而完善元件的API。隨著經驗增長,你會發現 Web Components 可以打造出與主流框架元件媲美的體驗,而那些良好的模式(如單向資料流、組件通信等)其實在原生環境下一樣適用。
常見問與答
最後,我們彙總一些初學者在實作 Web Components 封裝時常見的疑問,並給出解答,希望能澄清一些迷思,為你的開發之旅掃清障礙。
問:所有現代瀏覽器都支援 Web Components 嗎?還需要額外的 Polyfill 嗎?
答:幾乎所有現代瀏覽器(Chrome、Firefox、Safari、Edge(Chromium)等)都已完整支援 Web Components 標準,包括 Custom Elements v1、Shadow DOM v1 和 HTML Template。你可以在這些瀏覽器中放心使用自訂元素而不需任何額外設定。如果你的專案不再考慮 IE11 或舊版 Edge,那基本不需要 polyfill。如果需要支援極少數不支援的環境,可以引入官方提供的 @webcomponents/webcomponentsjs。但整體而言,如今 polyfill 的需求已大幅降低。開發時建議在主要瀏覽器上測試你的元件功能,確保沒有瀏覽器實作差異導致的問題。
問:Web Component 裡的內容對 SEO 友好嗎?搜尋引擎抓得到 shadow DOM 裡面的文字嗎?
答:大體上,SEO 不是問題。Google 等搜索引擎已具備解析 Web Components 的能力。只要你的元件在載入時會把內容插入 DOM(無論 light DOM 或 shadow DOM),Google 的爬蟲在渲染階段都能取得並索引這些內容。例如你封裝了一個元件會產生一些列表項目,只要生成後出現在頁面上,Google就會把這些文字視為頁面內容。需要注意的是,如果內容對 SEO 極其重要,最好避免懶載入過慢或用腳本延遲很久才插入,否則爬蟲可能來不及看到。或者可以考慮使用 Server-Side Rendering 或預渲染,直接把元件展現出的重要內容寫進初始 HTML,提高抓取效率。總之,Web Components 本身不會阻礙 SEO,但你的實作方式(是否及時渲染內容、內容是否語義化)依然很關鍵。
問:封裝外部第三方內容,用 Web Component 真的比較安全嗎?會不會有安全風險?
答:這要視情況而定。Web Component 的 Shadow DOM 能隔離樣式和結構,但不能隔離腳本的執行環境——也就是說,嵌入的第三方程式碼其實仍在你的頁面中運行,擁有與你頁面腳本同等的權限。如果你完全不信任那段外部程式碼(例如來源可疑的Widget),僅用 Web Component 封裝並不能防止它偷取資料或改動頁面。這種情況下,iframe + sandbox 仍是較安全的方案,可以限制第三方程式碼的作亂範圍。不過,在多數常見應用(如嵌入知名服務的內容,或你自己團隊的模組)中,Web Components 的安全性體現在控制力上:你掌握外部內容的載入時機、可以過濾或包裝輸入輸出,以及可輕鬆拆掉整個元件而不留殘餘。因此,用 Web Component 封裝可信任或半信任的第三方內容是合理的折衷,它比直接插 script 更有掌控,但若涉及不信任的來源,記得評估額外的安全措施(如Content Security Policy、sandbox屬性等)。
問:我已經在用 React/Vue/Angular 等框架了,還需要關注 Web Components 嗎?它們能一起工作嗎?
答:Web Components 與框架並不衝突,它是一種框架無關(framework-agnostic)的技術。事實上,各大框架都能和 Web Components 共存:你可以在 React 應用中直接使用自訂元素,就像使用原生DOM元素一樣(需注意在較老版本 React中可能要透過 React.createElement 明確定義自訂元素屬性,但新版本基本開箱即用)。Angular、Vue 也可以使用或封裝 Web Components,例如 Angular 有提供將元件導出為自訂元素的選項。Web Components 更多時候扮演跨框架橋樑的角色:比如一組 UI 元件庫可以用 Web Components 編寫,因此無論你的專案是用哪種框架,都能引入並使用這些元件,而不需要為不同框架分別維護多套實現。從長遠看,學習 Web Components 對前端開發者是有幫助的,就算你現階段主力使用某框架,它也能彌補框架的不足(例如在不方便引入整個框架的獨立頁面上,用 Web Components 製作小型互動模組)。總之,Web Components 是瀏覽器原生支持的標準,值得投入學習,未來你會發現它在很多場景下帶來便利。
問:Web Component 如何和外部頁面或其他元件通信?比如怎麼把數據傳進傳出?
答:Web Components 與外界通信主要通過屬性/Properties和事件。傳入數據時,可以像對普通元素設置屬性一樣給自訂元素賦值,或透過 JavaScript 獲取元素實例後設置其 property。例如 <my-widget data="123"> 或 myWidgetElement.data = 123。在元件內部,你可以實作 attributeChangedCallback 去響應屬性變化,或直接監聽 property 的 setter(自行定義)。傳出數據時,最佳做法是派發自訂事件:利用 this.dispatchEvent(new CustomEvent('事件名', { detail: ...})) 從元件內發出事件。頁面上可以透過 addEventListener 監聽該事件拿到 detail 裡的內容。這種模式類似於框架中的 props down/events up。以封裝的影片播放器為例,如果想通知父頁面「影片播放結束」,元件可以在偵測到結束時 dispatchEvent(new CustomEvent('video-ended')),父層就能捕捉。除了事件,父層也可以直接呼叫子元件的方法(前提是先取得元素)。例如 document.querySelector('youtube-player').pause(),前提是元件對外暴露了一個 pause 方法。總結:屬性和事件是主要溝通橋樑,遵循這套約定,你的元件與應用可以解耦且順暢地互動。
問:Shadow DOM 隔離了樣式,那我能不能從外部改變 Web Component 的外觀?
答:Shadow DOM 確實隔離了大部分 CSS,但並非完全不可調整。Web Components 提供了幾種途徑讓外部影響其樣式:其一是使用 CSS Custom Properties(CSS變數)。元件內部的CSS可以引用變數,如 color: var(--my-widget-text-color, black);,這樣外部頁面可以針對 my-widget 設定 --my-widget-text-color 來改變其文字顏色。其二是使用 :host 和 :host-context 選擇器,元件的 shadow CSS 可以因應宿主在不同環境下設定不同樣式,比如 :host([dark]) { ... } 讓外部透過加一個dark屬性切換深色主題。其三是 ::part 機制,如果元件內某元素有設定 part="名稱",外部就可用 元素名::part(名稱) 選擇到它並套用樣式。例如 <my-button> 的按鈕元素 part 名為 "button",則可以寫 my-button::part(button) { background: red; } 改變樣式。當然,前提是元件作者有暴露這些 parts。總之,外部無法像操作一般 DOM 那樣自由選擇 shadow DOM 裡的元素來寫CSS,但透過這些受控的接口,仍可以實現客製化風格。如果你是元件作者,應提前思考哪些樣式可能需要開放給使用者調整,然後運用上述手段實現。這樣既保留封裝性,又給了使用者足夠的靈活度。
問:學習並使用 Web Components 值得嗎?感覺很多項目還是用框架比較多。
答:值得!Web Components 代表了瀏覽器原生的組件化能力,長期來看它是前端生態重要的一環。首先,它跟任何框架無關,學會了可以通吃各種技術棧,降低日後項目遷移或整合的成本。其次,Web Components 非常適合打造可分享的元件庫或設計系統:你可以封裝一套 UI 元件給不同項目使用,不需要每個項目都安裝同一個框架才能用,哪怕一個簡單靜態頁面也能直接拿來用。再次,隨著標準推進,Web Components 的開發體驗也在提升,例如有Lit這樣的輕量庫幫助更簡易地撰寫元件,但底層仍是原生的標準。如果擔心瀏覽器支援問題,前面我們提到了,現在已不是大問題。當然,並非要你拋棄現有框架,而是將 Web Components 作為多一種工具:在適合的場合使用原生元件,在其他場合用框架,各展所長。投資學習 Web Components 將拓寬你的技術視野,讓你更理解瀏覽器底層是如何運作的,寫出更高效、可維護的前端代碼。隨著社群對 Web Components 的接受度提高,你掌握這項技術將使你走在趨勢之前。祝你在未來的開發中玩轉 Web Components,構建出優雅的外部嵌入元件封裝!