传递消息的三种方式
chrome.runtime.sendMessage。 一次性。它会到达每一个带有 runtime.onMessage 监听器的扩展上下文,也就是 service worker、popup、options 页面,以及任何其他已打开的扩展页面。它不会到达 content script。
chrome.tabs.sendMessage。 一次性,瞄准一个标签页。从 service worker 或另一个扩展页面调用它,去到达那个标签页里的 content script,还可以通过 frameId 只到达一个 frame。content script 必须已经在那里运行。
chrome.runtime.connect 与 chrome.tabs.connect。 一个保持打开的具名 port。两端可以随时投递消息,两端也都通过 port.onDisconnect 得知拆除。
在你自己的上下文之间传消息不需要额外权限。它需要的是对面有一个活着的监听器。
异步响应规则
runtime.onMessage 监听器默认是同步答复的。它一返回,通道就关闭,之后再调用 sendResponse 会被丢弃。返回 true 才是让通道保持打开的做法:
async 监听器函数返回的是 Promise,而 Promise 不是 true,因此在 Chromium 上,一个稍后调用 sendResponse 的 async 监听器仍然会关闭通道。请像上面那样在监听器内部做异步工作并返回 true。在调用方一侧,省略回调时 chrome.runtime.sendMessage 会返回 Promise,所以那里直接 await 即可,不涉及以上这些。
给每条消息一个明确的 type,在处理之前校验载荷,并在做特权工作之前检查 sender。content script 是你的代码,但它转发的数据来自你无法控制的页面。
各浏览器差异
在 Safari 上,在你授予扩展网站访问权限之前 content script 不会运行,因此在那之前
tabs.sendMessage 没有接收方。参见 Safari。
如果你的源码基于 browser.* 编写,同时也要构建 Chromium 目标,请传入 --polyfill。参见 跨浏览器兼容。
你会看到的控制台报错
把你看到的那一行复制去搜索。每一行对应一个原因。Unchecked runtime.lastError: Could not establish connection. Receiving end does not exist.
没有任何一方在监听。要么目标上下文没有 runtime.onMessage 监听器,要么对 tabs.sendMessage 而言,还没有 content script 被注入那个标签页。在扩展加载之前就已打开的标签页在重新加载前没有 content script,而像 chrome:// 或扩展商店这样的受限页面永远不会有。请先用 chrome.scripting.executeScript 注入,或者处理这次失败,做法见 在运行时注入脚本。
Unchecked runtime.lastError: The message port closed before a response was received.
监听器收到了消息,却在没有让通道保持打开的情况下返回了。请从监听器返回 true,或者同步作答。
Uncaught Error: Extension context invalidated.
扩展重新加载时,旧的 content script 还在页面里运行。那段代码现在指向一个不复存在的运行时,它发出的每个 chrome.* 调用都会抛错。请重新加载标签页。在 extension dev 期间,这发生在一次被归类为完整重载的改动之后,重载与 HMR 对此有说明。
Attempting to use a disconnected port object
在 onDisconnect 触发之后,仍有人向这个 port 投递消息。请在 onDisconnect 处理器里清掉你的引用,需要时再打开一个新 port。
Extension.js 的做法
Extension.js 编译调用这些 API 的代码,并不包装它们。协议是你自己的。工具链改变的是开发循环:- 一次被归类为
content-scripts的改动会重新注入受影响的条目并拆除上一次的挂载,因此监听器是被替换而不是被叠加。一次被归类为full的改动会重载扩展,而重载前就留在页面上的 content script 会变成上面那种Extension context invalidated情况。请重新加载标签页。 - 在
extension dev下,从scripts/文件夹注入的脚本会在编辑后被重放,因此你在被注入脚本里打开的 port 会由新的副本重新打开。参见 特殊文件夹。 - service worker 与 content script 的控制台输出会被转发到同一个通道,因此跨越边界的消息更容易在单一流里跟踪。

