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:
"background": {
"service_worker": "service-worker.js"
}在插件根目录新建 service-worker.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 必填,决定注入哪些页面;下面是官方文档的示例,把域名换成你真正想作用的网站):
"content_scripts": [
{
"matches": ["https://*.nytimes.com/*"],
"js": ["content-script.js"]
}
]官方示例里还有一行 "css": ["my-styles.css"]——content script 可以只注 JS、只注 CSS,或两者都注,按需取舍。
在插件根目录新建 content-script.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 改成:
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,输入:
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 按钮快速定位问题