消息传递

由于内容脚本在网页而不是应用的环境中运行,它们通常需要某种方式与应用的其余部分通信。例如,RSS 阅读器应用可能使用内容脚本检测页面上是否存在 RSS 供稿,然后通知后台页面,为当前页面显示页面按钮图标。

应用和内容脚本间的通信使用消息传递的方式。两边均可以监听另一边发来的消息,并通过同样的通道回应。消息可以包含任何有效的 JSON 对象(null、boolean、number、string、array 或 object)。对于一次性的请求有一个简单的 API,同时也有更复杂的 API,允许您通过长时间的连接与共享的上下文交换多个消息。另外您也可以向另一个应用发送消息,只要您知道它的标识符,这将在跨应用消息传递部分介绍。

简单的一次性请求

如果您只需要向您的应用的另一部分发送一个简单消息(以及可选地获得回应),您应该使用比较简单的 runtime.sendMessagetabs.sendMessage 方法。这些方法分别允许您从内容脚本向应用或者反过来发送可通过 JSON 序列化的消息,可选的 callback 参数允许您在需要的时候从另一边处理回应。

如下列代码所示从内容脚本中发送请求:

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

从应用向内容脚本发送请求与上面类似,唯一的区别是您需要指定发送至哪一个标签页。这一例子演示如何向选定标签页中的内容脚本发送消息。

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

在接收端,您需要设置一个 runtime.onMessage 事件监听器来处理消息。

chrome.runtime.onMessage.addListener(
  function(request, sender, sendResponse) {
    console.log(sender.tab ?
                "来自内容脚本:" + sender.tab.url :
                "来自应用");
    if (request.greeting == "您好")
      sendResponse({farewell: "再见"});
  });

注意: 如果多个页面都监听 onMessage 事件,对于某一次事件只有第一次调用 sendResponse() 能成功发出回应,所有其他回应将被忽略。

长时间的连接

有时候需要长时间的对话,而不是一次请求和回应。在这种情况下,您可以分别使用 runtime.connecttabs.connect 从您的内容脚本建立到应用(或者反过来)的长时间连接。建立的通道可以有一个可选的名称,让您区分不同类型的连接。

使用长时间连接的一种可能的情形为自动填充表单的应用。对于一次登录操作,内容脚本可以连接到应用页面,每次页面上的输入元素需要填写表单数据时向应用发送消息。共享的连接允许应用保留来自内容脚本的不同消息之间的状态联系。

建立连接时,两端都将获得一个 runtime.Port 对象,用来通过建立的连接发送和接收消息。

如下代码演示如何从内容脚本中建立连接,发送并监听消息:

var port = chrome.runtime.connect({name: "敲门"});
port.postMessage({joke: "敲门"});
port.onMessage.addListener(function(msg) {
  if (msg.question == "是谁?")
    port.postMessage({answer: "女士"});
  else if (msg.question == "哪位女士?")
    port.postMessage({answer: "Bovary 女士"});
});

从应用向内容脚本发送请求与之类似,唯一的区别是您需要指定连接到哪一个标签页。您只需要将以上例子中的连接调用替换为 tabs.connect

为了处理传入连接,您需要设置一个 runtime.onConnect 事件监听器。这一步无论在内容脚本还是应用页面中都是一样的。当您的应用的另一部分调用 connect() 时,会产生这一事件,同时传递您可以通过建立的连接发送和接受消息的 runtime.Port 对象。如下代码演示如何回应传入连接:

chrome.runtime.onConnect.addListener(function(port) {
  console.assert(port.name == "敲门");
  port.onMessage.addListener(function(msg) {
    if (msg.joke == "敲门")
      port.postMessage({question: "是谁?"});
    else if (msg.answer == "女士")
      port.postMessage({question: "哪位女士?"});
    else if (msg.answer == "Bovary 女士")
      port.postMessage({question: "我没听清楚。"});
  });
});

您可能想知道连接何时关闭,例如您需要为每一个打开的端口单独保留状态。这种情况下您可以监听 runtime.Port.onDisconnect 事件,当连接的另一端调用 runtime.Port.disconnect 或包含该端口的页面已结束(例如标签页转到了另一个页面)时,对于每一个端口确保都会发生一次该事件。

跨应用消息传递

除了在您的应用的不同组成部分间发送消息以外,您也可以使用消息传递 API 与其他应用通信。这样您可以提供一个公共的 API,让其他应用使用。

监听传入的请求和连接与处理内部的消息类似,唯一的区别是您分别使用 runtime.onMessageExternalruntime.onConnectExternal 事件。如下是分别处理这两个事件的例子:

// 用于简单的请求:
chrome.runtime.onMessageExternal.addListener(
  function(request, sender, sendResponse) {
    if (sender.id == blacklistedExtension)
      return;  // 不允许这一应用访问
    else if (request.getTargetData)
      sendResponse({targetData: targetData});
    else if (request.activateLasers) {
      var success = activateLasers();
      sendResponse({activateLasers: success});
    }
  });

// 用于长时间的连接:
chrome.runtime.onConnectExternal.addListener(function(port) {
  port.onMessage.addListener(function(msg) {
    // 有关处理 onMessage 事件的示例请参见其他例子
  });
});

同样,向另一个应用发送消息与在您的应用中发送消息类似,唯一的区别是您必须传递您需要与之通信的应用的标识符。例如:

// 我们需要与之通信的应用的标识符。
var laserExtensionId = "abcdefghijklmnoabcdefhijklmnoabc";

// 发出一个简单请求:
chrome.runtime.sendMessage(laserExtensionId, {getTargetData: true},
  function(response) {
    if (targetInRange(response.targetData))
      chrome.runtime.sendMessage(laserExtensionId, {activateLasers: true});
  });

// 建立一个长时间的连接:
var port = chrome.runtime.connect(laserExtensionId);
port.postMessage(...);

从网页发送消息

跨应用消息传递类似,您的应用或应用可以接受并响应来自普通网页的消息。要使用该特性,您必须首先在您的 manifest.json 中指定您希望与之通信的网站,例如:

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

这样会将消息传递 API 提供给所有匹配您指定的 URL 表达式的网页。URL 表达式必须至少包含一个二级域名,也就是说禁止使用类似于“*”、“*.com”、“*.co.uk”和“*.appspot.com”之类的主机名。在网页中,使用 runtime.sendMessageruntime.connect API 向指定应用或应用发送消息。例如:

// 我们希望与之通信的应用标识符。
var editorExtensionId = "abcdefghijklmnoabcdefhijklmnoabc";

// 发送一个简单的请求:
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 == blacklistedWebsite)
      return;  // 不允许该网页访问
    if (request.openUrlInEditor)
      openUrl(request.openUrlInEditor);
  });

安全性考虑

当您从内容脚本或另一个应用接收消息时,您的后台网页应该小心,以免遭到跨站脚本攻击。特别地,避免使用下面这些危险的 API:

chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) {
  // 警告!可能会执行恶意脚本!
  var resp = eval("(" + response.farewell + ")");
});
chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) {
  // 警告!可能会插入恶意脚本!
  document.getElementById("resp").innerHTML = response.farewell;
});

您应该首选不运行脚本的更安全的API:

chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) {
  // JSON.parse 不会执行攻击者的脚本。
  var resp = JSON.parse(response.farewell);
});
chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) {
  // innerText 不会让攻击者插入 HTML 元素。
  document.getElementById("resp").innerText = response.farewell;
});

例子

您可以在 examples/api/messaging -->目录中找到使用消息通信的简单例子, 另外请参见 contentscript_xhr 例子,在这个例子中内容脚本与所属应用交换消息,以便应用可以代表内容脚本发出跨域请求。有关更多例子以及查看源代码的帮助,请参见示例