浏览器插件:基础概念与架构模型

浏览器插件不是一个网页,也不是一个普通应用,而是一组运行在浏览器内、彼此隔离、靠消息通信协作的独立脚本上下文。理解插件开发的关键,不在于记住某个 API,而在于建立正确的心智模型:把一个插件看作"浏览器里的微服务集群"——后台 Service Worker 是常驻调度服务(但会被随时回收),内容脚本(Content Script)是潜伏在每个网页里的探针,Popup/Options/Side Panel 是按需启动的前端进程,它们都没有共享内存,只能通过消息总线和共享存储交换数据。本文是该系列的概念篇,目标是让你在写第一行代码前就理解"谁能做什么、谁和谁

分享

官方文档:

浏览器插件不是一个网页,也不是一个普通应用,而是一组运行在浏览器内、彼此隔离、靠消息通信协作的独立脚本上下文。理解插件开发的关键,不在于记住某个 API,而在于建立正确的心智模型:把一个插件看作"浏览器里的微服务集群"——后台 Service Worker 是常驻调度服务(但会被随时回收),内容脚本(Content Script)是潜伏在每个网页里的探针,Popup/Options/Side Panel 是按需启动的前端进程,它们都没有共享内存,只能通过消息总线和共享存储交换数据。本文是该系列的概念篇,目标是让你在写第一行代码前就理解"谁能做什么、谁和谁怎么说话、为什么不能这样写"。

概述

浏览器插件(Browser Extension)是用 HTML、CSS、JavaScript 编写、由浏览器加载并赋予额外权限(如读写跨域网络、修改任意网页、访问标签页和书签)的软件包。它运行在浏览器进程之内,因此能做到普通网页做不到的事,但同时被沙箱(Sandbox)和权限模型严格约束。

Why(为什么用插件而非网页):网页受同源策略(Same-Origin Policy)限制,无法跨域读取数据、无法修改其他站点、无法常驻后台。插件通过声明权限突破这些边界,实现广告拦截、密码管理、页面增强、自动化采集等网页本身无法完成的功能。

When(何时选择插件方案):当需求是"在用户浏览任意网页时介入"(注入 UI、改写请求、抓取内容),或"提供一个跨站点、随浏览器常驻的工具"时,插件是合适载体。若功能可以在自己的网站内完成,则不需要插件。

下面先建立四个核心模型——执行上下文、隔离、生命周期、通信,再讲权限与安全。

一、插件是什么,能做什么,不能做什么

能力边界(沙箱内 vs 沙箱外)

插件运行在浏览器授予的特权上下文中,但仍受沙箱约束。下表列出典型能力与禁区。

维度 插件能做 插件不能做
网络 跨域 fetch(声明 host_permissions 后);用 declarativeNetRequest 拦截/改写请求 用 MV3 阻塞式 webRequest 实时改包(企业策略外已禁用)
页面 向任意匹配网页注入脚本与样式,读写其 DOM 直接读取页面 JS 的变量/函数(隔离世界阻断)
浏览器 操作标签页、书签、历史、Cookie、下载、上下文菜单 访问本地文件系统任意路径(仅限用户授权的下载/文件选择)
存储 chrome.storage、IndexedDB、CacheStorage 持久化数据 在 Service Worker 全局变量里长期保存状态(会被回收)
代码 执行打包进扩展、经审核的 JS 加载并执行远程 JS(MV3 禁止远程代码)
进程 各上下文是独立 JS 环境 在上下文间共享内存/全局对象

核心约束有两条贯穿全文:MV3 禁止远程代码(所有可执行 JS 必须随包提交、经 Chrome Web Store 审核),以及Service Worker 无 DOM、会被回收(不能依赖常驻内存)。

二、WebExtensions 标准与浏览器实现

WebExtensions 是各家浏览器趋同的扩展 API 标准,最初由 Chrome 的扩展模型演化而来,Firefox、Edge、Safari 都在此基础上实现,API 命名空间高度一致(tabsstorageruntime 等)。

浏览器 引擎 API 命名空间 Promise 支持 Manifest 版本现状(2026-06) 备注
Chrome Chromium chrome.* 多数 API 支持 Promise 仅 MV3 MV2 已在稳定版停用
Edge Chromium chrome.*(兼容 browser.* 同 Chrome 仅 MV3 与 Chrome 包基本通用
Firefox Gecko browser.*(原生 Promise)/ 兼容 chrome.* 全面 Promise MV2 与 MV3 并行支持 后台用 Event Page,background.scripts
Safari WebKit browser.* 支持 MV2/MV3 需用 Xcode 打包为 App 扩展

实践要点:写跨浏览器插件时,用 browser.* 命名空间(配合 webextension-polyfill 在 Chrome 上补齐)能获得统一的 Promise 风格 API;只做 Chromium 系则直接用 chrome.*。本文示例统一用 chrome.*

三、Manifest V2 → V3 的演进

Manifest 是插件的入口清单文件 manifest.json,声明版本、权限、各上下文入口。Manifest V3(MV3)是当前唯一在 Chrome 稳定版受支持的版本,它对 V2 做了根本性重构。

三大根本变化

变化点 Manifest V2 Manifest V3 Google 给出的理由
后台 持久化 Background Page(常驻内存) 事件驱动的 Service Worker(空闲回收) 性能:常驻后台即使插件未工作也占用内存
远程代码 允许加载远程 JS(<script src=远程>、远程 eval 禁止,所有 JS 必须随包提交审核 安全:远程代码绕过商店审核,是恶意更新的主要通道
网络拦截 阻塞式 webRequest(可同步改写/阻断请求) declarativeNetRequest(声明规则,浏览器代为执行) 隐私+性能:旧模型让插件能看到全部流量,且每个请求都要往返插件代码
权限 host 权限与 API 权限混在 permissions host 权限独立到 host_permissions 透明:让用户更清楚插件能访问哪些站点
CSP 字符串形式,可放宽 对象形式,禁止 unsafe-eval、远程 script-src 安全:堵死注入入口

对开发者的影响(结论先行)

迁移到 MV3 后,三件事会直接改变你的写法:

  1. 后台不能存内存状态。原来在 background page 里 let cache = {} 长期累积数据的写法失效,必须改用 chrome.storage
  2. 拦截器要重写为声明式规则。广告拦截、请求改写类插件需把命令式逻辑翻译成 declarativeNetRequest 的 JSON 规则。
  3. 动态代码被堵死。不能再 eval、不能 new Function、不能加载 CDN 上的脚本,第三方库必须打包进扩展。

下面是 MV3 入口清单的最小骨架。

{
  "manifest_version": 3,
  "name": "示例插件",
  "version": "1.0.0",
  "background": {
    "service_worker": "background.js",
    "type": "module"
  },
  "content_scripts": [
    {
      "matches": ["https://*.example.com/*"],
      "js": ["content.js"],
      "run_at": "document_idle"
    }
  ],
  "action": {
    "default_popup": "popup.html"
  },
  "permissions": ["storage", "tabs"],
  "host_permissions": ["https://api.example.com/*"]
}

四、执行上下文模型

这是整个心智模型的核心。一个插件由多个互相隔离的 JS 上下文(Execution Context)组成,每个有独立的全局环境、生命周期和能力边界。

                    浏览器进程
   +-------------------------------------------------------------+
   |                                                             |
   |   +------------------+        +------------------------+    |
   |   | Service Worker   |<------>|  Popup / Options /     |    |
   |   | (后台调度,无DOM,|  消息   |  Side Panel / DevTools |    |
   |   |  会被回收)       |  总线   |  (有 DOM, 按需启停)    |    |
   |   +--------+---------+        +------------------------+    |
   |            ^                                                |
   |            | sendMessage / Port                            |
   |   +--------+--------------------------------------------+   |
   |   |        |          网页标签页 (Tab)                  |   |
   |   |  +-----+----------+        +---------------------+  |   |
   |   |  | Content Script |        |   页面自身的 JS     |  |   |
   |   |  | (Isolated World|<--DOM--→|   (MAIN World)      |  |   |
   |   |  |  隔离世界)     |postMessage                   |  |   |
   |   |  +----------------+        +---------------------+  |   |
   |   +--------------------------------------------------+   |   |
   |                                                          |   |
   |   +------------------+   (DOM 但不可见, 替 SW 干 DOM 活) |   |
   |   | Offscreen Doc    |<--------------------------------+    |
   |   +------------------+                                      |
   +-------------------------------------------------------------+
        共享层: chrome.storage(所有上下文可读写,作为"数据库")

各上下文对比

上下文 运行环境 有 DOM 生命周期 主要能力 主要限制
Service Worker(后台) 独立 Worker 线程,无 window 事件驱动,空闲约 30s 回收 几乎全部 chrome.* API;监听全局事件;调度 无 DOM;不能用全局变量存状态;不能用 XMLHttpRequest(用 fetch
Content Script(内容脚本) 注入到网页的隔离世界 是(页面 DOM) 随所在页面,页面卸载即销毁 读写页面 DOM;chrome.storage/runtime/i18n 子集 不能直接访问页面 JS 变量;多数 chrome.* 需通过消息转给后台
Popup(弹出页) 独立 HTML 页面 是(自己的 DOM) 打开时创建,关闭即销毁 完整 chrome.* API;做 UI 关闭即丢内存状态;不能跨标签持久
Options(选项页) 独立 HTML 页面 用户打开时存在 完整 chrome.* API;偏好设置 UI 同 Popup
Side Panel(侧边栏) 独立 HTML 页面 用户开启侧栏期间存在,可常驻 完整 chrome.* API;常驻侧边 UI Chrome 114+ 才有
DevTools 页面 挂在开发者工具内的页面 随 DevTools 面板存在 chrome.devtools.*、被检查页面信息 仅在 DevTools 打开时运行
Offscreen Document 不可见的隐藏文档 是(隐藏) 按 reason 创建,可能自动关闭 用 DOM/Web API 替无 DOM 的 SW 干活 同时只能存在一个;仅支持 chrome.runtime API

Offscreen Document:为 Service Worker 补上 DOM

Service Worker 没有 DOM,因此无法用 DOMParser 解析 HTML、无法用 <audio> 播放声音、无法访问 navigator.clipboard。Offscreen Document 是一个不可见的隐藏 HTML 文档,专门替后台执行这些依赖 DOM/Web API 的任务。

创建时必须声明 reason,取值如下表(列全文档定义的枚举)。

reason 枚举 用途 自动关闭
AUDIO_PLAYBACK 播放音频 无音频播放 30s 后自动关闭
CLIPBOARD 访问剪贴板 API
DOM_PARSER 使用 DOMParser 解析
DOM_SCRAPING 抓取 iframe 内 DOM
IFRAME_SCRIPTING 嵌入并脚本化 iframe
BLOBS 处理 Blob 对象
USER_MEDIA getUserMedia 媒体流
DISPLAY_MEDIA 屏幕/窗口捕获流
WEB_RTC WebRTC 功能
LOCAL_STORAGE 访问 localStorage
WORKERS 派生 Web Worker
GEOLOCATION 地理定位
BATTERY_STATUS 电池状态
MATCH_MEDIA matchMedia 查询
// background.js —— 在 Service Worker 中创建唯一的 offscreen 文档
async function ensureOffscreen() {
  // 同时只能有一个 offscreen 文档,先判断是否已存在
  const existing = await chrome.offscreen.hasDocument();
  if (existing) return;
  await chrome.offscreen.createDocument({
    url: "offscreen.html",
    reasons: ["DOM_PARSER"], // 必填:声明用途
    justification: "解析抓取到的 HTML 字符串", // 必填:给审核者看的理由
  });
}

五、隔离模型:Isolated World vs MAIN World

内容脚本默认运行在隔离世界(Isolated World),这是理解"为什么我读不到页面变量"的关键。

       同一个标签页内
  +---------------------------------------------+
  |                                             |
  |   Isolated World            MAIN World      |
  |  (Content Script)          (页面自己的 JS)  |
  |  +----------------+       +---------------+  |
  |  | 独立的 JS 堆   |       | 页面的 JS 堆  |  |
  |  | window.foo=1   |  ✗    | window.foo=2  |  |
  |  | (互不可见)     |<------| (互不可见)    |  |
  |  +-------+--------+       +-------+-------+  |
  |          |                       |          |
  |          +------ 共享同一份 ------+          |
  |                   DOM 树                     |
  |          (两边都能读写 document)            |
  +---------------------------------------------+
       跨世界通信只能走: DOM 事件 / window.postMessage

原理:内容脚本与页面 JS 运行在两个独立的 JS 执行环境,各有各的全局对象(window)、各有各的原型链,但它们共享同一棵 DOM 树。所以:

  • 内容脚本能 document.querySelector(...) 改 DOM——DOM 是共享的。
  • 内容脚本读不到页面里 window.someLib——JS 堆是隔离的。
  • 页面也篡改不了内容脚本注入的函数——这正是隔离要保护的安全边界(防止恶意页面劫持插件逻辑)。

正确与错误用法对比

// content.js(运行在 Isolated World)

// 错误:试图直接读取页面的 JS 变量——隔离世界看不到页面的 window 属性
console.log(window.__APP_STATE__); // undefined(不是页面里的那个值)

// 正确:通过共享 DOM 用 postMessage 与页面通信
window.postMessage({ source: "my-ext", type: "PING" }, "*");
window.addEventListener("message", (event) => {
  // 必须校验来源,否则任意页面/iframe 都能伪造消息
  if (event.source !== window) return;
  if (event.data?.source !== "page-app") return;
  console.log("收到页面回复:", event.data.payload);
});

若确实需要进入页面世界(例如改写页面里的 fetch),用 world: "MAIN" 注入。代价是失去隔离保护,且不能用任何 chrome.* API。

// manifest.json 片段:声明一个运行在 MAIN world 的内容脚本
{
  "content_scripts": [
    {
      "matches": ["https://*.example.com/*"],
      "js": ["inject-main.js"],
      "world": "MAIN",        // 进入页面世界
      "run_at": "document_start"
    }
  ]
}
维度 ISOLATED(默认) MAIN
能否访问页面 JS 变量/函数
能否用 chrome.* API 能(受限子集)
适用的 CSP 扩展的 CSP 页面自身的 CSP
安全性 高(与页面隔离) 低(与页面同环境,易被页面影响)
典型用途 读写 DOM、注入 UI hook 页面函数、改写 window.fetch

六、Service Worker 的事件驱动生命周期

MV3 后台是 Service Worker(SW),它不是常驻进程,而是按事件唤醒、空闲即回收的"无状态函数集合"。这是 MV3 最容易踩坑的地方。

   注册/安装              空闲              再次有事件
  +--------+   install   +--------+   30s无事件   +--------+   事件到达   +--------+
  | 安装中 |-----------> | 激活/  |------------->| 已终止 |----------->| 重新启动|
  |        | onInstalled | 运行中 |   被回收     | (内存清空)         | (重跑顶层)|
  +--------+             +--------+              +--------+            +--------+
                            ^                                              |
                            +----------------------------------------------+
                                 每次唤醒都从头执行 SW 顶层代码

关键事实(来自官方文档,核实于 2026-06-06)

  • SW 在以下情况被终止:30 秒无活动;单个请求超过 5 分钟;fetch 响应超过 30 秒。
  • 以下活动会重置空闲计时器:收到任意事件或 API 调用(Chrome 110+)、WebSocket 收发(116+)、长连接 Port 活动(114+)、Offscreen 消息(109+)、原生消息连接(105+)、最小周期 30s 的 alarm(120+)。
  • chrome.runtime.onInstalled 用于一次性初始化(建上下文菜单、写默认配置)。
  • chrome.runtime.onStartup 在浏览器配置启动时触发。
  • 被回收后,所有全局变量丢失,下次事件唤醒会重新执行 SW 顶层代码。

为什么不能用全局变量保存状态

// background.js

// 错误:用全局变量累积状态——SW 一旦被回收,counter 归零,数据丢失
let counter = 0;
chrome.action.onClicked.addListener(() => {
  counter += 1;            // 30s 空闲后 SW 终止,再点击时 counter 又是 0
  console.log(counter);
});
// 正确:状态写入 chrome.storage,唤醒后从存储恢复
chrome.action.onClicked.addListener(async () => {
  const { counter = 0 } = await chrome.storage.local.get("counter");
  const next = counter + 1;
  await chrome.storage.local.set({ counter: next }); // 持久化,跨回收存活
  console.log(next);
});

另一个高频坑:事件监听器必须在顶层同步注册,不能放进异步回调里——因为唤醒时只会重新执行顶层代码,晚注册的监听器会错过本次唤醒事件。

// 错误:在异步回调里注册监听器——SW 唤醒时来不及注册,事件丢失
chrome.storage.local.get("config").then(() => {
  chrome.runtime.onMessage.addListener(handler); // 注册太晚
});

// 正确:监听器在文件顶层同步注册
chrome.runtime.onMessage.addListener(handler);
function handler(msg, sender, sendResponse) {
  // 在这里再做异步工作
  return true; // 异步回复时必须 return true 保持通道开启
}

七、通信模型概览

各上下文无共享内存,协作只能靠三种机制。本节只讲模型,具体 API 见 浏览器插件-API速查大全

     一次性消息 (sendMessage / onMessage)
  Content Script  ────────请求/响应───────▶  Service Worker
        ▲                                          │
        └──────────────回复─────────────────────────┘

     长连接 (Port: connect / onConnect)
  Popup  ◀═══════════ 双向持续通道 ═══════════▶  Service Worker
        (适合频繁/流式通信,连接期间保活 SW)

     共享存储 (chrome.storage + onChanged)
  任意上下文 ──写──▶ [ chrome.storage ] ──onChanged 广播──▶ 其它上下文
        (把存储当数据库,用 onChanged 做跨上下文的"事件广播")
机制 形态 适用场景 特点
一次性消息 sendMessage / onMessage 请求-响应式调用 简单;异步回复需 return true
长连接 Port connect / onConnect 频繁、流式、有状态会话 连接期间保活 SW;可双向推送
共享存储 chrome.storage + onChanged 跨上下文共享数据/广播变更 可持久;不直接点对点,靠监听变更
DOM 桥接 window.postMessage 内容脚本 ↔ 页面 MAIN world 跨世界唯一通道;必须校验来源

选型原则:一问一答用一次性消息;高频或需服务端主动推送用 Port;要让多个上下文同步同一份数据用 storage + onChanged。

八、权限模型

插件能力来自声明的权限。MV3 把权限分为安装时(install-time)与运行时(runtime)两类,并把 host 权限单独列出。

字段 类型 默认值 说明
permissions string[] [] API 权限,如 storagetabsalarms,安装时一次性授予
optional_permissions string[] [] 可选 API 权限,运行时经 chrome.permissions.request 动态申请
host_permissions string[] [] 安装时申请的主机访问权限,按匹配模式列出,如 https://*.example.com/*
optional_host_permissions string[] [] 运行时动态申请的主机权限
activeTab (在 permissions 中) 用户主动点击插件时,临时授予当前标签页访问权,无需广域 host 权限

最小权限原则

// 错误:申请 <all_urls> 会让所有站点都触发权限警告,审核与信任成本陡增
{
  "permissions": ["tabs", "webNavigation", "cookies"],
  "host_permissions": ["<all_urls>"]
}
// 正确:只声明真正需要的站点,配合 activeTab 处理"用户点击时才介入"的场景
{
  "permissions": ["storage", "activeTab"],
  "host_permissions": ["https://api.example.com/*"]
}

activeTab 是降低权限的利器:用户点击插件图标的那一刻,浏览器临时授予当前标签页的脚本注入与读取权限,无需声明广域 host 权限,安装时也不弹出吓人的"读取你所有网站数据"警告。

运行时申请可选权限:

// 在用户触发某功能时再申请,而不是一上来全要
document.querySelector("#enable-feature").addEventListener("click", async () => {
  const granted = await chrome.permissions.request({
    origins: ["https://extra-site.com/*"],
  });
  if (!granted) return; // 用户拒绝则降级处理
  // 已授权,继续
});

九、安全模型

MV3 的安全基线由三条规则组成:强制 CSP、禁止远程代码、显式声明可被网页访问的资源。

内容安全策略(CSP)

MV3 用对象形式声明 CSP,且对扩展页面强制禁止 unsafe-eval 与远程脚本源。

字段 类型 默认值 说明
content_security_policy.extension_pages string script-src 'self'; object-src 'self' 扩展自身页面(popup/options 等)的 CSP,不允许放宽到远程或 eval
content_security_policy.sandbox string (无沙箱默认值) 沙箱页面(如需 eval 的场景)的 CSP

禁止远程代码

MV3 下所有可执行 JS 必须随扩展包提交、经商店审核。这意味着:

// 错误:从 CDN 动态加载脚本——MV3 禁止执行远程代码,CSP 会拦截
const s = document.createElement("script");
s.src = "https://cdn.example.com/lib.js";
document.head.appendChild(s);

// 错误:eval / new Function 动态执行字符串——被扩展页 CSP 禁止
eval("doSomething()");

// 正确:把第三方库通过构建工具打包进扩展,作为本地资源 import
import { doSomething } from "./vendor/lib.js"; // 随包提交,受审核
doSomething();

构建打包方案见 浏览器插件-技术栈与脚手架

web_accessible_resources

默认情况下,扩展内的文件(图片、脚本、字体)不能被网页直接引用。若要让页面或内容脚本注入的元素引用扩展资源,必须显式声明。

字段 类型 默认值 说明
resources string[] [] 可被访问的资源路径(支持通配)
matches string[] [] 允许访问这些资源的页面匹配模式
extension_ids string[] [] 允许访问的其它扩展 ID
use_dynamic_url boolean false true 时用随会话变化的动态 URL,降低被指纹识别的风险
// 让 example.com 的页面能引用扩展里的 inject.js 和图标
{
  "web_accessible_resources": [
    {
      "resources": ["inject.js", "icons/*.png"],
      "matches": ["https://*.example.com/*"],
      "use_dynamic_url": true
    }
  ]
}

暴露最少的资源、限定最窄的 matches,避免把扩展内部脚本暴露给任意站点(否则可能被用于指纹识别或被恶意页面利用)。

十、心智模型:把插件当作"浏览器里的微服务集群"

把整套架构映射到微服务,能快速建立直觉:

微服务概念 插件对应 说明
调度/网关服务 Service Worker 中枢逻辑,路由消息,但"无状态、会被缩容到 0"
边车探针(Sidecar) Content Script 部署到每个网页"实例"旁,采集/改写本地 DOM
前端进程 Popup / Options / Side Panel 按需启停,关闭即销毁
共享数据库 chrome.storage / IndexedDB 所有服务读写的唯一持久层
消息队列/RPC sendMessage / Port 服务间唯一通信方式,无共享内存
离线 worker Offscreen Document 替无 DOM 的调度服务跑需要 DOM 的批处理
服务网格隔离 Isolated World 探针与宿主(页面)隔离,防互相污染

由此推导出三条贯穿全系列的设计准则:

  1. 无状态优先:任何上下文随时可能被销毁,状态必须落到存储层。
  2. 消息即接口:上下文间不要假设能共享对象,所有协作走消息。
  3. 最小信任面:每个上下文只拿它必需的权限与资源,隔离边界不轻易打破。

最佳实践

  1. 一句话原则:状态一律落 storage,不依赖 SW 内存。
    Service Worker 会在 30 秒空闲后被回收,全局变量随之清空。所有需要跨事件存活的数据都写入 chrome.storage,唤醒后再读回。

    chrome.runtime.onMessage.addListener(async (msg) => {
      const { sessions = {} } = await chrome.storage.session.get("sessions");
      sessions[msg.tabId] = msg.data;
      await chrome.storage.session.set({ sessions }); // session 区随浏览器会话存活
    });
    
  2. 一句话原则:事件监听器在顶层同步注册。
    SW 唤醒时只重跑顶层代码,监听器若在异步回调里注册会错过当次事件。把所有 addListener 放在模块顶层。

    // 顶层同步注册,确保每次唤醒立即就位
    chrome.runtime.onInstalled.addListener(handleInstall);
    chrome.alarms.onAlarm.addListener(handleAlarm);
    
  3. 一句话原则:默认用 ISOLATED 世界,仅在必须 hook 页面时才进 MAIN。
    隔离世界保护插件逻辑不被页面篡改。除非要改写页面的 fetch/全局函数,否则不要用 world: "MAIN"

    // 仅对确需进入页面世界的脚本单独声明 world: "MAIN",其余保持默认
    await chrome.scripting.executeScript({
      target: { tabId },
      world: "MAIN",
      func: () => { /* hook 页面 fetch */ },
    });
    
  4. 一句话原则:用 activeTab + 最窄 host_permissions 替代 <all_urls>
    广域权限触发严苛审核与用户警告。多数"点击后处理当前页"的需求用 activeTab 即可。

    { "permissions": ["activeTab", "scripting"] }
    
  5. 一句话原则:第三方库打包进扩展,绝不远程加载。
    MV3 禁止远程代码,CSP 会拦截 CDN 脚本与 eval。用打包工具把依赖编译进本地文件。

    import dayjs from "dayjs"; // 由构建工具打包进扩展产物
    
  6. 一句话原则:DOM 相关后台任务交给 Offscreen Document,并复用单例。
    SW 无 DOM,需要 DOMParser、剪贴板、音频时创建 offscreen 文档,且全局只允许一个,创建前先检测。

    if (!(await chrome.offscreen.hasDocument())) {
      await chrome.offscreen.createDocument({
        url: "offscreen.html",
        reasons: ["CLIPBOARD"],
        justification: "写入系统剪贴板",
      });
    }
    
  7. 一句话原则:跨世界消息必须校验来源。
    window.postMessage 会被同页任意 iframe/脚本收到,处理前校验 event.source 与自定义标识,防止伪造。

    window.addEventListener("message", (e) => {
      if (e.source !== window || e.data?.source !== "page-app") return;
      // 可信消息
    });
    

常见陷阱

陷阱一:后台用全局变量缓存,过一会儿数据"自己丢了"

  • 现象:插件运行一段时间后,后台维护的计数器、缓存、定时器状态突然清零或失效。

  • 原因:Service Worker 在 30 秒空闲后被浏览器回收,进程内存(含全局变量、setTimeout 句柄)全部释放;下次事件触发时是全新进程。

  • 解决:状态写入 chrome.storage;定时任务改用 chrome.alarms(持久、可跨回收触发),不要用 setTimeout/setInterval

    // 用 alarms 替代 setInterval,回收后仍能按时唤醒 SW
    chrome.alarms.create("sync", { periodInMinutes: 5 });
    chrome.alarms.onAlarm.addListener((alarm) => {
      if (alarm.name === "sync") doSync();
    });
    

陷阱二:内容脚本里 console.log(window.页面变量) 全是 undefined

  • 现象:明明页面控制台能打印出 window.__DATA__,内容脚本里却读到 undefined

  • 原因:内容脚本运行在隔离世界,拥有独立的 window,与页面 JS 堆隔离,只共享 DOM,不共享 JS 变量。

  • 解决:经共享 DOM 用 window.postMessage 与页面通信;或注入一段 world: "MAIN" 脚本去页面世界取值后再 postMessage 回隔离世界。

    // inject-main.js(world: MAIN):在页面世界取值后回传
    window.postMessage({ source: "page-app", payload: window.__DATA__ }, "*");
    

陷阱三:onMessage 里异步回复,结果回调收到 undefined

  • 现象:内容脚本 sendMessage 后拿到的响应是 undefined,明明后台算出了结果。

  • 原因:onMessage 监听器若做异步工作(如 await fetch),同步函数已返回,消息通道默认关闭,sendResponse 失效。

  • 解决:在监听器里 return true 显式保持通道开启,待异步完成后再调用 sendResponse

    chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
      (async () => {
        const data = await fetch(msg.url).then((r) => r.json());
        sendResponse(data);
      })();
      return true; // 关键:保持消息通道开启直到 sendResponse 被调用
    });
    

陷阱四:页面引用扩展内文件报 net::ERR_BLOCKED_BY_CLIENT 或 404

  • 现象:内容脚本往页面插入 <img src="chrome-extension://.../icon.png"> 或注入脚本,浏览器拒绝加载。

  • 原因:扩展资源默认不对网页开放,未在 web_accessible_resources 声明的资源无法被页面上下文引用。

  • 解决:在 manifest 的 web_accessible_resources 中声明该资源及允许的 matches,并用 chrome.runtime.getURL 生成正确 URL。

    const url = chrome.runtime.getURL("icon.png"); // 生成合法的扩展资源 URL
    img.src = url;
    

参见

阅读更多

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