Skip to content
🎯 场景实战第 15 / 18 节

Content Script 与 Service Worker:插件的大脑和手脚 ​

上一课你用 manifest.json 把插件的骨架搭好了。这一课认识插件里最重要的两个角色:service worker 是大脑,待在后台响应事件;content script 是手脚,钻进网页里读页面、改页面。分清这两个角色,你才能把需求说清楚,AI 才能把代码放对文件。

学完你能做什么 ​

  • 说清 content script 和 service worker 各自能做什么、不能做什么
  • 在 manifest.json 里声明 background service worker 和 content_scripts,并让它们真正跑起来
  • 亲手验证「隔离世界(isolated world)」:网页和你的脚本互相看不见对方的 JS 变量,却共享同一份 DOM
  • 知道两者的日志分别去哪里看——这是下一课调试的地基

你现在的困境 ​

  • 让 AI 写插件,它一口气生成了 service-worker.js 和 content-script.js,你不知道为什么要有两个、各管什么
  • 脚本明明注入了页面,却读不到网页自己的变量;想在后台改页面,AI 又说「做不到」
  • 代码里打了 console.log,却找不到输出:扩展页没有,网页控制台也没有

什么时候用这一招 ​

这节课适合这样的你:想给插件加上「能改网页」或「能在后台响应浏览器事件」的能力,或者想看懂 AI 生成的插件文件结构。

而且不想:把代码放错文件反复返工,报错了也不知道该去哪里找日志。

🎒 开始前的准备 ​

  • 读完前置课程《manifest.json 详解:MV3 字段速查》
  • 手里有一个能 Load unpacked 本地加载的插件项目(Hello Extensions 那种就行)
  • 会打开 chrome://extensions 并重载插件(点插件卡片上的刷新图标)
  • 会打开网页的 DevTools(右键页面 →「检查」)看 Console

核心思路 ​

把插件想成一个人:

  • service worker(大脑,也就是插件后台脚本):官方称它为插件的「central event handler」(事件中枢)。它不依附任何页面,在浏览器后台等事件——关标签、点通知、导航到新页面都归它管;「需要时加载,空闲时卸载」。它不能访问 DOM,想改页面只能吩咐 content script 去干。
  • content script(手脚):官方定义是「运行在网页环境里的文件」,用标准 DOM 读页面、改页面、把信息传回插件本体。
  • 隔离世界(isolated world):content script 活在一个私有执行环境里,网页和其他插件都碰不到它;网页的 JS 变量它也看不见。但 DOM 是共享的——两边看到的是同一张页面。
content script(手脚)service worker(大脑)
运行位置具体网页里浏览器后台,不依附任何页面
能否操作 DOM✅ 能读能改❌ 不能
生命周期跟随页面按需启动,空闲即停
直接可用的扩展 API少量(dom、i18n、storage、runtime 部分方法)作为事件中枢响应各类扩展事件
典型任务改页面样式、读页面内容监听标签/通知等事件、管理标签
日志在哪看宿主网页的 Console扩展页 Inspect views

💡 老教程对不上号?

MV2 老教程里的 "background script" 就是今天的 service worker——名字变了,角色没变。现在开发插件只写 MV3,别照抄老代码。

跟我做 ​

第 1 步:声明 service worker(插件后台脚本) ​

为什么:大脑要先注册,Chrome 才知道该在后台启动谁。

在 manifest.json 里加一段 background:

json
"background": {
  "service_worker": "service-worker.js"
}

在插件根目录新建 service-worker.js:

js
console.log("service worker 已启动");

回 chrome://extensions 重载插件。

你应该看到:插件卡片上出现 Inspect views 区域,旁边有一条蓝色的 service worker 链接;点开它,DevTools 的 Console 里躺着你的日志。

💡 日志一会儿就「没了」?

service worker 按需启动、空闲即停。过一会儿它睡了,不代表坏了——下次有事件它会自己醒来。

  • 小检查:能在 Inspect views 打开的 DevTools 里看到「service worker 已启动」

第 2 步:写第一个 content script(content_scripts 静态注入) ​

为什么:手脚不会自己长出来——要在 manifest 里用 content_scripts 静态声明,它才会自动注入你指定的网站。

在 manifest.json 里加(matches 必填,决定注入哪些页面;下面是官方文档的示例,把域名换成你真正想作用的网站):

json
"content_scripts": [
  {
    "matches": ["https://*.nytimes.com/*"],
    "js": ["content-script.js"]
  }
]

官方示例里还有一行 "css": ["my-styles.css"]——content script 可以只注 JS、只注 CSS,或两者都注,按需取舍。

在插件根目录新建 content-script.js:

js
console.log("[content script] 注入成功,页面标题:", document.title);
document.body.style.border = "5px solid red";

重载插件,然后刷新目标网页——改 content script 必须「重载插件 + 刷新宿主页面」两步都做。

你应该看到:目标网页四周出现红框;网页 DevTools 的 Console 里有你打的日志(注意:是网页的 Console,不是扩展页的)。

  • 小检查:红框出现,且网页 Console 里能看到 content script 的日志

第 3 步:亲眼验证隔离世界(isolated world) ​

为什么:这是新手最容易困惑的地方——「脚本明明在页面里跑,怎么读不到网页的变量?」答案是隔离世界,自己动手看一遍最直观。

把 content-script.js 改成:

js
var extVar = "我来自插件";
console.log("[content script] 我能读到页面标题:", document.title);
document.body.style.border = "5px solid red";

// 5 秒后再读网页变量,留时间给你在网页控制台定义它
setTimeout(function () {
  console.log("[content script] 读网页变量 pageVar:", window.pageVar);
}, 5000);

重载插件 + 刷新页面,然后在 5 秒内切到网页的 Console,输入:

js
var pageVar = "我是网页自己的变量";

等 5 秒的日志出来后,再在网页 Console 里分别输入 extVar 和 document.body.style.border。

你应该看到:

  • content script 打出 读网页变量 pageVar:undefined——它看不见网页的变量

  • 网页 Console 输入 extVar 报 ReferenceError——网页也看不见你的变量

  • document.body.style.border 返回 "5px solid red"——DOM 是两边共享的

  • 小检查:三条现象都亲眼看到了,能对上「变量隔离、DOM 共享」这句话

第 4 步:把任务分给对的角色(什么时候用谁) ​

为什么:分清大脑和手脚,你才能把需求正确地告诉 AI,它才不会把代码写错文件。

你的需求交给谁
在具体网页上注入代码、改页面content script
不依赖页面、在后台等事件、监听浏览器行为、弹通知/管理标签service worker
点工具栏图标弹出小窗口(popup)action + popup
拦截、屏蔽或修改网络请求declarativeNetRequest

content script 能直接用的扩展 API 很少——官方列出来的是 dom、i18n、storage 和 runtime 的部分方法(如 runtime.sendMessage()、runtime.onMessage)。要用别的 API,就得靠消息转发给 service worker 代办——所以这两个文件经常成对出现,不是 AI 写多了。

你应该看到:拿这张表对照 AI 生成的代码,改页面的逻辑落在 content-script.js,事件监听落在 service-worker.js——各就各位,你就能看懂插件的结构了。

检查点 ✅ ​

  • 扩展页 Inspect views 能打开 service worker 的 DevTools,看到启动日志
  • 目标网页出现 content script 的改动,日志在网页 Console 里
  • 能演示:网页变量和 content script 变量互不可见,DOM 共享
  • 能说出 4 类需求分别交给哪个角色
  • 知道改 content script 后要「重载插件 + 刷新页面」两步都做

踩坑提醒 ​

❌ 在 service worker 里写 document.querySelector(...) ​

原因:service worker 不能访问 DOM,这段代码必然失败。

正确做法:改页面的事全部交给 content script;service worker 只管事件,需要动页面时通过消息吩咐 content script 去做。

❌ 在 content script 里直接调其它扩展 API ​

原因:官方明确说 content script「unable to access other APIs directly」——它只能直接用 dom、i18n、storage 和 runtime 的部分方法。

正确做法:用 runtime.sendMessage() 把请求转发给 service worker,由它代办后把结果传回来。

❌ 改了 content script,页面没变化 ​

原因:content script 的改动要「重载插件 + 刷新宿主页面」两步,少一步都看不到效果。

正确做法:先回 chrome://extensions 重载,再刷新目标网页。

❌ 在扩展页找 content script 的 console.log ​

原因:扩展页的 Errors 按钮只记录 runtime errors、console.warning、console.error,普通 console.log 不记;content script 的日志在宿主网页的 Console 里。

正确做法:content script 的日志去网页看;service worker 的日志才在扩展页 Inspect views。找不到报错时,先回《报错循环:把错误信息原样喂回去》把报错原样喂给 AI。

进阶技巧 ​

  • 静态声明之外还有两种注入方式:动态声明 chrome.scripting.registerContentScripts(Chrome 96+),以及编程式注入 scripting.executeScript(由插件在需要时注入,比如点了工具栏图标;需要 host 权限或临时授权 activeTab)。
  • 官方教程的后续几篇正好按角色展开:Run scripts on every page、Inject scripts into the active tab、Handle events with service workers——学完本课可以直接跟。
  • 想测试 service worker 的休眠唤醒:在它的 DevTools 里打开 Application → Service Workers 面板手动 start/stop。注意:Inspect(检查)service worker 会让它一直保持活跃,测完记得关掉 DevTools。

本课小结 ​

✅ 用一句话记住两个角色:

  • service worker = 大脑:插件的事件中枢,按需启动、空闲即停,不能碰 DOM
  • content script = 手脚:活在网页里,能读页面、改页面,被隔离世界保护
  • 隔离世界:JS 变量互不可见,DOM 两边共享
  • 日志位置:content script → 宿主网页 Console;service worker → 扩展页 Inspect views

关键配置字段回顾

字段推荐值说明
background.service_worker"service-worker.js"声明 MV3 后台脚本(MV2 的 background script 就是它)
content_scripts[].matches目标网站的 match pattern必填,决定注入哪些页面
content_scripts[].js["content-script.js"]要注入的 JS 文件列表

下一课预告 ​

两个角色的日志一个藏在网页里、一个藏在扩展页里——下一课《插件调不出来:日志去哪找、报错怎么读》把这些位置一次讲透。你会学到:

  • manifest 写错、权限拼错时分别会在哪里报错
  • popup、content script、service worker 的日志分别去哪看
  • 怎么用扩展页的 Errors 按钮快速定位问题
搞懂AI关于我们怎么写隐私说明

独立的第三方教程,与文中提到的任何产品及其公司没有隶属或合作关系;产品名称和商标归各自所有者。价格和政策以官方页面为准。