> ## Content Index
> Fetch the complete content index at: https://blog.vercanti.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# 浏览器插件-技术栈与脚手架
- URL: https://blog.vercanti.com/liu-lan-qi-cha-jian-ji-zhu-zhan-yu-jiao-shou-jia/
- Published: 2026-08-28T14:35:30.000Z
- Updated: 2026-08-28T14:58:58.000Z
- Description: 浏览器插件本质是「一组特殊上下文（context）的网页 + 一份清单文件 manifest.json」。manifest.json 把多个入口（popup 弹窗、options 选项页、side panel 侧边栏、content script 内容脚本、background/service worker 后台）声明给浏览器。普通前端构建工具默认面向「一个或多个 HTML 页面」，并不理解 manifest.json 里这些特殊入口与权限声明，也不会处理 content script 在目标页面的 CSS 注入、HMR（Hot Module Repla
- Author: yellowdog
- Tags: 前端开发, 浏览器插件开发

> \[!info\] 文档信息
> 
> - **主题**：浏览器插件（Browser Extension）开发的构建工具/框架选型与脚手架对比
> - **官方文档**：  
>  - CRXJS：<https://crxjs.dev/> · 仓库 <https://github.com/crxjs/chrome-extension-tools>
>  - WXT：<https://wxt.dev/> · 仓库 <https://github.com/wxt-dev/wxt>
>  - Plasmo：<https://docs.plasmo.com/> · 仓库 <https://github.com/PlasmoHQ/plasmo>
>  - vite-plugin-web-extension（aklinker1）：<https://vite-plugin-web-extension.aklinker1.io/>
>  - Extension.js：<https://extension.js.org/>
> - **核实日期**：2026-06-06（版本号来自 npm registry 实查）
> - **适用版本与最新版本号**：  
>  - `@crxjs/vite-plugin` **2.4.0**（`latest`），`beta` 标签停留在 `2.0.0-beta.33`（见下文“CRXJS 版本与维护状态”陷阱）
>  - `wxt` **0.20.26**（活跃维护，约每 1～2 周发版）
>  - `plasmo` **0.90.5**（约一年未更新，社区视为维护停滞/低活跃）
>  - `extension`（Extension.js）**3.18.2**
>  - `@samrum/vite-plugin-web-extension` **5.1.1**（约一年未更新）
>  - `vite-plugin-web-extension`（aklinker1）**4.5.1**（作者已宣布以 WXT 为后继，逐步弃用）
>  - `webextension-polyfill` **0.12.0**，`@webext-core/messaging` **3.0.2**
>  - 配套：Vite 5/6/7、Vue 3.5+、TypeScript 5.x、Manifest V3（MV3）

## 概述

浏览器插件本质是「一组特殊上下文（context）的网页 + 一份清单文件 `manifest.json`」。`manifest.json` 把多个入口（popup 弹窗、options 选项页、side panel 侧边栏、content script 内容脚本、background/service worker 后台）声明给浏览器。普通前端构建工具默认面向「一个或多个 HTML 页面」，并不理解 `manifest.json` 里这些特殊入口与权限声明，也不会处理 content script 在目标页面的 CSS 注入、HMR（Hot Module Replacement，热模块替换）跨上下文同步等问题。**插件脚手架/框架的核心价值，就是把 `manifest.json` 的生成、多入口编排、跨浏览器适配、开发期热重载这四件事自动化。**

**What（是什么）**：本文横向对比 6 种主流方案——CRXJS、WXT、Plasmo、vite-plugin-web-extension、Extension.js、以及「原生 Vite 多页 + 手写 manifest」，并给出 Vite + Vue 技术栈下 CRXJS 与 WXT 两套完整脚手架。

**Why（为什么需要选型）**：方案之间在「HMR 质量、多浏览器支持、manifest 管理方式、框架支持、学习曲线、维护活跃度」六个维度差异巨大，且部分老牌方案（Plasmo、CRXJS、aklinker1 的 vite-plugin-web-extension）正处于维护放缓或转向后继项目的状态。选错会导致项目中途被迫迁移。

**When（何时用哪个）**：

- 新项目、想要零配置全家桶、需要多浏览器（Chrome/Firefox/Edge/Safari）一套代码 → **首选 WXT**。
- 已有重度 Vite + Vue 工程、想保留对 `vite.config` 的完全掌控、只补一个插件 → **CRXJS**（但需接受其维护风险）。
- 团队主用 React、能接受 Parcel 生态 → Plasmo（仅维护已有项目，新项目慎选）。
- 只想要极简零配置、不挑框架的实验性项目 → Extension.js（较新，生态尚浅）。
- 学习底层原理、或插件极其简单（仅一个 background） → 原生 Vite 多页 + 手写 manifest。

---

## 一、构建工具/框架横向对比

### 1.1 总览对比表

| 方案                                       | 底层                           | manifest 管理               | HMR 质量                                  | 多浏览器                             | 框架支持                            | 学习曲线           | 维护活跃度（2026-06）    | 适用场景                  |
| ---------------------------------------- | ---------------------------- | ------------------------- | --------------------------------------- | -------------------------------- | ------------------------------- | -------------- | ----------------- | --------------------- |
| **WXT**                                  | Vite                         | 文件约定自动生成 + wxt.config     | 极佳（UI 即时 HMR，脚本快速重载）                    | Chrome/FF/Edge/Safari/全 Chromium | 任意（官方模块：Vue/React/Svelte/Solid） | 低（约定优于配置）      | **高**，发版频繁        | 新项目零配置首选、多浏览器         |
| **CRXJS**                                | Vite                         | manifest.config.ts（TS 编写） | 极佳（原生 HMR，content script 也热重载）          | Chrome 为主，FF 实验性                 | 任意（沿用 Vite 插件）                  | 中（需懂 Vite 多入口） | **中/不稳定**，曾寻找新维护者 | 已有 Vite 工程、要掌控 config |
| **Plasmo**                               | Parcel                       | 文件约定 + package.json 字段    | 良好                                      | Chrome/FF/Edge/Safari            | React 为主，Vue/Svelte 次之          | 中              | **低**，约一年未更新      | 维护已有 React 插件         |
| **vite-plugin-web-extension**（aklinker1） | Vite                         | 指向 manifest.json（可 .js）   | 良好                                      | Chrome/FF                        | 任意                              | 中              | **转维护**，作者推荐迁 WXT | 旧项目，新项目改用 WXT         |
| **@samrum/vite-plugin-web-extension**    | Vite                         | 传入 manifest 对象            | 无内置 HMR（需手动重载）                          | Chrome/FF                        | 任意                              | 中              | **低**，约一年未更新      | 不推荐新项目                |
| **Extension.js**                         | 自带（基于 Rspack/webpack 思路，零插件） | 文件约定 + 自动适配               | 良好（background/content/popup HMR）        | Chrome/Edge/FF                   | TS/React/Vue/Svelte/Preact      | 低（零配置）         | 中（较新，迭代快）         | 极简零配置、实验项目            |
| **原生 Vite 多页 + 手写 manifest**             | Vite                         | 完全手写 manifest.json        | 差（content script/background 需手动 reload） | 自行处理                             | 任意                              | 高（全部自管）        | 取决于自己             | 学习原理、极简插件             |

### 1.2 同名生态辨析：两个 `vite-plugin-web-extension`

这是新手最易混淆的点，两个包名相似但归属不同：

| 维度     | @samrum/vite-plugin-web-extension          | vite-plugin-web-extension（aklinker1） |
| ------ | ------------------------------------------ | ------------------------------------ |
| 作者     | samrum                                     | aklinker1（WXT 作者本人）                  |
| npm 包名 | @samrum/vite-plugin-web-extension（带 scope） | vite-plugin-web-extension（无 scope）   |
| 配置方式   | 在 vite.config 传入完整 manifest 对象             | 指向一份 manifest.json/.js，自动多浏览器构建      |
| 开发体验   | 无内置 HMR，改完手动 reload                        | 自动开独立浏览器 profile，集成 web-ext 运行       |
| 当前定位   | 维护放缓（最新 5.1.1，约一年前）                        | 作者已声明 **WXT 是其后继**，逐步弃用              |
| 结论     | 不建议新项目                                     | 旧项目可用，新项目直接上 WXT                     |

> \[!warning\] aklinker1 同时是 WXT 与 `vite-plugin-web-extension` 的作者  
> WXT 已支持 `vite-plugin-web-extension` 的全部能力且更流行、功能更多。aklinker1 官方建议新项目跳过该插件，直接用 WXT。

### 1.3 CRXJS 版本与维护状态（重点核实）

CRXJS 是 Vite + Vue 生态里历史最久、HMR 体验最好的插件，但其维护状态需要谨慎对待：

- npm `latest` 标签为 **2.4.0**，但 `beta` 标签停留在 **2.0.0-beta.33**。历史上 CRXJS 长期只发 `2.0.0-beta.x`，很多旧教程让你 `npm i @crxjs/vite-plugin@beta`——**今天 `@beta` 反而比 `@latest` 旧**，应直接装 `@latest`（2.4.0）。
- 项目曾公开「寻找新维护者」：原作者一度宣布若无新维护团队接手将归档，后由社区志愿者组成维护团队接手协调，目前仍在持续发版，但维护带宽不及 WXT 充裕。
- **决策含义**：CRXJS 适合「已经重度依赖自定义 `vite.config`、迁移成本高」的存量项目；纯新项目若不强需要对 config 的完全掌控，WXT 更稳妥。

### 1.4 选型决策树

```text
开始：要写一个浏览器插件
│
├─ 主用框架是 React 且只维护已有 Plasmo 项目？
│     是 → Plasmo（不为新项目引入）
│     否 ↓
│
├─ 需要一套代码同时上 Chrome + Firefox + Edge + Safari？
│     是 → WXT（多浏览器构建最省心）
│     否 ↓
│
├─ 已有成熟的 Vite + Vue 工程，且离不开自定义 vite.config？
│     是 → CRXJS（接受其维护风险，装 @latest 而非 @beta）
│     否 ↓
│
├─ 想要约定优于配置、零样板、官方 Vue 模块开箱即用？
│     是 → WXT（新项目通用首选）
│     否 ↓
│
├─ 插件极简（只有一个 background，无 UI），或想学底层原理？
│     是 → 原生 Vite 多页 + 手写 manifest
│     否 ↓
│
└─ 想试零配置新工具、不挑框架 → Extension.js

```

> \[!tip\] 一句话建议  
> **绝大多数 Vite + Vue 新项目选 WXT。** 强依赖既有 `vite.config` 的存量 Vue 工程选 CRXJS（装 `@latest`）。其余皆为特定场景。

---

## 二、各脚手架快速创建命令与目录结构

### 2.1 WXT

```bash
# 交互式创建（推荐），会询问框架（选 vue）与包管理器
npx wxt@latest init my-extension
cd my-extension
npm install
npm run dev          # 默认 Chrome；--browser firefox 切换

```

WXT 生成的目录（约定式 `entrypoints/`）：

```text
my-extension/
├── entrypoints/              # 入口约定目录，文件名/目录名即入口类型
│   ├── background.ts         # 后台 service worker（defineBackground）
│   ├── content.ts            # 内容脚本（defineContentScript）
│   ├── popup/                # 弹窗（含 index.html + main.ts + App.vue）
│   └── options/              # 选项页
├── components/               # Vue 组件
├── composables/              # Vue 组合式函数
├── assets/                   # 经构建处理的资源
├── public/                   # 原样拷贝的静态资源（图标等）
├── wxt.config.ts            # 唯一配置文件
├── package.json
└── tsconfig.json

```

### 2.2 CRXJS（结合官方 Vite Vue 模板）

```bash
# 先用官方模板拉一个 Vite + Vue + TS 工程，再加 CRXJS 插件
npm create vite@latest my-extension -- --template vue-ts
cd my-extension
npm install
npm install -D @crxjs/vite-plugin@latest   # 注意装 @latest，不是 @beta

```

CRXJS 推荐目录（手动组织，下文第三节给完整配置）：

```text
my-extension/
├── src/
│   ├── popup/      ├── options/   ├── sidepanel/
│   ├── content/    └── background/
├── manifest.config.ts        # 用 TS 定义 manifest（类型安全）
├── vite.config.ts
└── package.json

```

### 2.3 Plasmo

```bash
npm create plasmo my-extension      # React + TS 默认
# 选 Vue：使用社区 with-vue 示例模板
npm create plasmo --with-vue
cd my-extension
npm run dev

```

```text
my-extension/
├── popup.tsx                 # 文件名即入口：popup/options/newtab...
├── options.tsx
├── background.ts
├── contents/                 # 内容脚本目录
├── assets/
└── package.json              # manifest 字段写在这里

```

### 2.4 Extension.js

```bash
npx extension@latest create my-extension --template=vue
cd my-extension
npm run dev                    # 启动并自动打开浏览器加载

```

```text
my-extension/
├── manifest.json             # 手写但有自动浏览器适配
├── background.js
├── content/  ├── popup/  └── pages/
└── extension.config.js       # 可选，配置浏览器/启动命令

```

### 2.5 vite-plugin-web-extension（aklinker1）

```bash
npm create vite-plugin-web-extension@latest my-extension
cd my-extension && npm install && npm run dev

```

---

## 三、Vite + Vue + CRXJS 完整脚手架

### 3.1 manifest.config.ts（类型安全的 manifest）

```ts
// manifest.config.ts
import { defineManifest } from '@crxjs/vite-plugin'
import pkg from './package.json'

// defineManifest 提供完整 MV3 类型提示，避免手写 JSON 拼错字段
export default defineManifest({
  manifest_version: 3,
  name: pkg.name,
  version: pkg.version,              // 与 package.json 同步，发版只改一处
  description: pkg.description,
  icons: {
    16: 'public/icons/16.png',
    48: 'public/icons/48.png',
    128: 'public/icons/128.png',
  },
  action: {
    default_popup: 'src/popup/index.html',   // 弹窗入口
    default_icon: 'public/icons/48.png',
  },
  options_page: 'src/options/index.html',     // 选项页入口
  side_panel: {
    default_path: 'src/sidepanel/index.html', // 侧边栏入口（Chrome 114+）
  },
  background: {
    service_worker: 'src/background/index.ts', // 后台 service worker
    type: 'module',                            // MV3 推荐 ES module 形式
  },
  content_scripts: [
    {
      matches: ['<all_urls>'],                 // 注入的页面匹配规则
      js: ['src/content/index.ts'],            // 内容脚本入口
    },
  ],
  permissions: ['storage', 'sidePanel', 'activeTab'],
  // host_permissions 用于跨域请求目标站点
  host_permissions: ['https://*/*'],
})

```

### 3.2 vite.config.ts

```ts
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { crx } from '@crxjs/vite-plugin'
import manifest from './manifest.config'

export default defineConfig({
  plugins: [
    vue(),
    crx({ manifest }),       // 传入上面定义的 manifest
  ],
  server: {
    // CRXJS 的 HMR 依赖固定端口；端口冲突会导致 content script 无法热重载
    port: 5173,
    strictPort: true,        // 端口被占用时直接报错，避免随机换端口破坏 HMR
    hmr: {
      port: 5173,            // 显式锁定 HMR 端口（见“常见陷阱”）
    },
  },
})

```

### 3.3 多入口目录组织与示例

```text
src/
├── popup/
│   ├── index.html          # <script type="module" src="./main.ts">
│   ├── main.ts             # createApp(App).mount('#app')
│   └── App.vue
├── options/
│   ├── index.html  ├── main.ts  └── App.vue
├── sidepanel/
│   ├── index.html  ├── main.ts  └── App.vue
├── content/
│   └── index.ts            # 注入目标页面的逻辑
└── background/
    └── index.ts            # service worker 逻辑

```

```html
<!-- src/popup/index.html -->
<!doctype html>
<html lang="zh-CN">
  <head><meta charset="UTF-8" /><title>Popup</title></head>
  <body>
    <div id="app"></div>
    <script type="module" src="./main.ts"></script>
  </body>
</html>

```

```ts
// src/popup/main.ts
import { createApp } from 'vue'
import App from './App.vue'

createApp(App).mount('#app')

```

```ts
// src/background/index.ts
// service worker 中监听插件安装事件
chrome.runtime.onInstalled.addListener(() => {
  console.log('extension installed')
})

// 点击 action 图标时打开 side panel（需 sidePanel 权限）
chrome.sidePanel
  .setPanelBehavior({ openPanelOnActionClick: true })
  .catch((err) => console.error(err))

```

---

## 四、Vite + Vue + WXT 搭建对比

WXT 把上一节里 CRXJS 需要手写的 `manifest.config.ts`、多入口 HTML、HMR 端口配置全部用「文件约定 + 包装函数」替代。

### 4.1 wxt.config.ts

```ts
// wxt.config.ts
import { defineConfig } from 'wxt'

export default defineConfig({
  modules: ['@wxt-dev/module-vue'],   // 官方 Vue 模块，自动接好 @vitejs/plugin-vue
  manifest: {
    // 这里只写「文件约定无法表达」的字段；入口由 entrypoints/ 自动推导
    name: 'My Extension',
    permissions: ['storage', 'sidePanel', 'activeTab'],
    host_permissions: ['https://*/*'],
  },
  // 多浏览器：构建时用 --browser firefox 切换，manifest 自动适配 MV2/MV3 差异
})

```

### 4.2 entrypoints 约定 + defineBackground/defineContentScript

```ts
// entrypoints/background.ts
// defineBackground 包裹后台逻辑，WXT 据此生成 manifest.background 字段
export default defineBackground(() => {
  chrome.runtime.onInstalled.addListener(() => {
    console.log('installed')
  })
})

```

```ts
// entrypoints/content.ts
// defineContentScript 同时声明注入规则与逻辑，无需再到 manifest 手写 content_scripts
export default defineContentScript({
  matches: ['<all_urls>'],     // 注入页面匹配
  main() {
    console.log('content script injected')
  },
})

```

```text
entrypoints/
├── popup/
│   ├── index.html  ├── main.ts  └── App.vue   # 名为 popup → 自动设为 action.default_popup
├── options/
│   ├── index.html  ├── main.ts  └── App.vue   # 名为 options → 自动设为 options_page
├── background.ts                              # 自动 → background.service_worker
└── content.ts                                 # 自动 → content_scripts[]

```

### 4.3 CRXJS vs WXT 心智差异

| 维度       | CRXJS                      | WXT                                        |
| -------- | -------------------------- | ------------------------------------------ |
| manifest | 手写 manifest.config.ts 全量字段 | 入口字段自动推导，只补权限等                             |
| 入口注册     | manifest 里逐个指向 HTML/TS     | entrypoints/ 目录约定                          |
| 后台/内容脚本  | 普通 TS 文件 + manifest 声明     | defineBackground / defineContentScript 自描述 |
| 多浏览器     | 主要 Chrome，FF 需自处理          | \--browser 一键切换，自动适配 MV2/MV3               |
| 配置自由度    | 高（直接改 vite.config）         | 中（通过 vite() 钩子扩展）                          |

---

## 五、配套工具链

### 5.1 TypeScript 与 chrome 类型

```bash
npm i -D typescript @types/chrome

```

| 包                            | 作用                                     |
| ---------------------------- | -------------------------------------- |
| @types/chrome                | chrome.\* API 的 TS 类型（WXT/CRXJS 模板已内置） |
| @types/webextension-polyfill | browser.\* 跨浏览器 API 类型                 |

### 5.2 跨浏览器：webextension-polyfill

Chrome 用回调式 `chrome.*`，Firefox 用 Promise 式 `browser.*`。`webextension-polyfill` 把 `chrome.*` 统一成返回 Promise 的 `browser.*`，便于 `async/await`：

```bash
npm i webextension-polyfill
npm i -D @types/webextension-polyfill

```

```ts
import browser from 'webextension-polyfill'

// 统一用 Promise 风格，Chrome/Firefox 一致
const { token } = await browser.storage.local.get('token')

```

> \[!note\] WXT 内置 `@wxt-dev/browser`  
> WXT 用户无需手动装 polyfill，直接 `import { browser } from 'wxt/browser'` 即可获得统一的 `browser` 对象与类型。

### 5.3 ESLint

```bash
npm i -D eslint

```

为插件项目额外加 webextensions 全局环境，避免 `chrome`/`browser` 被报未定义：

```js
// eslint.config.js（flat config）
import globals from 'globals'

export default [
  {
    languageOptions: {
      globals: {
        ...globals.browser,
        ...globals.webextensions,   // 提供 chrome / browser 全局
      },
    },
  },
]

```

### 5.4 UnoCSS / Tailwind 在插件里

UI 上下文（popup/options/sidepanel）与普通 Vue 页面用法完全一致。**唯一要注意 content script**：content script 注入到目标页面，若直接用全局 Tailwind/UnoCSS，preflight（基础样式重置）会污染宿主页面，且宿主页面样式也可能反污染你的 UI。

```ts
// 推荐：content script 的 UI 用 Shadow DOM 隔离（WXT 提供 createShadowRootUi）
export default defineContentScript({
  matches: ['<all_urls>'],
  cssInjectionMode: 'ui',        // 让 CSS 注入到 Shadow DOM，而非宿主页面
  async main(ctx) {
    const ui = await createShadowRootUi(ctx, {
      name: 'my-ui',
      position: 'inline',
      onMount(container) {
        // 在隔离的 shadow root 内挂载 Vue 应用
      },
    })
    ui.mount()
  },
})

```

CRXJS 下没有内置 Shadow DOM 辅助，需自行创建 shadow root 并把构建产物 CSS 字符串注入其中。

### 5.5 组件库（Element Plus / PrimeVue）在 popup 的注意点

- **尺寸**：popup 宽度受限（通常 ≤ 800px，高度 ≤ 600px），重型组件库默认样式可能溢出，建议固定 `body { width: 360px }`。
- **Teleport/全局挂载**：Element Plus 的 `ElMessage`、`ElDialog` 默认挂到 `document.body`。在 content script 的 Shadow DOM 场景下，需把 `appendTo` 指向 shadow root，否则弹层渲染到宿主页面外、样式丢失。
- **按需引入**：用 `unplugin-vue-components` \+ 对应 Resolver 自动按需，减小 popup 体积（影响首屏打开速度）。
- **CSP**：MV3 默认 CSP 禁止 inline script 与 `eval`。组件库若依赖运行时编译模板会被拦截，应使用预编译（SFC `<template>` 编译，而非运行时 `template` 选项）。

---

## 六、状态管理：Pinia + storage 持久化

popup 每次关闭即销毁、重开重建，内存状态不持久；多个上下文（popup/options/background）各自独立运行，内存不共享。**结论：Pinia 负责单个上下文内的响应式状态，`chrome.storage`/`browser.storage` 负责跨上下文与持久化。**

```ts
// stores/settings.ts
import { defineStore } from 'pinia'
import browser from 'webextension-polyfill'

export const useSettings = defineStore('settings', {
  state: () => ({ theme: 'light' as 'light' | 'dark' }),
  actions: {
    // 初始化时从 storage 读回（popup 重开后恢复）
    async load() {
      const { theme } = await browser.storage.local.get('theme')
      if (theme) this.theme = theme as 'light' | 'dark'
    },
    // 变更时写入 storage，并通知其他上下文
    async setTheme(theme: 'light' | 'dark') {
      this.theme = theme
      await browser.storage.local.set({ theme })
    },
  },
})

// 监听 storage 变化，让 options 修改后 popup 自动同步
browser.storage.onChanged.addListener((changes, area) => {
  if (area === 'local' && changes.theme) {
    useSettings().theme = changes.theme.newValue
  }
})

```

> \[!tip\] `@webext-core/storage` 简化持久化  
> `@webext-core/storage` 提供带类型、带默认值、可监听的 storage 封装（`defineItem`），比裸 `storage.get/set` 更安全，可替代手写同步逻辑。

---

## 七、测试工具

| 工具                        | 层级    | 用途                             |
| ------------------------- | ----- | ------------------------------ |
| vitest                    | 单元/组件 | 测纯逻辑、Pinia store、Vue 组件        |
| @webext-core/fake-browser | 单元    | 在 vitest 里 mock browser.\* API |
| playwright                | E2E   | 启动真实 Chromium 并加载插件，测端到端流程     |
| puppeteer                 | E2E   | 同上，老牌方案，加载未打包插件                |

### 7.1 vitest mock 浏览器 API

```ts
// settings.test.ts
import { fakeBrowser } from '@webext-core/fake-browser'
import { beforeEach, expect, it } from 'vitest'

beforeEach(() => fakeBrowser.reset())   // 每个用例重置内存版 storage

it('persists theme', async () => {
  await fakeBrowser.storage.local.set({ theme: 'dark' })
  const { theme } = await fakeBrowser.storage.local.get('theme')
  expect(theme).toBe('dark')
})

```

### 7.2 Playwright 加载已构建插件做 E2E

```ts
// e2e/popup.spec.ts
import { test, chromium, expect } from '@playwright/test'
import path from 'node:path'

test('popup renders', async () => {
  const pathToExtension = path.resolve('dist')   // 先 npm run build 产出 dist
  // MV3 service worker 需用 persistent context + 加载插件参数
  const context = await chromium.launchPersistentContext('', {
    headless: false,
    args: [
      `--disable-extensions-except=${pathToExtension}`,
      `--load-extension=${pathToExtension}`,
    ],
  })
  // 从 service worker 拿到扩展 ID，再打开 popup 页面断言
  const [sw] = context.serviceWorkers()
  const extId = sw.url().split('/')[2]
  const page = await context.newPage()
  await page.goto(`chrome-extension://${extId}/popup/index.html`)
  await expect(page.locator('#app')).toBeVisible()
  await context.close()
})

```

---

## 八、打包发布工具

| 工具                  | 作用                                               |
| ------------------- | ------------------------------------------------ |
| web-ext（Mozilla 官方） | Firefox 下 run（带热重载运行）、lint、sign（签名）、build（打 zip） |
| wxt zip             | WXT 内置，一键产出各浏览器商店所需 zip（含 Firefox 所需源码包）         |
| zip 产物              | Chrome Web Store / Edge Addons 上传需要 zip 包        |
| CI（GitHub Actions）  | 自动构建 + zip + 调用商店 API 上架                         |

### 8.1 web-ext 常用命令

```bash
npm i -D web-ext

web-ext run --source-dir ./dist            # 启动 Firefox 加载插件并热重载
web-ext lint --source-dir ./dist           # 上架前静态校验 manifest/权限
web-ext build --source-dir ./dist          # 产出 web-ext-artifacts/*.zip
web-ext sign --source-dir ./dist \
  --api-key=$AMO_KEY --api-secret=$AMO_SECRET   # AMO 签名（自托管 .xpi）

```

### 8.2 GitHub Actions 自动上架（WXT 示例）

```yaml
# .github/workflows/release.yml
name: Release Extension
on:
  push:
    tags: ['v*']
jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 20 }
      - run: npm ci
      - run: npm run zip          # WXT：产出 chrome zip
      - run: npm run zip:firefox  # WXT：产出 firefox zip + 源码包
      # 用 publish-browser-extension / chrome-webstore-upload 等 action 上传
      - run: npx wxt submit \
          --chrome-zip .output/*-chrome.zip \
          --firefox-zip .output/*-firefox.zip \
          --firefox-sources-zip .output/*-sources.zip
        env:
          CHROME_EXTENSION_ID: ${{ secrets.CHROME_EXTENSION_ID }}
          CHROME_CLIENT_ID: ${{ secrets.CHROME_CLIENT_ID }}
          CHROME_CLIENT_SECRET: ${{ secrets.CHROME_CLIENT_SECRET }}
          CHROME_REFRESH_TOKEN: ${{ secrets.CHROME_REFRESH_TOKEN }}

```

---

## 九、最佳实践

1. **新项目默认 WXT，存量 Vue+Vite 工程才用 CRXJS。** WXT 把 manifest、入口、HMR、多浏览器全部约定化，样板最少；CRXJS 的价值仅在「你已重度依赖自定义 `vite.config`」。  
```bash  
npx wxt@latest init my-ext   # 新项目一行起步  
```
2. **版本号集中到 `package.json`，manifest 引用它。** 发版只改一处，避免 manifest 与 package.json 版本不一致被商店拒绝。  
```ts  
import pkg from './package.json'  
export default defineManifest({ version: pkg.version /* ... */ })  
```
3. **content script 的 UI 一律用 Shadow DOM 隔离。** 防止宿主页面 CSS 与你的样式互相污染。WXT 用 `cssInjectionMode: 'ui'` \+ `createShadowRootUi`；CRXJS 手动建 shadow root 注入 CSS 字符串。
4. **跨浏览器统一用 `browser.*`（polyfill 或 `wxt/browser`），全程 `async/await`。** 不要混用 `chrome.*` 回调与 `browser.*` Promise，否则 Firefox 上行为不一致。
5. **CI 用 `web-ext lint` 在上架前卡关。** 把 manifest 字段错误、过度权限在合并前就拦下，避免商店审核打回。  
```bash  
web-ext lint --source-dir ./.output/chrome-mv3  
```
6. **状态分层：Pinia 管内存响应式，storage 管持久化与跨上下文，并监听 `storage.onChanged` 做同步。** popup 关闭即销毁，纯内存状态会丢。
7. **遵循最小权限原则。** `permissions` 与 `host_permissions` 只声明确实用到的；`<all_urls>` 会显著拉长商店审核时间，能用 `activeTab` 就别要广域 host 权限。

---

## 十、常见陷阱

> \[!warning\] 陷阱 1：CRXJS 装错 dist-tag（`@beta` 反而更旧）  
> **现象**：照旧教程 `npm i @crxjs/vite-plugin@beta` 装到 `2.0.0-beta.33`，新版 Vite 7 下报错或 HMR 失效。  
> **原因**：CRXJS 历史上长期只发 `2.0.0-beta.x`，`beta` 标签从未更新；而 `latest` 已是 **2.4.0**。`@beta` 现在指向的是一个比 `@latest` 旧的版本。  
> **解决**：明确装 `npm i -D @crxjs/vite-plugin@latest`（或锁定 `^2.4.0`），不要再用 `@beta`。

> \[!warning\] 陷阱 2：CRXJS HMR 端口冲突导致 content script 不热重载  
> **现象**：popup 能 HMR，但改 content script 后注入页面不更新，控制台报 WebSocket 连接失败。  
> **原因**：CRXJS 的 HMR 依赖一个固定端口与目标页面建立连接。若该端口被占用或随每次启动变化（`strictPort: false`），content script 侧连不上 HMR server。  
> **解决**：在 `vite.config` 锁定 `server.port` \+ `server.strictPort: true` \+ `server.hmr.port`，确保端口稳定；端口被占用时换一个固定值而非让 Vite 随机选。

> \[!warning\] 陷阱 3：content script 的 CSS 污染宿主页面 / 被宿主页面污染  
> **现象**：插件注入的按钮样式在某些网站完全错乱，或注入后目标网站自身排版被你的 reset 样式破坏。  
> **原因**：content script 默认把 CSS 注入宿主页面全局作用域，与宿主页面样式互相覆盖；Tailwind/UnoCSS 的 preflight reset 影响尤其大。  
> **解决**：用 Shadow DOM 承载注入 UI（WXT `cssInjectionMode: 'ui'`），让样式封闭在 shadow root；组件库弹层把 `appendTo` 指向 shadow root，避免渲染到外部丢样式。

> \[!warning\] 陷阱 4：manifest 里的资源路径写成构建后路径  
> **现象**：开发正常，构建后图标/content script 404，或 `web_accessible_resources` 拿不到文件。  
> **原因**：CRXJS/WXT 期望你在 manifest 里写**源码路径**（如 `src/popup/index.html`、`public/icons/48.png`），插件负责转换为产物路径；若手写成 `dist/...` 或 `assets/xxx.js` 这类构建产物路径，映射就会断。  
> **解决**：manifest 中一律写源码相对路径，让脚手架做路径改写；静态图标放 `public/` 并以 `public/...` 引用。

> \[!warning\] 陷阱 5：MV3 CSP 拦截运行时模板编译 / inline 脚本  
> **现象**：popup 白屏，控制台报 `Refused to evaluate ... unsafe-eval` 或 CSP 违规。  
> **原因**：MV3 默认 CSP 禁止 `eval` 与 inline script。Vue 运行时编译（runtime-compiler）、某些组件库的动态模板会触发。  
> **解决**：使用 SFC 预编译（默认的 `vue` 构建即是 runtime-only），不要在代码里传字符串 `template` 选项；第三方库若内含 `eval` 需替换或寻找 MV3 兼容版本。

---

## 参见

- [浏览器插件开发完全指南](https://blog.vercanti.com/liu-lan-qi-cha-jian-kai-fa-wan-quan-zhi-nan-vite-vue-manifest-v3/)
- [浏览器插件-基础概念与架构模型](https://blog.vercanti.com/liu-lan-qi-cha-jian-ji-chu-gai-nian-yu-jia-gou-mo-xing/)
- [浏览器插件-设计模式与优雅架构](https://blog.vercanti.com/liu-lan-qi-cha-jian-she-ji-mo-shi-yu-you-ya-jia-gou/)
- [Vite初级指南](https://blog.vercanti.com/vite-chu-ji-zhi-nan/)
- [Vite中级指南](https://blog.vercanti.com/vite-zhong-ji-zhi-nan/)
- [Vite高级指南](https://blog.vercanti.com/vite-gao-ji-zhi-nan/)
- [Vue3入门](https://blog.vercanti.com/vue-3-ru-men-zhi-nan/)
- [Pinia完全指南](https://blog.vercanti.com/pinia-wan-quan-zhi-nan/)
- [TailwindCSS完全指南](https://blog.vercanti.com/tailwindcss-wan-quan-zhi-nan/)
- [TypeScript完全指南](https://blog.vercanti.com/typescript-wan-quan-zhi-nan/)