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

# 浏览器插件 高级开发指南（Manifest V3）
- URL: https://blog.vercanti.com/liu-lan-qi-cha-jian-gao-ji-kai-fa-zhi-nan-manifest-v3/
- Published: 2026-08-28T14:35:32.000Z
- Updated: 2026-08-28T14:59:02.000Z
- Description: 本文面向已掌握 Manifest V3 基础、要做复杂、高性能、生产级插件的工程师，聚焦深水区：Service Worker（服务工作线程，简称 SW）生命周期深控、Offscreen Document（屏外文档）、declarativeNetRequest（声明式网络请求，简称 DNR）高级规则、性能与大数据存储、身份认证、Native Messaging（原生消息）、WASM（WebAssembly）加载、安全加固、并发竞态、自动化测试与 CI/CD。基础概念见 浏览器插件-基础概念与架构模型(/liu-lan-qi-cha-jian-ji-chu-
- Author: yellowdog
- Tags: 前端开发, 浏览器插件开发

> 官方文档：
> 
> - Service Worker 生命周期 <https://developer.chrome.com/docs/extensions/develop/concepts/service-workers/lifecycle>
> - Offscreen API <https://developer.chrome.com/docs/extensions/reference/api/offscreen>
> - declarativeNetRequest <https://developer.chrome.com/docs/extensions/reference/api/declarativeNetRequest>
> - identity API <https://developer.chrome.com/docs/extensions/reference/api/identity>
> - Native Messaging <https://developer.chrome.com/docs/extensions/develop/concepts/native-messaging>
> - 内容安全策略（CSP）<https://developer.chrome.com/docs/extensions/reference/manifest/content-security-policy>
> 
> 适用版本：Manifest V3 / Chrome 138+  
> 核实日期：2026-06-06

本文面向已掌握 Manifest V3 基础、要做复杂、高性能、生产级插件的工程师，聚焦深水区：Service Worker（服务工作线程，简称 SW）生命周期深控、Offscreen Document（屏外文档）、declarativeNetRequest（声明式网络请求，简称 DNR）高级规则、性能与大数据存储、身份认证、Native Messaging（原生消息）、WASM（WebAssembly）加载、安全加固、并发竞态、自动化测试与 CI/CD。基础概念见 [浏览器插件-基础概念与架构模型](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-zhong-ji-kai-fa-zhi-nan-manifest-v3-vite-vue/)。

---

## 一、Service Worker 生命周期深控

### MV3 SW 与传统 Background Page 的本质差异

MV2 的 background page 是常驻页面，全局变量可长期存活。MV3 的 SW 是事件驱动（event-driven）的短生命周期进程，浏览器随时回收。它没有 DOM、没有 `window`、没有 `localStorage`，全局状态会在回收后丢失。任何"我把数据存在内存变量里"的设计在 MV3 都是错误的。

### 唤醒与回收机制（精确时序）

| 行为           | 触发条件                                                                                      | 来源              |
| ------------ | ----------------------------------------------------------------------------------------- | --------------- |
| 启动           | 安装/更新时 install → chrome.runtime.onInstalled → activate；浏览器启动时仅触发 chrome.runtime.onStartup | 官方 lifecycle 文档 |
| 唤醒           | 收到已注册的事件，或被其它上下文通过消息/API 调用唤醒                                                             | 同上              |
| 回收（空闲）       | 30 秒无活动。收到事件或调用任意扩展 API 会重置该计时器（Chrome 110+ 任意 API 调用均重置，不限于事件处理器）                        | 同上              |
| 回收（超时）       | 单个请求处理超过 5 分钟                                                                             | 同上              |
| 回收（fetch 超时） | 网络响应超过 30 秒                                                                               | 同上              |
| 续命           | Chrome 114+：长生命周期 messaging port 保持 SW 存活；Chrome 116+：WebSocket 消息活动延长存活                  | 同上              |

关键事实：**计时器只关心"是否有活动"**。一次 `chrome.storage.local.get()` 调用就会把 30 秒计时器重置。SW 没有"快被回收了"的事件可监听，必须假设它随时消失。

### 顶层同步注册监听器（硬性要求）

SW 被唤醒处理事件时，浏览器会重新执行整个脚本文件。事件监听器必须在脚本**顶层同步注册**，否则唤醒时事件已经派发、监听器尚未注册，事件丢失。

```js
// background.js（service worker 入口）

// 正确：顶层同步注册，脚本一执行就完成订阅
chrome.runtime.onMessage.addListener(handleMessage);
chrome.alarms.onAlarm.addListener(handleAlarm);
chrome.action.onClicked.addListener(handleActionClick);

// 错误：在异步回调里注册，唤醒时来不及订阅，事件会丢失
chrome.storage.local.get('config', (cfg) => {
  // 错误：此处注册的监听器在 SW 冷启动唤醒时尚未就绪
  chrome.runtime.onMessage.addListener(handleMessage);
});

async function handleMessage(message, sender, sendResponse) {
  // 异步逻辑放进处理函数内部，而非延迟注册
  const cfg = await chrome.storage.local.get('config');
  sendResponse({ ok: true, theme: cfg.config?.theme });
  return true; // 见下文：异步 sendResponse 必须 return true
}

```

异步消息处理需在监听器中 `return true`，告诉 Chrome `sendResponse` 会被异步调用，保持消息通道开启。

### 用 alarms + 事件驱动替代常驻，不要滥用保活

需要定时任务时，使用 `chrome.alarms` 而非 `setInterval`。`setInterval` 在 SW 回收后失效，且会被空闲计时器视为活动从而异常续命，浪费资源。`alarms` 最小周期为 30 秒（`periodInMinutes` 最小 0.5），到点由浏览器唤醒 SW。

```js
// 注册周期任务（幂等，重复 create 会覆盖同名 alarm）
chrome.runtime.onInstalled.addListener(() => {
  chrome.alarms.create('sync', { periodInMinutes: 5 });
});

chrome.alarms.onAlarm.addListener(async (alarm) => {
  if (alarm.name === 'sync') {
    await syncData(); // 处理完后 SW 自然回收，下次到点再唤醒
  }
});

```

**保活的正确做法与误区**：网络上流行用 `setInterval` 反复调用 `chrome.runtime.getPlatformInfo()` 续命，或开一个空 port 心跳。这违背 MV3 设计意图、增加内存与电量开销，且 Chrome 会逐步收紧续命漏洞。只有在真正需要持续运行的场景（如维持 WebSocket 长连接接收推送）才考虑续命，且应使用官方支持的机制：

```js
// 仅当确实需要维持 WebSocket 时——Chrome 116+ 下 WS 消息会延长 SW 存活
let ws;
function connect() {
  ws = new WebSocket('wss://push.example.com');
  ws.addEventListener('message', (e) => handlePush(JSON.parse(e.data)));
  ws.addEventListener('close', () => chrome.alarms.create('reconnect', { delayInMinutes: 0.5 }));
}
chrome.alarms.onAlarm.addListener((a) => { if (a.name === 'reconnect') connect(); });

```

绝大多数插件不需要保活。把任务拆成由事件/alarm 触发的短任务，让 SW 自由回收，才是高性能 MV3 的正道。

### 冷启动优化

SW 每次唤醒都重新执行脚本。冷启动慢的常见原因：顶层做了大量同步初始化、import 了庞大的库、启动时一次性读全部 storage。优化手段：

- 顶层只注册监听器，不做重活；初始化逻辑延迟到首个事件触发时执行。
- 用 `import()` 动态加载只在特定路径才需要的模块（配合 `"type": "module"` 的 SW，见 manifest `background.type`）。
- 启动读 storage 时只读当前需要的 key，不要 `get(null)` 拉全部。

```jsonc
// manifest.json：SW 启用 ES Module，方可在 SW 内使用静态/动态 import
{
  "background": { "service_worker": "background.js", "type": "module" }
}

```

---

## 二、Offscreen Document：在 SW 里用不了 DOM 的解法

SW 无 DOM、无 `Audio`、无剪贴板、无 `DOMParser`、无 `URL.createObjectURL` 对应的部分 Blob 能力。Offscreen Document 是一个不可见的 HTML 文档，提供完整 DOM 环境，由 SW 创建并通过消息通信。

### createDocument 的 reasons 与字段

```js
chrome.offscreen.createDocument(parameters): Promise<void>

```

| 字段            | 类型         | 说明                     |
| ------------- | ---------- | ---------------------- |
| url           | string     | 扩展内相对路径的 HTML 文件       |
| reasons       | Reason\[\] | 创建理由枚举数组，浏览器据此判定生命周期策略 |
| justification | string     | 给审核/用户看的文字说明，必填        |

`reasons` 合法枚举值：`TESTING`、`AUDIO_PLAYBACK`、`IFRAME_SCRIPTING`、`DOM_SCRAPING`、`BLOBS`、`DOM_PARSER`、`USER_MEDIA`、`DISPLAY_MEDIA`、`WEB_RTC`、`CLIPBOARD`、`LOCAL_STORAGE`、`WORKERS`、`BATTERY_STATUS`、`MATCH_MEDIA`、`GEOLOCATION`。

生命周期：`AUDIO_PLAYBACK` 在 30 秒无音频播放后自动关闭；其它 reason 不设自动关闭，需手动 `closeDocument()`。每个 profile 同时只允许存在一个 offscreen document。

### 防止重复创建（getContexts）

重复创建会抛错。创建前用 `chrome.runtime.getContexts()`（Chrome 116+）检测：

```js
// offscreen.js 的管理封装（放在 SW 中）
const OFFSCREEN_URL = 'offscreen.html';
let creating; // 用 Promise 串行化，避免并发创建竞态

async function ensureOffscreen() {
  const existing = await chrome.runtime.getContexts({
    contextTypes: ['OFFSCREEN_DOCUMENT'],
    documentUrls: [chrome.runtime.getURL(OFFSCREEN_URL)],
  });
  if (existing.length > 0) return;

  if (creating) {
    await creating; // 已有创建在途，等它完成
  } else {
    creating = chrome.offscreen.createDocument({
      url: OFFSCREEN_URL,
      reasons: ['DOM_PARSER'],
      justification: '解析远端返回的 HTML 片段，SW 中无 DOMParser',
    });
    await creating;
    creating = null;
  }
}

```

### 与 SW 通信

Offscreen document 仅支持 `chrome.runtime` 消息 API。典型模式是 SW 发任务、offscreen 用 DOM 处理后回传结果，用 `target` 字段区分消息归属，避免误处理。

```js
// SW 侧：请求解析 HTML
async function parseHtml(html) {
  await ensureOffscreen();
  return chrome.runtime.sendMessage({ target: 'offscreen', type: 'parse', html });
}

```

```js
// offscreen.js：只处理发给自己的消息
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
  if (msg.target !== 'offscreen') return; // 不是给我的，放行给其它监听器
  if (msg.type === 'parse') {
    const doc = new DOMParser().parseFromString(msg.html, 'text/html');
    const titles = [...doc.querySelectorAll('h1')].map((h) => h.textContent);
    sendResponse({ titles });
  }
  return true;
});

```

剪贴板写入也是常见用途：SW 无 `navigator.clipboard`，需经 offscreen 文档执行。任务完成后若无后续需要，调用 `chrome.offscreen.closeDocument()` 释放资源。

---

## 三、declarativeNetRequest 复杂规则

DNR 用声明式规则修改/拦截网络请求，替代 MV2 的阻塞式 `webRequest`。规则在浏览器内部执行，性能高、隐私好，但表达力受限于规则模型。

### 规则结构

```jsonc
{
  "id": 1,            // 必填，整数，≥1，同一 ruleset 内唯一
  "priority": 1,      // 可选，默认 1，越大越优先
  "action": { "type": "block" },
  "condition": { "urlFilter": "ads", "resourceTypes": ["script"] }
}

```

### action 类型

| type             | 说明              | 关键字段                                                         |
| ---------------- | --------------- | ------------------------------------------------------------ |
| block            | 拦截请求            | —                                                            |
| redirect         | 重定向             | redirect.url / extensionPath / transform / regexSubstitution |
| modifyHeaders    | 改请求/响应头         | requestHeaders\[\] / responseHeaders\[\]                     |
| allow            | 放行，覆盖低优先级 block | —                                                            |
| allowAllRequests | 放行整个框架下所有请求     | 仅限 main\_frame/sub\_frame                                    |
| upgradeScheme    | HTTP 升级为 HTTPS  | —                                                            |

### condition 字段

| 字段                                          | 说明                                                                                   |
| ------------------------------------------- | ------------------------------------------------------------------------------------ |
| urlFilter                                   | 通配模式（非完整正则），如 \||example.com^                                                        |
| regexFilter                                 | RE2 正则匹配 URL                                                                         |
| resourceTypes / excludedResourceTypes       | main\_frame、sub\_frame、script、image、xmlhttprequest、stylesheet、font、media、websocket 等 |
| initiatorDomains / excludedInitiatorDomains | 发起请求的页面域名                                                                            |
| requestDomains / excludedRequestDomains     | 目标请求域名                                                                               |
| requestMethods / excludedRequestMethods     | get、post 等（小写）                                                                       |
| domainType                                  | firstParty / thirdParty                                                              |
| responseHeaders / excludedResponseHeaders   | 按响应头匹配                                                                               |
| tabIds / excludedTabIds                     | 仅 session 规则可用                                                                       |

### 正则规则与 regexSubstitution 重定向

```jsonc
// 用正则捕获组重写跳转目标
{
  "id": 101, "priority": 1,
  "action": {
    "type": "redirect",
    "redirect": { "regexSubstitution": "https://cdn.example.com/\\1" }
  },
  "condition": {
    "regexFilter": "^https://origin\\.example\\.com/(.*)$",
    "resourceTypes": ["image", "media"]
  }
}

```

### modifyHeaders（增删改请求/响应头）

```jsonc
{
  "id": 201, "priority": 1,
  "action": {
    "type": "modifyHeaders",
    "requestHeaders": [
      { "header": "x-api-token", "operation": "set", "value": "abc123" },
      { "header": "referer", "operation": "remove" }
    ],
    "responseHeaders": [
      { "header": "x-frame-options", "operation": "remove" },
      { "header": "access-control-allow-origin", "operation": "set", "value": "*" }
    ]
  },
  "condition": { "requestDomains": ["api.example.com"], "resourceTypes": ["xmlhttprequest"] }
}

```

`operation` 取 `set` / `remove` / `append`。只有部分头（如 `cookie`、`user-agent`、`accept`、`cache-control` 等）支持 `append`。若某规则对某头执行 append，则更低优先级规则对该头只能继续 append，不能 set/remove。

### 静态规则 + 动态规则 + 会话规则配合

| 类型          | 管理方式                                | 持久性      | 典型用途                 |
| ----------- | ----------------------------------- | -------- | -------------------- |
| 静态（static）  | manifest rule\_resources 指向 JSON 文件 | 随扩展更新持久  | 内置规则集（如广告过滤名单）       |
| 动态（dynamic） | updateDynamicRules()                | 跨会话持久    | 用户自定义、运行时下发          |
| 会话（session） | updateSessionRules()                | 浏览器关闭即清空 | 临时/敏感规则、含 tabIds 的规则 |

```jsonc
// manifest.json：静态规则集 + 反馈权限
{
  "permissions": ["declarativeNetRequest", "declarativeNetRequestFeedback"],
  "host_permissions": ["<all_urls>"],
  "declarative_net_request": {
    "rule_resources": [
      { "id": "ruleset_ads", "enabled": true, "path": "rules/ads.json" }
    ]
  }
}

```

```js
// 运行时增删动态规则（原子操作，全成功或全失败）
await chrome.declarativeNetRequest.updateDynamicRules({
  removeRuleIds: [1, 2],
  addRules: [{
    id: 1000, priority: 2,
    action: { type: 'block' },
    condition: { urlFilter: 'tracker', domainType: 'thirdParty' },
  }],
});

```

### 规则优先级解析

1. 先比 `priority`，大者胜。
2. 同 `priority` 时按 action 优先级：`allow` / `allowAllRequests` \> `block` \> `upgradeScheme` \> `redirect`。
3. 跨扩展同 action 同 priority 时，最后安装的扩展胜。

### 调试 matchedRules

```js
// onRuleMatchedDebug 仅对未打包（unpacked）扩展生效，用于本地调试
chrome.declarativeNetRequest.onRuleMatchedDebug.addListener((info) => {
  console.log('命中规则', info.rule.ruleId, info.request.url);
});

// getMatchedRules 需 declarativeNetRequestFeedback 权限，回溯已命中规则
const { rulesMatchedInfo } = await chrome.declarativeNetRequest.getMatchedRules({});

// testMatchOutcome：开发期预测某请求会命中哪些规则，无需真正发请求
const outcome = await chrome.declarativeNetRequest.testMatchOutcome({
  url: 'https://api.example.com/data', type: 'xmlhttprequest', method: 'get',
});

```

数值上限（关键）：动态+会话规则安全上限 30000、不安全规则下限 5000；静态启用规则集 50 个；静态规则总量保证 30000；每类型正则规则 1000 条。

---

## 四、性能优化

### 内容脚本懒加载 / 按需注入

不要对所有页面静态注入大脚本。用 `chrome.scripting.executeScript` 在用户触发动作时按需注入，减少对无关页面的开销。

```js
// 错误：manifest 静态 content_scripts 对所有匹配页面无条件注入重脚本，拖慢页面加载
// 正确：用户点击图标时才注入
chrome.action.onClicked.addListener(async (tab) => {
  await chrome.scripting.executeScript({
    target: { tabId: tab.id },
    files: ['content/heavy-feature.js'], // 仅此刻注入
  });
});

```

`registerContentScripts` 可在运行时动态注册/注销内容脚本，配合用户配置启停功能模块。

### 减小 bundle 与代码分割

- 用 Vite/Rollup 的代码分割，把按需功能拆成动态 `import()` 的 chunk。
- tree-shaking 友好的库（如 `date-fns` 按需引入而非引整个 `moment`）。
- SW 用 `"type": "module"`，把冷启动用不到的逻辑延迟动态导入。

```js
// SW 内：仅在需要导出功能时才加载大体积的导出模块
async function handleExport(data) {
  const { exportToXlsx } = await import('./export/xlsx.js'); // 动态 chunk
  return exportToXlsx(data);
}

```

### 避免阻塞页面与防抖大量消息

内容脚本中高频事件（scroll、mousemove、mutation）要防抖/节流，且批量发消息而非逐条发，减少跨进程 IPC 开销。

```js
// 内容脚本：批量收集 + 防抖一次性发送，避免每帧一条消息打爆 SW
let buffer = [];
let timer = null;
function enqueue(event) {
  buffer.push(event);
  if (timer) return;
  timer = setTimeout(() => {
    chrome.runtime.sendMessage({ type: 'batch', events: buffer });
    buffer = [];
    timer = null;
  }, 200); // 200ms 合并窗口
}

```

### storage 批量读写

每次 `chrome.storage` 调用都有 IPC 与序列化成本。批量读写显著优于循环单条。

```js
// 错误：循环里逐条写，N 次 IPC
for (const item of items) await chrome.storage.local.set({ [item.id]: item });

// 正确：聚合成一个对象一次写
const batch = Object.fromEntries(items.map((i) => [i.id, i]));
await chrome.storage.local.set(batch);

```

### 长列表虚拟化

popup / options / side panel 渲染上千条数据时用虚拟滚动（如 `@tanstack/virtual`，见 [浏览器插件-中级开发指南](https://blog.vercanti.com/liu-lan-qi-cha-jian-zhong-ji-kai-fa-zhi-nan-manifest-v3-vite-vue/)），只渲染可视区 DOM，避免一次性插入海量节点导致卡顿。

---

## 五、大数据与存储

### IndexedDB（idb 库封装）

`chrome.storage` 适合配置类小数据；结构化大数据、需索引查询的场景用 IndexedDB。原生 API 繁琐，用 `idb`（Promise 封装）。SW 中可直接使用 IndexedDB。

```js
import { openDB } from 'idb';

const dbPromise = openDB('app-db', 1, {
  upgrade(db) {
    const store = db.createObjectStore('records', { keyPath: 'id' });
    store.createIndex('by-time', 'createdAt'); // 建索引支持范围查询
  },
});

export async function putRecords(records) {
  const db = await dbPromise;
  const tx = db.transaction('records', 'readwrite');
  await Promise.all([...records.map((r) => tx.store.put(r)), tx.done]); // 一个事务批量写
}

export async function queryByTimeRange(from, to) {
  const db = await dbPromise;
  return db.getAllFromIndex('records', 'by-time', IDBKeyRange.bound(from, to));
}

```

### storage 配额管理与 unlimitedStorage

`chrome.storage.local` 默认约 10 MB（可被 `unlimitedStorage` 解除）。IndexedDB / CacheStorage 受浏览器配额管理。需要存大量数据时声明 `unlimitedStorage` 权限，并用 `navigator.storage.estimate()` 监控用量。

```jsonc
{ "permissions": ["unlimitedStorage"] }

```

```js
const { usage, quota } = await navigator.storage.estimate();
console.log(`已用 ${(usage / 1048576).toFixed(1)}MB / ${(quota / 1048576).toFixed(0)}MB`);

```

### 缓存策略

用 `CacheStorage` 缓存远端资源（如离线图标、字典），配合版本号失效。SW 中可直接 `caches.open()`。结合 TTL 字段或基于 alarm 的定期清理避免无限增长。

---

## 六、身份认证

### chrome.identity.getAuthToken（Google OAuth）

仅适用于 Google 账户，凭据来自 manifest 的 `oauth2` 键。token 在内存缓存，可放心多次非交互调用。交互式请求应由用户 UI 触发并解释授权用途，勿在启动时无故弹窗。

```jsonc
// manifest.json
{
  "permissions": ["identity"],
  "oauth2": {
    "client_id": "xxxxx.apps.googleusercontent.com",
    "scopes": ["https://www.googleapis.com/auth/userinfo.email"]
  }
}

```

```js
function getGoogleToken(interactive) {
  return new Promise((resolve, reject) => {
    chrome.identity.getAuthToken({ interactive }, (token) => {
      if (chrome.runtime.lastError || !token) return reject(chrome.runtime.lastError);
      resolve(token);
    });
  });
}

// token 失效（401）时清除缓存再重取，否则会一直拿到坏 token
async function callApiWithRetry(url) {
  let token = await getGoogleToken(false);
  let res = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
  if (res.status === 401) {
    await chrome.identity.removeCachedAuthToken({ token });
    token = await getGoogleToken(true);
    res = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
  }
  return res.json();
}

```

`clearAllCachedAuthTokens()` 用于登出时彻底清空所有缓存 token、账户偏好与授权状态。

### launchWebAuthFlow（通用 OAuth2 + PKCE）

非 Google provider 用 `launchWebAuthFlow`。它打开一个 web 视图，等待重定向到 `https://<extension-id>.chromiumapp.org/*`，用 `chrome.identity.getRedirectURL()` 生成该回调地址并在 provider 后台登记。生产环境必须用 PKCE（Proof Key for Code Exchange），不要在前端嵌入 client secret。

```js
function base64url(buf) {
  return btoa(String.fromCharCode(...new Uint8Array(buf)))
    .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}

async function loginWithPkce() {
  const verifier = base64url(crypto.getRandomValues(new Uint8Array(32)));
  const challenge = base64url(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier)));
  const redirectUri = chrome.identity.getRedirectURL(); // 在 provider 处登记此 URI

  const authUrl = new URL('https://auth.example.com/authorize');
  authUrl.search = new URLSearchParams({
    response_type: 'code', client_id: 'CLIENT_ID', redirect_uri: redirectUri,
    scope: 'openid profile', code_challenge: challenge, code_challenge_method: 'S256',
    state: crypto.randomUUID(),
  }).toString();

  const redirect = await chrome.identity.launchWebAuthFlow({ url: authUrl.toString(), interactive: true });
  const code = new URL(redirect).searchParams.get('code');

  // 用 code + verifier 换 token（无 client secret，PKCE 保护）
  const tokenRes = await fetch('https://auth.example.com/token', {
    method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      grant_type: 'authorization_code', code, redirect_uri: redirectUri,
      client_id: 'CLIENT_ID', code_verifier: verifier,
    }),
  });
  return tokenRes.json(); // { access_token, refresh_token, ... }
}

```

### token 安全存储与自有后端鉴权

- access token 放 `chrome.storage.session`（内存级、随浏览器关闭清空），refresh token 等长期凭据若必须落盘，用 Web Crypto 加密后再存（见安全章节）。
- 与自有后端鉴权：插件携带 OAuth token 调后端，后端校验后签发自有短期 JWT，插件后续用 JWT 调业务接口，缩短第三方 token 暴露面。

---

## 七、Native Messaging（原生消息）

当插件需要访问本机能力（读写本地文件、调用本地程序、与桌面应用通信）时，用 Native Messaging 与一个本地"native host"进程通过 stdin/stdout 通信。需 `nativeMessaging` 权限。

### 连接 API

```js
// 持久连接：双向多次通信
const port = chrome.runtime.connectNative('com.my_company.my_host');
port.onMessage.addListener((msg) => console.log('收到', msg));
port.onDisconnect.addListener(() => console.log('断开', chrome.runtime.lastError));
port.postMessage({ cmd: 'read', path: 'C:/data/x.txt' });

// 单次消息：发一条、收一条
chrome.runtime.sendNativeMessage('com.my_company.my_host', { cmd: 'ping' }, (resp) => {
  console.log('响应', resp);
});

```

### native host 清单字段

| 字段               | 说明                                           |
| ---------------- | -------------------------------------------- |
| name             | host 标识，小写字母数字、下划线、点（不能首尾为点）                 |
| description      | 简述                                           |
| path             | 可执行程序路径（Linux/macOS 绝对路径；Windows 可相对于清单文件）   |
| type             | 固定 "stdio"                                   |
| allowed\_origins | 允许连接的扩展 ID 列表（chrome-extension://ID/，不支持通配符） |

```json
{
  "name": "com.my_company.my_host",
  "description": "本地文件桥接",
  "path": "C:\\Program Files\\MyHost\\host.exe",
  "type": "stdio",
  "allowed_origins": ["chrome-extension://abcdefghijklmnoabcdefghijklmnoab/"]
}

```

### 清单安装位置（Windows / macOS / Linux）

Windows 通过注册表指向清单文件路径：

```
HKEY_CURRENT_USER\SOFTWARE\Google\Chrome\NativeMessagingHosts\com.my_company.my_host
（默认值 = 清单 JSON 的绝对路径）
HKEY_LOCAL_MACHINE\SOFTWARE\Google\Chrome\NativeMessagingHosts\com.my_company.my_host

```

macOS 系统级：`/Library/Google/Chrome/NativeMessagingHosts/`。Linux 用户级：`~/.config/google-chrome/NativeMessagingHosts/`。

### 消息格式与限制

协议：**4 字节消息长度（native 字节序）+ JSON 内容**。入站消息上限 1 MB，出站上限 64 MiB。native host 实现需先读 4 字节长度，再读对应字节数解析 JSON，回写同理。

```python
# Python native host 读写示例（关键：长度前缀 + 本机字节序）
import sys, json, struct

def read_message():
    raw_len = sys.stdin.buffer.read(4)
    if not raw_len:
        sys.exit(0)
    length = struct.unpack('@I', raw_len)[0]  # @ = native 字节序
    return json.loads(sys.stdin.buffer.read(length).decode('utf-8'))

def send_message(obj):
    data = json.dumps(obj).encode('utf-8')
    sys.stdout.buffer.write(struct.pack('@I', len(data)))
    sys.stdout.buffer.write(data)
    sys.stdout.buffer.flush()

while True:
    msg = read_message()
    send_message({'echo': msg})

```

---

## 八、WASM 在插件中的加载与 CSP 配置

MV3 默认 CSP 禁止 `wasm-eval` 之外的动态求值。要在插件中实例化 WebAssembly，需在 manifest 的 CSP 中加入 `'wasm-unsafe-eval'`。

```jsonc
// manifest.json
{
  "content_security_policy": {
    "extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'"
  }
}

```

```js
// 把 .wasm 作为扩展资源打包，用 fetch + instantiateStreaming 加载
async function loadWasm() {
  const url = chrome.runtime.getURL('engine.wasm');
  const { instance } = await WebAssembly.instantiateStreaming(fetch(url), {});
  return instance.exports;
}

```

SW 与 offscreen 均可加载 WASM。`.wasm` 文件需通过打包工具复制进产物目录，并确保其 MIME 为 `application/wasm`（扩展内置资源由 Chrome 正确处理）。计算密集型逻辑（加解密、图像处理、压缩）放进 WASM 可显著提速。

---

## 九、安全加固

### MV3 CSP 详解

MV3 对扩展页面（`extension_pages`）强制严格 CSP：默认 `script-src 'self'`，禁止内联脚本、`eval`、远程脚本加载。`sandbox` 键可为沙箱页面设置更宽松策略。安全要点：

- 不得从远程加载并执行代码（MV3 政策硬性要求），第三方库必须打包进扩展。
- `'unsafe-inline'`、`'unsafe-eval'` 在 `extension_pages` 不被允许（仅 WASM 例外用 `'wasm-unsafe-eval'`）。

### 防止内容脚本被页面攻击

内容脚本与页面共享 DOM，但运行在隔离世界（isolated world），变量不互通。然而 DOM 是共享攻击面：页面可篡改 DOM、覆盖原型方法。

```js
// 错误：直接信任页面 DOM 文本并以特权身份转发，页面可注入恶意内容
const text = document.querySelector('#x').innerHTML;
chrome.runtime.sendMessage({ html: text });

// 正确：缓存原始引用、取 textContent、做白名单校验后再传
const { sendMessage } = chrome.runtime;       // 缓存，防页面覆盖
const text = document.querySelector('#x')?.textContent ?? '';
if (/^[\w-]{1,64}$/.test(text)) sendMessage.call(chrome.runtime, { token: text });

```

### 消息来源校验（sender 校验）

`onMessage` / `onMessageExternal` 必须校验 `sender`，否则任意页面（externally\_connectable）或恶意内容脚本可冒充触发特权操作。

```js
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
  // 校验来自本扩展、且来自可信内容脚本所在页面
  if (sender.id !== chrome.runtime.id) return; // 非本扩展，丢弃
  const url = sender.tab?.url ?? '';
  if (!url.startsWith('https://app.example.com/')) return; // 域名白名单
  // ...处理可信消息
});

// 外部消息（网页直连）单独校验来源 origin
chrome.runtime.onMessageExternal.addListener((msg, sender) => {
  if (sender.origin !== 'https://app.example.com') return;
});

```

### 避免 XSS（innerHTML / DOMPurify）

```js
// 错误：把不可信数据塞进 innerHTML，构成 XSS
el.innerHTML = userData;

// 正确 1：纯文本用 textContent
el.textContent = userData;

// 正确 2：必须渲染 HTML 时用 DOMPurify 消毒
import DOMPurify from 'dompurify';
el.innerHTML = DOMPurify.sanitize(userHtml, { ALLOWED_TAGS: ['b', 'a', 'p'] });

```

### 敏感数据加密

落盘的敏感数据（token、密码）用 Web Crypto AES-GCM 加密；密钥不可硬编码，由用户口令派生（PBKDF2）或存于 `storage.session`。

```js
async function deriveKey(password, salt) {
  const base = await crypto.subtle.importKey('raw', new TextEncoder().encode(password), 'PBKDF2', false, ['deriveKey']);
  return crypto.subtle.deriveKey(
    { name: 'PBKDF2', salt, iterations: 200000, hash: 'SHA-256' },
    base, { name: 'AES-GCM', length: 256 }, false, ['encrypt', 'decrypt'],
  );
}

async function encrypt(key, plaintext) {
  const iv = crypto.getRandomValues(new Uint8Array(12));
  const data = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, new TextEncoder().encode(plaintext));
  return { iv: [...iv], data: [...new Uint8Array(data)] };
}

```

### 最小权限

仅声明必需权限；用 `optional_permissions` \+ `chrome.permissions.request()` 在用户触发时按需申请，而非启动即索要 `<all_urls>`。`host_permissions` 尽量精确到具体域名。最小权限既降低审核风险，也减少被攻破后的影响面。

---

## 十、多上下文并发与竞态

SW、多个标签页内容脚本、popup、options 可能同时读写同一份 storage，产生读改写（read-modify-write）竞态：两个上下文都读到旧值、各自加一、后写覆盖先写，丢失更新。

### 用 navigator.locks 串行化

`navigator.locks`（Web Locks API）在 SW、offscreen、页面间共享锁名，可跨上下文串行化临界区。

```js
// 任意上下文调用：同名锁保证读改写原子
async function incrementCounter() {
  await navigator.locks.request('counter-lock', async () => {
    const { counter = 0 } = await chrome.storage.local.get('counter');
    await chrome.storage.local.set({ counter: counter + 1 }); // 临界区内独占
  });
}

```

### 用队列串行化（单上下文内）

同一 SW 内的高频任务可用 Promise 链队列，确保按序执行，避免交叉。

```js
let chain = Promise.resolve();
function enqueue(task) {
  chain = chain.then(task).catch((e) => console.error(e));
  return chain;
}
// enqueue(() => doWrite(a)); enqueue(() => doWrite(b)); // 严格顺序执行

```

`chrome.storage` 单次 `set` 本身原子，但"读—改—写"跨多次调用不原子，必须显式加锁或排队。

---

## 十一、自动化测试

### vitest 单测（mock chrome API）

`chrome.*` 在测试环境不存在，需 mock。用 `@webext-core/fake-browser` 或 `sinon-chrome` 提供假实现。

```js
// vitest.setup.ts
import { fakeBrowser } from '@webext-core/fake-browser';
import { beforeEach, vi } from 'vitest';

vi.stubGlobal('chrome', fakeBrowser); // 全局注入假 chrome
beforeEach(() => fakeBrowser.reset()); // 每个用例前重置状态

```

```js
// counter.test.ts
import { describe, it, expect } from 'vitest';
import { incrementCounter } from '../src/counter';

describe('counter', () => {
  it('读改写后计数加一', async () => {
    await chrome.storage.local.set({ counter: 5 });
    await incrementCounter();
    const { counter } = await chrome.storage.local.get('counter');
    expect(counter).toBe(6);
  });
});

```

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

E2E 需用真实 Chromium 加载已构建产物，验证内容脚本注入、popup 渲染等真实行为。MV3 下需用持久化上下文加载未打包扩展。

```js
// e2e/fixtures.ts
import { test as base, chromium } from '@playwright/test';
import path from 'node:path';

const pathToExtension = path.resolve('dist'); // 已构建产物目录

export const test = base.extend({
  context: async ({}, use) => {
    const context = await chromium.launchPersistentContext('', {
      channel: 'chromium',
      args: [
        `--disable-extensions-except=${pathToExtension}`,
        `--load-extension=${pathToExtension}`,
      ],
    });
    await use(context);
    await context.close();
  },
  extensionId: async ({ context }, use) => {
    let [sw] = context.serviceWorkers();
    if (!sw) sw = await context.waitForEvent('serviceworker'); // 等 SW 起来拿 ID
    await use(sw.url().split('/')[2]);
  },
});

```

```js
// e2e/popup.spec.ts
import { test } from './fixtures';
import { expect } from '@playwright/test';

test('popup 正常渲染', async ({ page, extensionId }) => {
  await page.goto(`chrome-extension://${extensionId}/popup.html`);
  await expect(page.getByRole('button', { name: '同步' })).toBeVisible();
});

```

---

## 十二、CI/CD

### GitHub Actions 自动构建 + 打 zip + 自动上架

打包产物用 zip；上架用各商店 API。Chrome Web Store 用 `chrome-webstore-upload`，凭据为 `CLIENT_ID` / `CLIENT_SECRET` / `REFRESH_TOKEN`，存进仓库 Secrets。

```yaml
# .github/workflows/release.yml
name: release
on:
  push:
    tags: ['v*']
jobs:
  build-and-publish:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 20, cache: 'npm' }
      - run: npm ci
      - run: npm run build          # 产物输出到 dist/
      - run: npm test               # 单测门禁
      - name: 打包 zip
        run: cd dist && zip -r ../extension.zip . && cd ..

      # 上架 Chrome Web Store
      - name: Publish to Chrome Web Store
        run: |
          npx chrome-webstore-upload-cli@3 upload \
            --source extension.zip \
            --extension-id "$CWS_EXTENSION_ID" \
            --client-id "$CWS_CLIENT_ID" \
            --client-secret "$CWS_CLIENT_SECRET" \
            --refresh-token "$CWS_REFRESH_TOKEN" \
            --auto-publish
        env:
          CWS_EXTENSION_ID: ${{ secrets.CWS_EXTENSION_ID }}
          CWS_CLIENT_ID: ${{ secrets.CWS_CLIENT_ID }}
          CWS_CLIENT_SECRET: ${{ secrets.CWS_CLIENT_SECRET }}
          CWS_REFRESH_TOKEN: ${{ secrets.CWS_REFRESH_TOKEN }}

      - name: 归档构建产物
        uses: actions/upload-artifact@v4
        with: { name: extension, path: extension.zip }

```

Edge Add-ons 用 Partner Center API（`PRODUCT_ID` \+ API key 上传同一 zip）；Firefox AMO 用 `web-ext sign` 或 AMO API（`JWT_ISSUER` \+ `JWT_SECRET`）。三店可用同一 zip 产物（注意 Firefox 对 MV3 SW 的差异，必要时输出 `background.scripts` 兼容版）。各店 Secrets 分别配置，建议拆成独立 job 并行发布、互不阻塞。

---

## 最佳实践

1. **顶层同步注册所有事件监听器，异步逻辑下沉到处理函数**。SW 唤醒会重跑脚本，延迟注册会丢事件。监听器同步注册保证唤醒即就绪：  
```js  
chrome.runtime.onMessage.addListener(handle); // 顶层  
async function handle(m, s, send) { const c = await chrome.storage.local.get('c'); send(c); return true; }  
```
2. **用 alarms + 事件驱动替代保活与 setInterval**。让 SW 自由回收，按需唤醒，省内存省电、契合 MV3 设计。只有维持长连接推送等极少场景才用官方支持的续命方式：  
```js  
chrome.alarms.create('sync', { periodInMinutes: 5 });  
chrome.alarms.onAlarm.addListener((a) => a.name === 'sync' && syncData());  
```
3. **跨上下文读改写用 navigator.locks 串行化**。SW 与多标签页并发写同一 storage 会丢更新，Web Locks 跨上下文加锁保证原子：  
```js  
await navigator.locks.request('cfg', async () => {  
  const { n = 0 } = await chrome.storage.local.get('n');  
  await chrome.storage.local.set({ n: n + 1 });  
});  
```
4. **storage / 消息批量化，高频事件防抖**。每次 IPC 都有成本，聚合写入与合并发送大幅降低跨进程开销：  
```js  
await chrome.storage.local.set(Object.fromEntries(items.map((i) => [i.id, i])));  
```
5. **所有 onMessage 校验 sender，所有 HTML 渲染消毒**。校验 `sender.id` 与来源域名防伪造，用 `textContent` 或 DOMPurify 防 XSS：  
```js  
if (sender.id !== chrome.runtime.id) return;  
el.innerHTML = DOMPurify.sanitize(html);  
```
6. **SW 中无 DOM 的能力一律走 Offscreen Document，并用 getContexts 去重**。`DOMParser`、剪贴板、`Audio` 等需 offscreen，创建前检测避免重复创建抛错：  
```js  
const c = await chrome.runtime.getContexts({ contextTypes: ['OFFSCREEN_DOCUMENT'] });  
if (!c.length) await chrome.offscreen.createDocument({ url, reasons: ['DOM_PARSER'], justification: '解析 HTML' });  
```
7. **最小权限 + 按需申请**。用 `optional_permissions` 与 `chrome.permissions.request()` 在用户操作时申请，`host_permissions` 精确到域名，降低审核与攻击面：  
```js  
const granted = await chrome.permissions.request({ origins: ['https://api.example.com/*'] });  
```

---

## 常见陷阱

1. **现象**：插件运行一段时间后定时任务停了、消息无人响应。  
**原因**：用了 `setInterval` 或把状态放在 SW 全局变量里，SW 空闲 30 秒被回收，定时器和内存状态全丢。  
**解决**：定时改用 `chrome.alarms`；状态持久化到 `chrome.storage` / IndexedDB；监听器顶层同步注册。
2. **现象**：偶发的"我明明发了消息，但回调拿到 undefined / 通道关闭"。  
**原因**：异步消息处理器没 `return true`，Chrome 认为同步处理完毕，提前关闭了消息通道，`sendResponse` 失效。  
**解决**：在 `onMessage` 监听器中需要异步回复时显式 `return true`，并确保 `sendResponse` 一定被调用。
3. **现象**：`chrome.offscreen.createDocument` 抛 "Only a single offscreen document may be created"。  
**原因**：未检测已有文档就重复创建，或并发创建存在竞态。  
**解决**：创建前用 `chrome.runtime.getContexts({ contextTypes: ['OFFSCREEN_DOCUMENT'] })` 检测；用一个 `creating` Promise 串行化并发创建请求。
4. **现象**：多个标签页同时操作后，storage 里的计数/列表丢了部分更新。  
**原因**：跨上下文读改写竞态，后写覆盖先写。  
**解决**：用 `navigator.locks.request()` 把读改写包成临界区，或在单上下文用 Promise 队列串行化。
5. **现象**：DNR 规则不生效或命中了不该命中的请求。  
**原因**：`priority` / action 优先级理解有误（同 priority 下 `allow` \> `block` \> `redirect`），或 `regexFilter` 写法不符合 RE2，或超过正则规则上限。  
**解决**：用 `testMatchOutcome()` 预测命中，开发期用未打包扩展开 `onRuleMatchedDebug` 观察实际命中，必要时提高目标规则 `priority`。

---

## 参见

- [浏览器插件开发完全指南](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/)
- [浏览器插件-API速查大全](https://blog.vercanti.com/liu-lan-qi-cha-jian-chrome-api-quan-liang-su-cha-da-quan/)
- [浏览器插件-中级开发指南](https://blog.vercanti.com/liu-lan-qi-cha-jian-zhong-ji-kai-fa-zhi-nan-manifest-v3-vite-vue/)
- [浏览器插件-设计模式与优雅架构](https://blog.vercanti.com/liu-lan-qi-cha-jian-she-ji-mo-shi-yu-you-ya-jia-gou/)
- [浏览器插件-调试与排错手册](https://blog.vercanti.com/liu-lan-qi-cha-jian-diao-shi-yu-pai-cuo-shou-ce/)
- [浏览器插件-避坑与开发技巧](https://blog.vercanti.com/liu-lan-qi-cha-jian-bi-keng-yu-kai-fa-ji-qiao/)
- [TypeScript最佳实践](https://blog.vercanti.com/typescript-zui-jia-shi-jian/)