跳轉到主要內容
CRXJS 是給 Chrome 擴充功能使用的 Vite plugin。如果你已不再侷限於只支援 Chrome、想要一流的 Firefox 輸出,或被重新載入循環的小毛病困擾,本指南會在不重寫 UI 程式碼的前提下,把典型的 CRXJS 專案遷移到 Extension.js。
你來到這頁是因為在 Vite 8 上執行 vite build 出現 [crx:manifest-post] Content script fileName is undefined 嗎?在遷移前,請先參考專門的修正頁的解法。

哪些會改、哪些不變

**不變的部分:**你的 React/Vue/Svelte 元件、Tailwind 設定、測試、chrome.* API 呼叫。 會變的部分:
  • vite.config.ts + @crxjs/vite-plugin 變成 extension.config.js(或完全不需要設定)。
  • manifest.config.ts(TypeScript 模組)變成純 manifest.json,搭配瀏覽器前綴鍵
  • vite / vite build 腳本變成 extension dev / extension build
  • Extension.js 的 dev 取代 Vite dev server,內建瀏覽器啟動、設定檔管理與各目標的重新載入。

步驟 1:安裝 Extension.js

npm install extension@latest --save-dev
npm uninstall @crxjs/vite-plugin vite
如果你的專案還會把 Vite 用在非擴充功能的範圍(例如行銷網站),請把 Vite 安裝侷限在那個工作區。

步驟 2:轉換 manifest

CRXJS 使用 TypeScript 模組:
manifest.config.ts
import { defineManifest } from "@crxjs/vite-plugin";
import pkg from "./package.json";

export default defineManifest({
  manifest_version: 3,
  name: pkg.name,
  version: pkg.version,
  action: { default_popup: "src/popup/index.html" },
  background: { service_worker: "src/background/index.ts" },
  content_scripts: [{ matches: ["<all_urls>"], js: ["src/content/index.ts"] }],
});
在 Extension.js 中,改寫一份真正的 manifest.json,並參考實體檔案:
manifest.json
{
  "manifest_version": 3,
  "name": "my-extension",
  "version": "1.0.0",
  "action": { "default_popup": "popup/index.html" },
  "background": { "service_worker": "background/index.ts" },
  "content_scripts": [{ "matches": ["<all_urls>"], "js": ["content/index.ts"] }]
}
注意事項:
  • manifest 中的副檔名仍是 .ts/.tsx。Extension.js 會在建置期編譯它們。
  • 如果你把目錄扁平化,可以省略 src/ 前綴。Extension.js 會遵從你在 manifest 中宣告的路徑。
  • 對於瀏覽器專屬的值,請使用前綴鍵(firefox:browser_specific_settings),而不是為每個建置寫一份 manifest。

步驟 3:更新 package.json 腳本

把這個:
{
  "scripts": {
    "dev": "vite",
    "build": "vite build"
  }
}
換成:
{
  "scripts": {
    "dev": "extension dev",
    "build": "extension build",
    "start": "extension start"
  }
}
完整指令請見指令參考

步驟 4:移除 vite.config.ts

如果你的 vite.config.ts 只是為了串接 CRXJS,請直接刪除。如果它還有其他 plugin,請把對應功能移到 extension.config.js,或到 Rspack 設定 做進階的打包工具客製。

步驟 5:處理 HMR 差異

CRXJS 的 HMR 透過 Vite WebSocket 推送更新。Extension.js 使用重新載入與 HMR 中說明的另一套模型:
  • Popup、options、devtools 頁面:HMR。
  • Content script:定向重新載入。
  • 背景 service worker:完整重啟。
如果你的程式碼依賴 Vite 的 import.meta.hot 在 content script 中執行邏輯,請用一般模組程式碼取代那些分支。Extension.js 會在你的原始碼之外處理重新載入的協調。

步驟 6:跨瀏覽器輸出

CRXJS 鎖定 Chromium。要同時輸出 Firefox:
extension build --browser=chrome,firefox --zip
你會得到 dist/chromedist/firefox,各自帶有瀏覽器正確的 manifest,以及可上架 Chrome Web Store 與 addons.mozilla.org 的 .zip 壓縮檔。詳見跨瀏覽器相容性

步驟 7:驗證

extension dev --browser=chrome
驗證 popup、options、content script 與背景行為。接著驗證 Firefox:
extension dev --browser=firefox
如果你的程式碼呼叫 chrome.*,而你希望同一份原始碼在 Firefox 上也能執行而不用修改,請加上 --polyfill

常見地雷

  • service worker 的 import 陳述句: 當背景進入點使用 ESM 語法時,Extension.js 會自動設定 type: "module"。CRXJS 也是這樣做。不必額外處理。
  • web_accessible_resources 的型別: Manifest V3 使用 [{resources, matches}] 區塊。兩個框架都會輸出正確結構;直接照搬現有項目即可。
  • 帶 hash 的資產路徑: Extension.js 在 dist/<browser> 下使用穩定路徑。如果你的程式碼硬寫了 Vite 風格的 hash 檔名,請改用 manifest 相對路徑。

延伸閱讀