niskan516
~/blog ~/projects ~/about
← ~/blog

2025-12-10

2025 年做瀏覽器擴充功能學到的事

用 WXT 和 SolidJS 做 Volume Hero 的筆記——擴充功能生態比你想的好多了。

Volume Hero 是我某個週日下午做的。一個瀏覽器擴充功能,可以把音訊和視訊音量提升至 600%,並記憶每個網站的設定。聽起來很簡單,實作不是。

為什麼擴充功能有趣

瀏覽器擴充功能在軟體堆疊中佔據一個獨特的位置。它們是網頁程式碼,卻對頁面、瀏覽器 API,甚至作業系統有更高的存取權限。它們像應用程式一樣發布——使用者安裝、持續運行、到處都能用。

大多數開發者忽視擴充功能作為一種創作媒介。這是個錯誤。

2025 年擴充功能開發的現狀

三年前,做一個跨瀏覽器的擴充功能很麻煩。Manifest V2 vs V3 的差異、browser vs chrome API 前綴、每個瀏覽器各自不同的工具鏈。大多數擴充功能樣板都很老舊。

然後 WXT 出現了。這是一個專為瀏覽器擴充功能設計的框架,處理了:

  • 預設使用 Manifest V3(可降級至 V2)
  • 開發時的熱重載
  • 單一程式碼庫交叉編譯到多個瀏覽器
  • TypeScript 開箱即用
  • 基於檔案的進入點自動發現

這就是 Vite 對網頁應用做的事,但換成了擴充功能。讓開發體驗真的變得愉快。

在 Popup 裡用 SolidJS

擴充功能的 UI 我用了 SolidJS。Popup 需要小、快、響應式——一個拖動時即時更新的滑桿。SolidJS 在這種情境下非常適合,它的細粒度響應性意味著零 VDOM 額外開銷。滑桿的更新感覺是即時的。

棘手的部分不是 UI——是 Web Audio API。

Web Audio API 很厲害

要把音量提升超過 100%,不能只是設定 element.volume = 2.0,那個上限是 1.0。必須透過 GainNode 路由音訊:

const ctx = new AudioContext()
const source = ctx.createMediaElementSource(element)
const gain = ctx.createGain()
gain.gain.value = 3.0 // 300%
source.connect(gain)
gain.connect(ctx.destination)

這對 HTML5 音訊和視訊元素有效。問題在於:一旦你把元素路由到 AudioContext,它就跟那個 context 綁在一起了。如果 context 被暫停(瀏覽器分頁隱藏、自動播放政策),音訊就會中斷。

正確處理這件事——偵測暫停、恢復 context、避免重複路由元素——才是大部分實際工作所在。

每個網站記憶設定

我最驕傲的功能是每個網站記憶設定。到 YouTube,調到 200%,離開。明天回來——還是 200%。

這其實就是一個以網域為鍵的 chrome.storage.sync 呼叫。簡單,但 UX 的差異是巨大的。它把擴充功能從手動控制變成了會學習你偏好的東西。

我會做什麼不同的事

我會更早搞清楚 Content Script 的隔離問題。擴充功能在多個環境中運行——背景 Service Worker、Popup、Content Script——它們之間的訊息傳遞是 bug 的溫床。從一開始就把架構弄對能省很多麻煩。

我也會寫測試。擴充功能測試很煩(需要 mock 瀏覽器 API),但音訊 context 管理的邏輯真的需要覆蓋率。

結語

如果你每天在瀏覽器裡手動解決某個問題,考慮做一個擴充功能吧。工具鏈比大多數人想的更成熟。WXT + SolidJS + TypeScript 是一個出乎意料扎實的技術組合。

Volume Hero 是開源的,在 github.com/sinhong2011/volume-hero 可以看源碼。