浏览器插件-技术栈与脚手架

浏览器插件本质是「一组特殊上下文(context)的网页 + 一份清单文件 manifest.json」。manifest.json 把多个入口(popup 弹窗、options 选项页、side panel 侧边栏、content script 内容脚本、background/service worker 后台)声明给浏览器。普通前端构建工具默认面向「一个或多个 HTML 页面」,并不理解 manifest.json 里这些特殊入口与权限声明,也不会处理 content script 在目标页面的 CSS 注入、HMR(Hot Module Repla

分享

[!info] 文档信息

概述

浏览器插件本质是「一组特殊上下文(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 选型决策树

开始:要写一个浏览器插件
│
├─ 主用框架是 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

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

WXT 生成的目录(约定式 entrypoints/):

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 模板)

# 先用官方模板拉一个 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 推荐目录(手动组织,下文第三节给完整配置):

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

2.3 Plasmo

npm create plasmo my-extension      # React + TS 默认
# 选 Vue:使用社区 with-vue 示例模板
npm create plasmo --with-vue
cd my-extension
npm run dev
my-extension/
├── popup.tsx                 # 文件名即入口:popup/options/newtab...
├── options.tsx
├── background.ts
├── contents/                 # 内容脚本目录
├── assets/
└── package.json              # manifest 字段写在这里

2.4 Extension.js

npx extension@latest create my-extension --template=vue
cd my-extension
npm run dev                    # 启动并自动打开浏览器加载
my-extension/
├── manifest.json             # 手写但有自动浏览器适配
├── background.js
├── content/  ├── popup/  └── pages/
└── extension.config.js       # 可选,配置浏览器/启动命令

2.5 vite-plugin-web-extension(aklinker1)

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)

// 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

// 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 多入口目录组织与示例

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 逻辑
<!-- 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>
// src/popup/main.ts
import { createApp } from 'vue'
import App from './App.vue'

createApp(App).mount('#app')
// 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

// 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

// entrypoints/background.ts
// defineBackground 包裹后台逻辑,WXT 据此生成 manifest.background 字段
export default defineBackground(() => {
  chrome.runtime.onInstalled.addListener(() => {
    console.log('installed')
  })
})
// entrypoints/content.ts
// defineContentScript 同时声明注入规则与逻辑,无需再到 manifest 手写 content_scripts
export default defineContentScript({
  matches: ['<all_urls>'],     // 注入页面匹配
  main() {
    console.log('content script injected')
  },
})
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 类型

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-polyfillchrome.* 统一成返回 Promise 的 browser.*,便于 async/await

npm i webextension-polyfill
npm i -D @types/webextension-polyfill
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

npm i -D eslint

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

// 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。

// 推荐: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 的 ElMessageElDialog 默认挂到 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 负责跨上下文与持久化。

// 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

// 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

// 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(带热重载运行)、lintsign(签名)、build(打 zip)
wxt zip WXT 内置,一键产出各浏览器商店所需 zip(含 Firefox 所需源码包)
zip 产物 Chrome Web Store / Edge Addons 上传需要 zip 包
CI(GitHub Actions) 自动构建 + zip + 调用商店 API 上架

8.1 web-ext 常用命令

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 示例)

# .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」。

    npx wxt@latest init my-ext   # 新项目一行起步
    
  2. 版本号集中到 package.json,manifest 引用它。 发版只改一处,避免 manifest 与 package.json 版本不一致被商店拒绝。

    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 字段错误、过度权限在合并前就拦下,避免商店审核打回。

    web-ext lint --source-dir ./.output/chrome-mv3
    
  6. 状态分层:Pinia 管内存响应式,storage 管持久化与跨上下文,并监听 storage.onChanged 做同步。 popup 关闭即销毁,纯内存状态会丢。

  7. 遵循最小权限原则。 permissionshost_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.xbeta 标签从未更新;而 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.htmlpublic/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 兼容版本。


参见

阅读更多

Web 安全基础

1. HTML 转义(服务端渲染必须): 2. CSP(Content Security Policy): 3. HttpOnly Cookie:防止 JS 读取会话 Cookie: 4. 前端框架防护: 攻击者在第三方网站构造一个表单,诱导已登录用户提交,浏览器会自动携带目标站的 Cookie。 触发条件: 1. 用户已登录目标网站(Cookie 有效) 2. 目标 API 仅凭 Cookie 识别用户身份 3. 请求来源未验证 1. CSRF Token(推荐): 2. SameSite Cookie: 3. 验证 Origin/Referer 头:

By yellowdog

HTTP 协议深度指南

HTTP(HyperText Transfer Protocol)是 Web 的基础传输协议,基于 TCP/IP,采用请求/响应模型。 相关文档:Web安全基础(/web-an-quan-ji-chu/) FastAPI完全指南(/fastapi-wan-quan-zhi-nan/) Nginx完全指南(/nginx-wan-quan-zhi-nan/) 幂等性:多次执行相同请求,服务器状态结果相同。PUT /users/1 多次执行结果一致;POST /users 每次创建新资源,非幂等。 浏览器直接从本地缓存读取,不向服务器发送请求。 缓存命中时,状

By yellowdog

系统设计基础

SLA 对照表: 选择建议:无状态服务(Web 层、API 层)优先水平扩展;数据库初期垂直扩展,达到瓶颈后考虑分库分表或读写分离。 缓存穿透(查询不存在的 key,每次都打到 DB): 缓存击穿(热点 key 过期,瞬间大量请求打到 DB): 缓存雪崩(大量 key 同时过期,或缓存服务宕机): 令牌桶 Python 实现: Redis 实现分布式限流(滑动窗口): URL 命名规则: Cursor 分页响应格式: 雪花算法结构(64 bit): 定义:分布式系统不能同时满足以下三个特性: 在分布式环境中 P 是必须保证的,所以实际是 CP vs AP

By yellowdog

算法思路与模板

二分查找要求序列有序,每次将搜索范围缩减一半,时间复杂度 O(log n)。 两个指针从两端向中间收缩,常用于有序数组。 滑动窗口维护一个满足条件的区间 left, right,right 不断向右扩张,条件不满足时收缩 left。 滑动窗口通用框架: 1. 确定"子问题":原问题可以分解为哪些规模更小的同类问题 2. 定义 dpi 或 dpij 的含义,要足够清晰 3. 推导状态转移方程 4. 确定初始状态(边界条件) 5. 确定计算顺序(确保依赖的子问题先计算) 每件物品最多选一次。dpj = 容量为 j 时的最大价值,逆序遍历容量防止重复选取。 每

By yellowdog