開發紀錄
替換 UI 元件庫時,先保住頁面的使用契約
從本站的共用元件層、Button 與 Drawer 事件轉接,說明更換底層元件時,如何分清頁面依賴、相容層責任與仍需要實際操作的驗證。
- 元件設計
- Astro
- Starwind UI
- 重構
一個按鈕換成另一套元件後,外觀可能幾乎一樣。頁面真正依賴的卻不只有外觀:它可能要輸出連結、預設不能送出表單,也可能透過資料屬性成為 Drawer 的觸發器。
如果每個頁面都要知道新套件的內部命名,替換就會一路擴散到消費端。之後再換一次底層,又得重新確認同樣的細節。
本站目前在頁面與 Starwind 的 source/runtime 之間,保留一個網站自己持有的共用元件層,作為頁面使用元件的固定入口。以下依據 2026 年 10 月 24 日的使用點,說明這層邊界保護什麼,以及它不能單獨證明什麼;不是一份把所有元件換掉的通用步驟。
頁面先依賴網站的介面,再由介面選擇底層
這個共用元件層提供網站自己的介面。頁面從網站的 ui/button 或 ui/drawer 引用元件,不直接依賴 Starwind 的 source 目錄或 runtime。
可以把關係簡化成:
頁面與文章版型
→ 網站的共用元件層
→ 本地 variants、Starwind primitive 與 runtime
這不表示每一層都必須再包一個相同的元件。這個共用元件層裡,有的元件直接組合 primitive,有的則使用本地 variants,再保留原生 HTML 的輸出。重點是外部依賴在哪裡被解讀,而不是 wrapper 有幾層。
也要分清楚保留什麼契約。props 名稱、預設值、slot、原生標籤與 data-slot,都可能已被頁面或腳本使用;只保留相同的 import 名稱,還不能叫相容。
Button 的例子:同名元件,不代表同一份介面
本站的共用 Button 支援 as="button"、as="a" 和 as="summary"。button 分支預設 type="button",表單需要送出時再明確指定 type="submit";連結分支則輸出真正的 anchor。
它也把 variants 與呼叫端的 class 合併,保留 data-slot。當 data-slot 被指定為 Drawer 或 Popover 的 trigger/close,這個元件會補上底層 runtime 辨識的對應資料屬性。
所以,這個入口不是單純把上游的同名 Button 重新匯出。若換成另一份只支援 button 與 anchor 的元件,即使預設外觀相同,也可能遺失 summary 或 overlay 的使用方式。
Button 外觀與 HTML 語意解釋的是元素應該如何選;這裡要保護的是,頁面已經作出的選擇,不應在替換底層時被偷偷改掉。
事件也是契約,不只是一串可以改名的字
文章目錄的腳本會向 Drawer 發出 drawer:set,以 { open: false } 要求關閉;它也監聽 drawer:change 和 drawer:change-complete,在狀態與關閉完成時安排更新。
目前的相容層把這些網站端事件接到 Starwind controller。收到 drawer:set 時,先確認 detail.open 是布林值,再呼叫 setOpen;runtime 的 openChange 與 closeComplete,則分別轉成網站原本監聽的事件。
以實際責任對照,關係是:
| 網站端的依賴 | 相容層目前處理的事 |
|---|---|
發出 drawer:set |
檢查 detail.open 後要求 controller 切換 |
監聽 drawer:change |
轉送 openChange 的狀態資訊 |
監聽 drawer:change-complete |
轉送 closeComplete 的完成資訊 |
| 事件來源欄位 | 將 runtime 的 event 對應為 originalEvent |
「要求關閉」「狀態改變」與「關閉完成」不是同一個時刻。不能因為事件名稱相近,就把它們全部接在同一個 callback 上,假設頁面的後續處理不受影響。
這張表只描述目前列出的轉接,不保證所有事件的冒泡、取消或時序都完全等價。相容層另有開關意圖的轉接;涉及取消行為時,還要沿對應路徑確認,不能從一個 CustomEvent 的存在推導全部相容性。
樣式映射、事件轉接與互動驗收是三種工作
共用色彩可以讓新元件維持網站表面,variants 可以維持按鈕外觀,相容層可以承接事件。這幾件事都不會自動證明焦點、Escape 或外部點擊仍依預期運作。
以文章目錄的 modal Drawer 為例,更換底層後仍應實際開啟,確認焦點進入、Tab 與 Shift + Tab、關閉後焦點目的地,以及 Escape 的處理。WAI 的 modal dialog pattern 把這些列為具體互動責任;關閉時一般回到觸發元素,也保留符合工作流程的例外。[5]
本站相容層還有 pagehide 的清理與 pageshow 的重新安裝。程式裡有這些分支,只能說明設計了生命週期路徑;要確認返回頁面後沒有重複監聽或失去控制,仍需要相應的瀏覽器情境,不能只看 source 就當成 BFCache 實測。
無腳本狀態也要獨立看。文章正文與原生連結應保持可讀,目錄另有 noscript 清單;這個內容退路,不等於 runtime 的互動元件在沒有 JavaScript 時仍以相同方式運作。
相容層要能說明自己承擔的差異
保留共用元件層,不是把所有舊行為永遠凍結。若頁面契約本身不合理,仍可以改,但應把它當成明確的介面變更,而不是藏在換套件的工作裡。
我會先選一個實際使用點,把它依賴的 props、HTML、slot、資料屬性和事件列清楚。替換後再分別確認輸出與操作:Button 的標籤和預設屬性,不能代替 Drawer 的焦點與關閉測試;示範頁成功,也不能代表每個頁面都接對了。
本地 UI 元件的維護責任談的是原始碼進入專案後如何照顧差異。這篇補上替換時的另一個問題:哪些差異應留在網站自己的入口,才不必讓每個頁面重新認識底層。
本文是目前契約與轉接的整理,沒有重新驗收全部 overlay、生命週期或第三方版本,也沒有量測替換節省多少工時。共用元件層讓差異有可追查的位置;當它只能靠越來越多特殊情況維持,就應回頭檢查頁面契約,而不是繼續把成本藏進包裝層。
Sources
[5] https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal — W3C WAI — Dialog (Modal) Pattern