Skip to main content
一个扩展运行在多个彼此隔离的上下文里。service worker 持有特权代码,content script 看得见页面,popup 或 options 页面承载界面。它们不共享内存,因此每个跨越边界的值都是以消息的形式跨越的。本页说明每个方向该用哪个调用、决定异步答复能否到达的规则,以及一次失败的交互会打印哪些控制台行。

传递消息的三种方式

chrome.runtime.sendMessage 一次性。它会到达每一个带有 runtime.onMessage 监听器的扩展上下文,也就是 service worker、popup、options 页面,以及任何其他已打开的扩展页面。它不会到达 content script。 chrome.tabs.sendMessage 一次性,瞄准一个标签页。从 service worker 或另一个扩展页面调用它,去到达那个标签页里的 content script,还可以通过 frameId 只到达一个 frame。content script 必须已经在那里运行。 chrome.runtime.connectchrome.tabs.connect 一个保持打开的具名 port。两端可以随时投递消息,两端也都通过 port.onDisconnect 得知拆除。 在你自己的上下文之间传消息不需要额外权限。它需要的是对面有一个活着的监听器。

异步响应规则

runtime.onMessage 监听器默认是同步答复的。它一返回,通道就关闭,之后再调用 sendResponse 会被丢弃。返回 true 才是让通道保持打开的做法:
由这条规则可以推出两点。一个 async 监听器函数返回的是 Promise,而 Promise 不是 true,因此在 Chromium 上,一个稍后调用 sendResponseasync 监听器仍然会关闭通道。请像上面那样在监听器内部做异步工作并返回 true。在调用方一侧,省略回调时 chrome.runtime.sendMessage 会返回 Promise,所以那里直接 await 即可,不涉及以上这些。 给每条消息一个明确的 type,在处理之前校验载荷,并在做特权工作之前检查 sender。content script 是你的代码,但它转发的数据来自你无法控制的页面。
长时间的交互改用 port,port 带有名字,因此一个监听器可以分辨它的调用方:

各浏览器差异

在 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 objectonDisconnect 触发之后,仍有人向这个 port 投递消息。请在 onDisconnect 处理器里清掉你的引用,需要时再打开一个新 port。

Extension.js 的做法

Extension.js 编译调用这些 API 的代码,并不包装它们。协议是你自己的。工具链改变的是开发循环:
  • 一次被归类为 content-scripts 的改动会重新注入受影响的条目并拆除上一次的挂载,因此监听器是被替换而不是被叠加。一次被归类为 full 的改动会重载扩展,而重载前就留在页面上的 content script 会变成上面那种 Extension context invalidated 情况。请重新加载标签页。
  • extension dev 下,从 scripts/ 文件夹注入的脚本会在编辑后被重放,因此你在被注入脚本里打开的 port 会由新的副本重新打开。参见 特殊文件夹
  • service worker 与 content script 的控制台输出会被转发到同一个通道,因此跨越边界的消息更容易在单一流里跟踪。
把特权工作留在 service worker 里,让 content script 保持狭窄,让载荷保持小。转发整页快照的 content script 比只转发功能所需三个字段的那个更慢,攻击面也更大。

参见