消息传递

由于内容脚本是在网页(而非扩展程序)环境中运行,因此它们通常需要通过某种方式与扩展程序的其余部分进行通信。例如,RSS 阅读器扩展程序可能会使用内容脚本检测页面上是否存在 RSS Feed,然后通知后台页面,以便显示该页面的页面操作图标。

扩展程序与其内容脚本之间的通信是通过消息传递来实现的。任一端都可以监听另一端发送的消息,并在同一通道上响应。消息可以包含任何有效的 JSON 对象(null、布尔值、数字、字符串、数组或对象)。其中有一个适用于一次性请求的简单 API,以及一个更为复杂的 API(允许您建立长期连接),以便通过共享上下文交换多条消息。如果您知道其他扩展程序的 ID,也可以向该扩展程序发送消息,这在跨扩展程序消息部分中进行了介绍。

简单的一次性请求

如果您只需要将一条消息发送到扩展程序的其他部分(并可以选择返回响应),则应使用简化的 runtime.sendMessagetabs.sendMessage。这样,您就可以从内容脚本向扩展程序发送一次性 JSON 可序列化消息,反之亦然。利用可选的回调参数,您可以处理来自另一端的响应(如果有)。

从内容脚本发送请求如下所示:

chrome.runtime.sendMessage({greeting: "hello"}, function(response) {
  console.log(response.farewell);
});

从扩展程序向内容脚本发送请求的过程非常类似,不同之处在于您需要指定要将请求发送到哪个标签页。此示例演示了如何向所选标签页中的内容脚本发送消息。

chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
  chrome.tabs.sendMessage(tabs[0].id, {greeting: "hello"}, function(response) {
    console.log(response.farewell);
  });
});

在接收端,您需要设置一个 runtime.onMessage 事件监听器来处理消息。这在内容脚本或扩展程序网页中看起来是一样的。

chrome.runtime.onMessage.addListener(
  function(request, sender, sendResponse) {
    console.log(sender.tab ?
                "from a content script:" + sender.tab.url :
                "from the extension");
    if (request.greeting == "hello")
      sendResponse({farewell: "goodbye"});
  }
);

在上面的示例中,同步调用了 sendResponse。如果您要异步使用 sendResponse,请将 return true; 添加到 onMessage 事件处理脚本中。

注意:如果有多个网页正在监听 onMessage 事件,则只有第一个针对特定事件调用 sendResponse() 的网页才会成功发送响应。系统将忽略针对该事件的所有其他响应。
注意:仅当同步使用,或者事件处理脚本返回 true 以表明其将异步响应时,sendResponse 回调才有效。如果没有处理程序返回 true 或者 sendResponse 回调被垃圾回收,系统将自动调用 sendMessage 函数的回调。

长期有效的连接

有时,持续时间超过单个请求和响应的对话非常有用。 在这种情况下,您可以分别使用 runtime.connecttabs.connect 打开从内容脚本到扩展页面的长期有效渠道,反之亦然。您可以选择为通道命名,以便区分不同类型的连接。

一个应用场景可能是自动表单填写扩展程序。内容脚本可以针对特定登录信息打开通向扩展程序页面的渠道,并为页面上的每个输入元素向扩展程序发送消息,以请求要填充的表单数据。共享连接可让扩展程序保持共享状态,从而将来自内容脚本的多条消息关联起来。

在建立连接时,每一端都会获得一个 runtime.Port 对象,该对象用于通过该连接发送和接收消息。

下面展示了如何从内容脚本打开频道,并发送和监听消息:

var port = chrome.runtime.connect({name: "knockknock"});
port.postMessage({joke: "Knock knock"});
port.onMessage.addListener(function(msg) {
  if (msg.question == "Who's there?")
    port.postMessage({answer: "Madame"});
  else if (msg.question == "Madame who?")
    port.postMessage({answer: "Madame... Bovary"});
});

从扩展程序向内容脚本发送请求的过程非常相似,不同之处在于您需要指定要连接到哪个标签页。只需将上例中的 connect 调用替换为 tabs.connect 即可。

为了处理传入的连接,您需要设置 runtime.onConnect 事件监听器。无论从内容脚本还是扩展程序网页中,都是如此。当扩展程序的其他部分调用“connect()”时,此事件将以及可用于通过连接发送和接收消息的 runtime.Port 对象触发。响应传入连接的操作如下所示:

chrome.runtime.onConnect.addListener(function(port) {
  console.assert(port.name == "knockknock");
  port.onMessage.addListener(function(msg) {
    if (msg.joke == "Knock knock")
      port.postMessage({question: "Who's there?"});
    else if (msg.answer == "Madame")
      port.postMessage({question: "Madame who?"});
    else if (msg.answer == "Madame... Bovary")
      port.postMessage({question: "I don't get it."});
  });
});

端口生命周期

端口设计为扩展程序不同部分之间的双向通信方法,其中(顶级)帧被视为最小的部分。调用 tabs.connectruntime.connectruntime.connectNative 后,系统会创建一个端口。此端口可立即用于通过 postMessage 将消息发送到另一端。

如果标签页中包含多个帧,调用 tabs.connect 会导致多次调用 runtime.onConnect 事件(针对标签页中的每一帧各一次)。同样,如果使用 runtime.connect, onConnect 事件可能会被多次触发(针对扩展进程中的每一帧一次)。

您可能希望确定连接何时关闭,例如您是否为每个打开的端口保持了单独的状态。为此,您可以监听 runtime.Port.onDisconnect 事件。当通道的另一端没有有效端口时,会触发此事件。这发生在以下情况中:

  • 另一端没有 runtime.onConnect 的监听器。
  • 包含该端口的标签页未加载(例如,该标签页经过导航)。
  • 调用 connect 的帧已卸载。
  • 收到端口(通过 runtime.onConnect)的所有帧均已卸载。
  • runtime.Port.disconnect另一端调用。请注意,如果 connect 调用导致接收器端的多个端口,并且其中任何端口上调用了 disconnect(),则 onDisconnect 事件只会在发送端的端口触发,而不会在其他端口上触发。

跨附加信息消息功能

除了在扩展程序中的不同组件之间发送消息之外,您还可以使用 Messaging API 与其他扩展程序进行通信。这样,您就可以公开其他扩展程序可以利用的公共 API。

监听传入请求和连接的情况与内部类似,只不过您使用的是 runtime.onMessageExternalruntime.onConnectExternal 方法。以下是一个示例:

// For simple requests:
chrome.runtime.onMessageExternal.addListener(
  function(request, sender, sendResponse) {
    if (sender.id == blocklistedExtension)
      return;  // don't allow this extension access
    else if (request.getTargetData)
      sendResponse({targetData: targetData});
    else if (request.activateLasers) {
      var success = activateLasers();
      sendResponse({activateLasers: success});
    }
  });

// For long-lived connections:
chrome.runtime.onConnectExternal.addListener(function(port) {
  port.onMessage.addListener(function(msg) {
    // See other examples for sample onMessage handlers.
  });
});

同样,向其他扩展程序发送消息类似于在您的扩展程序内发送消息。 唯一的区别是,您必须传递要与之通信的扩展程序的 ID。例如:

// The ID of the extension we want to talk to.
var laserExtensionId = "abcdefghijklmnoabcdefhijklmnoabc";

// Make a simple request:
chrome.runtime.sendMessage(laserExtensionId, {getTargetData: true},
  function(response) {
    if (targetInRange(response.targetData))
      chrome.runtime.sendMessage(laserExtensionId, {activateLasers: true});
  }
);

// Start a long-running conversation:
var port = chrome.runtime.connect(laserExtensionId);
port.postMessage(...);

从网页发送邮件

跨扩展程序消息传递类似,您的应用或扩展程序可以接收和响应来自常规网页的消息。如需使用此功能,您必须先在 manifest.json 中指定要与哪些网站通信。例如:

"externally_connectable": {
  "matches": ["*://*.example.com/*"]
}

这样一来,系统便会向与您指定的网址格式匹配的所有网页公开 Messaging API。网址格式必须至少包含二级域名,也就是说,不允许使用“*”“*.com”“*.co.uk”和“*.appspot.com”等主机名格式。在网页上,使用 runtime.sendMessageruntime.connect API 将消息发送到特定应用或扩展程序。例如:

// The ID of the extension we want to talk to.
var editorExtensionId = "abcdefghijklmnoabcdefhijklmnoabc";

// Make a simple request:
chrome.runtime.sendMessage(editorExtensionId, {openUrlInEditor: url},
  function(response) {
    if (!response.success)
      handleError(url);
  });

在您的应用或扩展程序中,您可以通过 runtime.onMessageExternalruntime.onConnectExternal API 监听网页中的消息,这与跨扩展程序消息传递类似。只有网页可以启动连接。示例如下:

chrome.runtime.onMessageExternal.addListener(
  function(request, sender, sendResponse) {
    if (sender.url == blocklistedWebsite)
      return;  // don't allow this web page access
    if (request.openUrlInEditor)
      openUrl(request.openUrlInEditor);
  });

原生消息传递

扩展程序和应用可以与注册为原生消息传递主机的原生应用交换消息。如需详细了解此功能,请参阅原生即时通讯

安全注意事项

内容脚本的可信度较低

内容脚本不如扩展程序后台网页可信(例如,恶意网页可能会破坏运行内容脚本的渲染器进程)。假设来自内容脚本的消息可能是由攻击者创建的,并确保验证并清理所有输入。假设发送到内容脚本的所有数据都可能泄露到网页。 限制从内容脚本接收的消息触发的特权操作的范围。

跨站点脚本攻击

当收到内容脚本或其他扩展程序发来的消息时,您的脚本应小心谨慎,不要受跨站脚本攻击的影响。这条建议适用于在扩展程序后台网页中运行的脚本,以及在其他网站来源中运行的内容脚本。 具体来说,请避免使用危险 API,例如以下 API:

chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) {
  // WARNING! Might be evaluating an evil script!
  var resp = eval("(" + response.farewell + ")");
});
chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) {
  // WARNING! Might be injecting a malicious script!
  document.getElementById("resp").innerHTML = response.farewell;
});

而是首选不会运行脚本的更安全 API:

chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) {
  // JSON.parse does not evaluate the attacker's scripts.
  var resp = JSON.parse(response.farewell);
});
chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) {
  // innerText does not let the attacker inject HTML elements.
  document.getElementById("resp").innerText = response.farewell;
});

示例

您可以在 examples/api/messaging 目录中找到通过消息进行通信的简单示例。原生即时通讯示例演示了 Chrome 应用如何与原生应用通信。如需更多示例以及查看源代码方面的帮助,请参阅示例