跳至主要內容

開發紀錄

替換 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