浏览器扩展
构建 Manifest V3 扩展,支持 Chrome/Firefox 并发布到 Web Store。
设计与 Manifest
定义扩展类型、权限,以及决定一切的 manifest.json。
为什么
Manifest V3 在 Chrome 是强制要求——manifest 决定权限、CSP 和能力,Web Store 严格审查。权限过多触发审核拒绝和用户不信任;过少则功能失效。先把这个契约做对。
何时使用
在写任何 content/popup/background 代码之前。2-5 天定稿 manifest + 权限范围。
如何做
1. 选择扩展界面
- Popup:点图标 -> 小 HTML 面板(最常见)
- Content script:注入页面(DOM 修改、抓取)
- Background service worker(MV3):事件、闹钟、跨 tab 状态
- Side panel / DevTools panel:专用 UI
2. 写 manifest.json(MV3)
- manifest_version: 3
- permissions:请求最小集(activeTab、storage、alarms)
- host_permissions:只放你实际触碰的域名
- action.default_popup 指向 popup HTML
- background.service_worker 指向 SW
3. 强制内容安全策略
- MV3 默认 CSP 禁止远程代码(无 eval、无远程脚本)
- 所有代码必须打包进扩展包
- 用打包器(Vite/webpack)产出单一 dist/
4. 规划跨浏览器(Chrome + Firefox)
- Firefox 支持 MV3,用 browser.* API(基于 Promise)
- 用 webextension-polyfill 统一 chrome.* 和 browser.*
- 从一开始就双测;仅在浏览器检测后分歧
5. 为审核限定权限
- activeTab(点击时授予)> tabs(常开)— 优先 activeTab
- 在商店描述中为每个权限给出理由
示例
manifest.json(MV3 popup + content script):
{
"manifest_version": 3,
"name": "Page Tidy",
"version": "1.0.0",
"action": { "default_popup": "popup.html" },
"background": { "service_worker": "sw.js" },
"permissions": ["activeTab", "storage", "alarms"],
"host_permissions": ["https://*.example.com/*"],
"content_scripts": [{
"matches": ["https://*.example.com/*"],
"js": ["content.js"]
}]
}
打包器(Vite):src/ -> dist/,manifest 拷入
跨浏览器:webextension-polyfill 统一 chrome/browser API
常见问题
- ⚠请求宽泛主机权限(<all_urls>)——审核慢且吓退用户
- ⚠用 eval/远程脚本——MV3 CSP 运行时拒绝
- ⚠只为 Chrome 构建,发布时才发现 Firefox MV3 差异
最佳实践
- ✓请求 activeTab 而非 tabs——仅在用户操作时授予,便于审核
- ✓用 Vite 打包一切;MV3 禁远程代码,运行时无 CDN 脚本
- ✓第一天就用 webextension-polyfill,让 Chrome 和 Firefox 共享一套代码
开发与测试
构建 popup/content/SW,配备热重载和跨浏览器测试。
为什么
扩展 bug 与环境强相关:service worker 空闲 30 秒后挂起,content script 与页面 JS 竞态,popup 状态每次关闭后重置。不测这些生命周期,就会上线只在真实浏览中出现的 bug。
何时使用
manifest 定稿后。先做 popup UI(反馈最快),再 content script,再 SW。
如何做
1. 搭建热重载开发
- vite + @crxjs/vite-plugin:popup、content script 的 HMR
- 加载未打包:chrome://extensions -> 开发者模式 -> 加载 dist/
2. 构建 popup
- React/Vue/Svelte popup,保持 < 300KB(每次点击都加载)
- 状态存 chrome.storage.local,不放内存(popup 会重置)
3. 构建 content script
- 只匹配特定 URL;在 document_idle 注入避免竞态
- 用 chrome.runtime.sendMessage 与 SW 通信
- 用 Shadow DOM 隔离样式,避免污染页面 CSS
4. 处理 service worker 生命周期
- SW 空闲约 30 秒后挂起——绝不在 SW 内存中存状态
- 用 chrome.storage 持久化,chrome.alarms 做定时
- 每次唤醒重建监听器(events API 幂等)
5. 跨浏览器测试
- 在 Chrome(chromium)和 Firefox(about:debugging)加载
- Vitest 做单元逻辑;Playwright + 扩展做 E2E
- 测试 SW 挂起:空闲 60 秒,再触发事件
示例
Popup 状态持久化:
// popup.tsx — 打开时加载,变更时保存
const [prefs, setPrefs] = useState<Prefs>();
useEffect(() => {
chrome.storage.local.get('prefs').then(r => setPrefs(r.prefs));
}, []);
const update = (p) => chrome.storage.local.set({ prefs: p });
SW 闹钟(抗挂起):
chrome.alarms.create('sync', { periodInMinutes: 30 });
chrome.alarms.onAlarm.addListener(a => {
if (a.name === 'sync') doSync();
});
E2E(Playwright + 扩展):
const ctx = await chromium.launchPersistentContext('', {
args: [`--load-extension=${distPath}`],
});
常见问题
- ⚠在 SW 内存存状态——挂起后丢失,导致间歇性'首次点击无效'bug
- ⚠Content script CSS 泄漏到页面——破坏宿主站点样式,导致卸载
- ⚠Popup 包 > 1MB——打开慢,用户觉得扩展卡顿
最佳实践
- ✓把 service worker 当无状态——持久化到 chrome.storage,每次事件重建
- ✓把 content script UI 包在 Shadow DOM 根中,样式永不与页面冲突
- ✓Popup 包保持小(< 300KB);重视图代码分割,按需懒加载
商店发布
打包、签名并发布到 Chrome Web Store 和 Firefox Add-ons(AMO)。
为什么
Web Store 是你唯一的分发渠道——一次拒绝就阻断所有用户。商店审核强制权限说明、隐私实践和单一用途政策。首次干净提交建立信任;反复拒绝可能让开发者账号被标记。
何时使用
扩展跨浏览器测试通过且 manifest 权限已说明时。Chrome 审核预留 1-7 天,Firefox < 2 天。
如何做
1. 打包扩展
- vite build -> dist/ 含 manifest.json
- Chrome 打包 zip dist/;Firefox 用 web-ext 签名
- 含 128x128 图标和商店素材(截图、推广图)
2. 创建商店上架信息
- Chrome Web Store:$5 一次性开发者费,再上传 .zip
- Firefox AMO:免费,用 web-ext sign --api-key/--api-secret 签名
- 写清晰描述,为每个权限给出理由
3. 准备隐私实践
- 收集任何数据都需隐私政策 URL
- 说明收集什么数据、存哪里、如何使用
- "单一用途"声明:一句话描述扩展用途
4. 提交并处理审核
- Chrome:上传 zip,提交审核,关注开发者面板
- 精确回应拒绝反馈——只重提修复项
- Firefox:签名 xpi 审核后自动在 AMO 上线
5. 上线后更新
- 更新上传新 zip(Chrome 重新审核,通常更快)
- 用 chrome.storage.sync 跨设备备份用户设置
- 通过 Web Store 面板监控卸载原因
示例
打包 + 发布(Chrome):
vite build # -> dist/
cd dist && zip -r ../ext.zip . # 打包
# 在 chrome.google.com/webstore/devconsole 上传 ext.zip
签名 + 发布(Firefox):
web-ext build # -> web-ext-artifacts/*.xpi
web-ext sign --api-key $KEY --api-secret $SECRET
# 签名 xpi 自动在 AMO 上线
上架描述(权限说明):
"权限:
- activeTab:点击图标时读取当前标签页
- storage:本地记住你的偏好
无数据离开你的设备。"
常见问题
- ⚠收集数据却无隐私政策——立即拒绝,可能标记账号
- ⚠单一用途声明模糊——审核员拒绝'做太多事'的扩展
- ⚠未针对具体审核反馈就重提——数周循环
最佳实践
- ✓打包前就写好隐私政策和单一用途声明——它们是审核要求
- ✓在上架描述中为每个权限给出理由;审核员首先读这个
- ✓逐条回应拒绝反馈,只重提修复版而非重写