消息通信:content ↔ service worker ↔ popup 三方对话
扩展不是一段代码,而是分处不同上下文的几个角色:网页里的 content script、按需唤醒的 service worker、点图标才出现的 popup、独立运行打开的 options 页。它们互相看不见彼此的变量,唯一的协作方式就是消息。这一篇把一次性的 sendMessage 和长连接的 Port 都讲透,并给出方向对照表。
方向与方法总表
| 发送方 | 目标 | 用什么 |
|---|---|---|
| content script | service worker / popup | chrome.runtime.sendMessage |
| popup / options | 当前标签页的 content script | chrome.tabs.sendMessage |
| service worker | content script | chrome.tabs.sendMessage |
| 任意两方长连接 | 双向多次 | chrome.runtime.connect / tabs.connect |
记忆锚点:发往「扩展内部」用 runtime,发往「某个标签页」用 tabs。content script 没有 tabs 权限概念,它只跟扩展内部通信,所以只有 runtime 一侧。
一次性请求:sendMessage
从 content script 发给后台,返回 Promise:
// content.js
const res = await chrome.runtime.sendMessage({ type: "query", text: "hello" });
console.log(res.answer);
后台监听并应答:
// background.js
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
if (msg.type === "query") {
fetch(`https://api.example.com/search?q=${encodeURIComponent(msg.text)}`)
.then(r => r.json())
.then(data => sendResponse({ answer: data.summary }));
return true; // 关键:声明将异步应答
}
});
这里的 return true 是最经典的坑:onMessage 监听器若不同步调用 sendResponse,就必须返回字面量 true 保持消息通道开启,否则 Promise 永远悬空或收到 undefined。官方文档明确写着要用 return true 的字面量而非任何 truthy 值。反方向(后台发往标签页)先查到 tabId 再用 tabs.sendMessage,参数几乎一样。
应答的两种异步写法
除了 return true + 手动 sendResponse,还可以直接从监听器返回 Promise(Chrome 148 起逐步铺开)。两种写法都合法,但注意 async 函数的陷阱:async 监听器永远返回 Promise,哪怕你没 return——此时隐式 resolve 成 undefined,会被当作「已应答 null」,在多监听器场景下会抢占先注册监听器的应答机会。所以混用两种风格时,确保 async 监听器要么真返回结果,要么不要承担应答职责。
错误传播也有讲究:监听器抛出的 Error 会传导给发送方(Chrome 146 起逐步铺开),reject 一个非 Error 值则发送方只能收到泛化的错误信息。协议设计上建议应答统一为 { ok: true, data } 形状,把业务错误放进 data 而不是靠异常传播。
长连接:connect 与 Port
需要双向多轮对话(如进度推送、状态订阅)时,用长连接:
// content.js:发起
const port = chrome.runtime.connect({ name: "progress" });
port.postMessage({ step: "start" });
port.onMessage.addListener((msg) => {
if (msg.type === "progress") console.log(msg.percent);
});
// background.js:接住
chrome.runtime.onConnect.addListener((port) => {
if (port.name !== "progress") return;
port.onMessage.addListener((msg) => {
port.postMessage({ type: "progress", percent: 50 });
});
});
Port 还有 onDisconnect 事件,是清理定时器、通知对端的好时机。另外一个隐藏知识点:从 Chrome 114 起,长连接端口上的消息往来会保持 service worker 存活,但仅打开端口不重置计时器——想靠长连接给后台续命,得真的在发消息。
实战建议
消息协议要约定形状,推荐 { type, payload } 两段式,type 用常量而非裸字符串,多了一层拼写保护。通信做多了你会发现和 HTTP API 设计思路一致:路由、负载、错误码一个不少。若你的扩展要和外部网页通信,那是另一条通道(externally_connectable 声明),本篇不展开。
调试消息通信的技巧(在两端分别打日志、观察 sender)见第五章 《调试技巧》 篇。官方文档:https://developer.chrome.com/docs/extensions/develop/concepts/messaging
一个完整的双向示例
把三个上下文串起来,做一个「popup 查询 → 后台取数 → content 展示」的最小闭环,这是最常见的中等复杂度拓扑:
// background.js:数据中介
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
if (msg.type === "fetch-summary") {
fetch(`https://api.example.com/summary?url=${encodeURIComponent(msg.url)}`)
.then(r => r.json())
.then(data => sendResponse({ ok: true, summary: data.text }))
.catch(() => sendResponse({ ok: false, summary: "" }));
return true;
}
if (msg.type === "show-toast") {
chrome.tabs.sendMessage(msg.tabId, { type: "toast", text: msg.text });
sendResponse({ ok: true });
}
});
// popup.js:发起方
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
const res = await chrome.runtime.sendMessage({ type: "fetch-summary", url: tab.url });
if (res.ok) {
await chrome.runtime.sendMessage({ type: "show-toast", tabId: tab.id, text: res.summary });
}
// content.js:展示方
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
if (msg.type === "toast") { renderToast(msg.text); sendResponse({ ok: true }); }
});
拓扑要点:popup 不直接调接口(它一关就死,请求可能中断),而是委托后台;后台是唯一的网络出口与路由中枢;content 只管展示。固定这个分层,消息拓扑永远不会乱。
消息协议的版本化
扩展要长期迭代,消息协议建议从一开始就带版本意识。最小做法是给 type 加前缀空间:v1:query、v1:translate。大改协议时新增 v2 类型,后台同时监听两版并做兼容,等老版本用户升级完毕再下线 v1。这套思路与 HTTP API 版本化一致,成本几乎为零,却能在协议演进时避免「新 popup 配旧后台」的错配报错。
未注册监听器时的报错(Receiving end does not exist)是常态而非异常——目标标签页可能还没注入。发送前用 chrome.tabs.sendMessage 的 try/catch 包住,失败则先 executeScript 再重发一次,是通用的健壮性写法。
调试与验收
消息问题的验收标准:发端拿到预期应答、收端日志完整、异常路径有兜底。调试技巧(两端打日志、观察 sender)见第五章 《调试技巧》 篇。官方文档:https://developer.chrome.com/docs/extensions/develop/concepts/messaging
消息里传 URL、文本时做清洗与匹配是常事,正则工具箱见 正则实战;多文件工程的消息常量定义与编辑器组织见 HBuilderX 效率技巧。
本章由 WorkBuddy 原创整理,API 行为依据 Chrome 扩展官方文档(Manifest V3)。
