這是 kiho 的第一篇開發日誌。
kiho 是利用業餘時間做的一個瀏覽器擴充套件,目的是幫助我一邊閱讀、一邊學習英文。 而實際上,kiho 也已經發佈了第一版到 Chrome Web Store,因此這第一篇日誌更像是到目前為止的回顧。
除了每天使用 kiho 學習英文之外,也根據自己的回饋持續改版升級。 但因為一些發佈上正在等待的問題,以及一些其他歷史原因,讓我想開始這一系列的開發日誌來記錄一些細節、進度。 包括:怎麼會有它、怎麼到今天,甚至之後可能要往哪裡去,讓之後的回顧可以有清楚的脈絡。
我很喜歡寫這些紀錄 ... 生活上的、工作上的、雜七雜八的瑣事,包括我自己的無知,希望自己能在 AI 趨勢下繼續保持書寫的能力,並享受這個過程。如果剛好能夠幫助到你,那就太好了。
或者你發現了錯誤,也歡迎隨時聯絡我,可以教我一些正確的方式。
由來
kiho 取自我的母語,台灣的台語用語:「kì-hō」(記號) - 一個清楚標示、讓你日後能一眼認出的記號,每個你查過的字都在頁面上留下一個記號,下次再回到同一頁,記號就在那裡。
其實在 kiho 之前,有另一個相同目的的專案:LinguaLens:
- Lingua:拉丁文,意為「語言」
- Lens:透鏡、鏡頭
合起來就是 - 透過一層語言的鏡片去看網頁。
那是一個依照官方教學打造的第一個擴充套件,也是我最早的原型。 但是一些原因 ( 更多是自己的無知 ) 最終沒有持續下去。
問題
我的母語不是英語,對於英語也沒有特別的熟悉與道地。但因為工作與興趣的關係,每天都會需要經常瀏覽英語的網站、資料、文件以及相關的影片等等。遇到不會的單字、片語字詞,在過去的流程裡大概就是:
- 選擇單字、片語字詞
- 讓 Chrome Google 翻譯、貼到 Claude Chat 裡,請 AI 分析、解釋
- 複製到筆記裡、做整理
- 切回原文,重新找回剛剛讀到哪
但不管是透過什麼工具,這個流程一直有問題,因為:
- 上下文切換打斷了閱讀。
- 字詞記錄在筆記裡,會需要回到筆記裡查找。
- 經常重複筆記相同的字詞。
- 查過的字散落在一堆 AI 對話裡,什麼都累積不下來。
- 幾週後重讀同一篇文章時,根本不記得哪些字已經查過。
- 更多時候,需要查詢的字詞需要包含上下文,才能讓 AI 提供更準確的解釋。
- Google 翻譯的品質通常都不太符合當下的文意。
- 我要的不是另一個全文翻譯。
- 需要手動將這些字詞建立到 Anki 等工具裡,以便安排複習時間。
而且,下次再瀏覽同一個頁面時,有很大機率會重複一次流程 (因為我又忘了上次的字詞)。
所以,我需要一個符合自己的方式來收斂這個流程。
選擇
先把目前的技術選擇都列出來:
- WXT — 擴充框架。它最後編譯出來就是一個純粹的 Manifest V3 擴充。
- React 19 + TypeScript - 負責所有 UI。
- Tailwind CSS v4 + shadcn/ui - 做元件。
- Playwright - 做 end-to-end 測試。
我是一個後端工程師,對於前端的技術選型並沒有非常的深入,所以如果硬要說為什麼這麼選擇,大概是:
- 不希望在不同的瀏覽器之間重複建立擴充,當然 Safari 應該是不大可能。
- 我跟 Claude 一起協作,需要一個 Claude 最熟悉的技術。
- 我理解一些 Tailwind CSS,也希望採用 shadcn/ui 的設計,因為真的很好看。
Shadow DOM
回顧第一個功能,也是自己最無知的部分。
kiho 會把 UI 畫在正在閱讀的網頁上面 - 劃選字詞時,會浮出一個小小的「查詢」按鈕,還有一張顯示解釋的 hover 卡片。
天真的我,認為只要在擴充裡捕捉這個事件,並接著將擴充的 UI 畫在網頁上就好了。
但實際上遭遇到的是 - 反覆調整 UI 依舊無法達到預期的結果。這也是第一版擴充:LinguaLens 失敗的主要原因。 後來才理解到,注入 UI 最明顯的風險就是 CSS。每個網頁有它自己的樣式,有些頁面能把精心做的小 tooltip 弄成一團糟。反方向也一樣重要 - 因為我不會希望 kiho 的 CSS 外洩出去、把別人的文章排版弄得亂七八糟。
標準解法是 Shadow DOM:把 UI 渲染在一個 shadow root 裡,它會封裝樣式,讓網頁原本的樣式進不來、我的樣式也不會洩漏出去。WXT 剛好給了一個 helper 做這件事 - createShadowRootUi,kiho 用的就是它。
樣式就這樣隔離了。
Font
為了讓自己看這些解釋字詞時,眼睛可以相對舒適,所以我選擇了兩個 web font:
- Inter - 用於顯示 UI 元件的字型
- Source Serif 4 - 閱讀文字
但不知道為何,一直沒有被正確地載入,UI 一直使用系統的字型,沒有任何錯誤或是警告。
後來才理解到,宣告在 shadow root 裡面的 @font-face 規則,會被 Chrome 忽略。 字型載入是對著 document 解析的,不是對著 shadow tree,所以一個只活在 shadow root stylesheet 裡的 @font-face 永遠不會被註冊。
標準解法是:把字型宣告在 document.head 上,再從 shadow root 裡面用字型家族名稱去引用它。
除此之外,是 MV3-specific 的問題。
@font-face 規則裡的 src 不能像一般網頁那樣直接指向字型檔,page context 碰不到擴充的 bundled URL:
- vendored 那些字型檔住在擴充裡
origin:chrome-extension://<extension-id>/fonts/...。 - 但 @font-face 的 CSS 是掛在網頁的 document 上,例如 https://example.com。瀏覽器要載入 src: url(...) 時,是 站在 page context 去解析,不是站在擴充的情境。
那要如何取得?需要透過 browser.runtime.getURL(...) 解析,而且 URL 要能從頁面載入,字型檔就必須在 manifest 裡被宣告成 web_accessible_resources:
// wxt.config.ts
web_accessible_resources: [
{ resources: ['fonts/*.woff2'], matches: ['*://*/*'] },
],接著,在每個頁面把字型 face 註冊到 document 層,指向 web-accessible:
const interUrl = browser.runtime.getURL('/fonts/inter-latin.woff2');
const style = document.createElement('style');
style.id = 'kiho-overlay-fonts';
style.textContent = `
@font-face {
font-family: 'Inter Variable';
font-weight: 100 900;
font-display: swap;
src: url('${interUrl}') format('woff2-variations');
}`;
(document.head ?? document.documentElement).appendChild(style);持久化
雖然在理解 Shadow DOM、Overlay、Font 或是 MV3-specific 時花了很多時間,也一直在鬼打牆。但真正難的並不是在這些地方。因為這些是屬於開發一個擴充功能需要理解的部分。
kiho 的主要功能,也是我認為值得做的事,是 持久化。
一個能夠跨造訪而留下來的標記、記號,不會因為這次關閉瀏覽器、頁面後,下次再次開啟相同頁面時,就失去當初學習的記號。 又因為這些記號不會不見,就可以加上重複複習的能力,讓整個學習流程完整。
當然,無知的我再次認為,只要存到 storage 就可以了。
但是當標記了一個字後,kiho 必須有辦法在下次打開這一頁時,重新找到同樣那一段文字,只是:
- 這一頁可能已經變了。
- DOM 位移了。
- 這個站是會隨著導覽重新渲染的 single-page。
- 內容是後來才載入的。
- 文字躲在另一個 shadow root 裡。
要在這些狀況下都能穩定地重新找到那一段,才是 kiho 真正的核心,也是真正解決我自己的痛點。因此,我的基本原則就是:
寧可不要顯示,也不要標錯
一個安靜沒有出現的標記,可能只是一點小失望;但一個接錯位置的標記 - 底線畫到錯的片語、把上週的筆記顯示在一個根本不屬於它的句子上時,就跟我自己在筆記裡重複紀錄相同的字詞是一樣的結果:混亂。所以每一層都被刻意做成寧可回傳找不到,也不去用猜的。
Claude Code
不懂前端工程的我,主要開發的方式是透過與 Claude Code 協作。透過 Claude Code,我可以快速地獲得解決問題的建議,並且在需要時進行調整。
我建立了一個小團隊:Orchestrator、Engineer、QA 以及 Reviewer。他們各司其職,並遵循一套規範:Karpathy 風格的品質與安全審查、刻意的 harness 設計、有限的迴圈上限、主動升級問題,以及把每一次的經驗與過程留存到文件裡。
作為後端工程師,在這個過程裡我不再扮演親手寫程式的角色,我轉移到寫 Spec、判斷與決策,並且對那些我完全沒有搞懂的部分負責。 就像上面提到的:Shadow DOM、Font、MV3 等等,我不需要事先知道它們,但我需要認得出那個修法是對的、值得留下來。
結語
kiho 的目標是成為一個能夠跨造訪而留下來的學習工具,讓學習過程更加順暢、完整。
這是第一篇開發日誌,因此跳過了一些主題 - 複習流程、繁體中文(台灣用語)規則、安全性等等,期許自己會在這系列日誌裡,逐一地記錄下來。