消息传递

自从content script内容运行在网页环境而不是在扩展中,我们经常需要一些方法和其余的扩展进行通信。例如,一个 RSS阅读扩展可能会使用content scripts去检测RSS阅读扩展应该提供给哪个页面,然后通知后台页面以便显示一个页面交互图标。

通过使用消息传递机制在扩展和content scripts中通信,任何一方可以收到另一方传递来的消息,并且在相同的通道上答复。这个消息可以包含 任何一个有效的JSON对象(null, boolean, number, string, array, or object)。这里有一些简单的API对于 一次简单的请求,还有更多复杂的API,它们可以帮助你长时间保持连接在一个共享的环境中交换大量的信息,它也可以帮助你传递消息给另外一个你知道ID的扩展 ,在扩展之间的消息传递这里会有详细的讲解。

一次简单的请求

如果你仅仅需要给你自己的扩展的另外一部分发送一个消息(可选的是否得到答复),你可以简单地使用chrome.extension.sendRequest()或者chrome.tabs.sendRequest()方法。这个方法可以帮助你传送一次JSON序列化消息从content script到扩展,反之亦然。如果接受消息的一方存在的话,可选的回调参数允许处理传回来的消息。

像下面这个例子一样,可以从content script 发起一个请求:

contentscript.js
================
chrome.extension.sendRequest({greeting: "hello"}, function(response) {
  console.log(response.farewell);
});

传递一个请求到扩展很容易,你需要指定哪个标签发起这个请求。下面这个例子展示了如何指定标签发起一个请求。

background.html
===============
chrome.tabs.getSelected(null, function(tab) {
  chrome.tabs.sendRequest(tab.id, {greeting: "hello"}, function(response) {
    console.log(response.farewell);
  });
});

接受消息的一方,需要启动一个chrome.extension.onRequest事件监听器用来处理消息。这个方法在content script和扩展中都是一样的。这个请求将会保留直到你做出了回应。下面的这个例子是一个很好的做法调用一个空对象请求然后得到答复的例子。

chrome.extension.onRequest.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"});
    else
      sendResponse({}); // snub them.
  });

小提示:如果多个页面都发起了相同的请求,都在等待答复,只有第一个发起请求的页面会得到响应,其他的将会被忽略。

长时间的保持连接

有时候持续长时间的保持会话会比一次简单的请求有用。你可以建立一个长时间存在的通道从content script到扩展, 反之亦然,使用chrome.extension.connect()或者chrome.tabs.connect()方法,你可以把这个通道命名,为了更方便区分不同类型的连接。

一个有用的例子就是自动填写表单扩展。content script可以建立一个通道在登录页面和扩展之间,同时发出一条消息给扩展,告诉扩展需要填写的内容。共享的连接允许扩展保持共享状态,从而连接几个来自content script.的消息。

当建立连接,两端都有一个Port 对象通过这个连接发送和接收消息。

下面展示了如何从content script建立一个通道,发送和接受消息:

contentscript.js
================
var port = chrome.extension.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"});
});

从扩展到content script发送一个请求看起来非常简单,除了你需要指定哪个标签需要连接, 简单的办法就是上面例子中的chrome.tabs.connect(tabId, {name: "knockknock"}).

为了处理正在等待的连接,你需要用chrome.extension.onConnect 事件监听器,对于content script或者扩展页面,这个方法都是一样的,但你的扩展的另外一个部分调用"connect()", 这个事件一旦被触发,通过这个连接你可以利用Port对象进行发送和接收消息,下面的例子展示了如何处理连接:

chrome.extension.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."});
  });
});

你可能想知道这个连接什么时候被关闭。例如,如果你在为每个开放的端口维护分开的状态,如果你想知道它什么时候关闭,因此,需要监听Port.onDisconnect 事件,这个事件被激发,要么是通道调用了Port.disconnect()这个方法,要么这个页面包含的端口没有被加载(例如这个标签是个导航栏) ,onDisconnect()保证仅仅被激发一次。

扩展之间的消息传递

除了在扩展的组件之间传送消息,你还可以使用消息API来和其他的扩展之间进行通信,你可以使用一个其他扩展也可以利用的公共API。

对于扩展内部来说,监听一个传入的请求和连接是一样的,你可以使用chrome.extension.onRequestExternal或者chrome.extension.onConnectExternal方法,如下面的例子所示:

// For simple requests:
chrome.extension.onRequestExternal.addListener(
  function(request, sender, sendResponse) {
    if (sender.id == blacklistedExtension)
      sendResponse({});  // 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.extension.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.extension.sendRequest(laserExtensionId, {getTargetData: true},
  function(response) {
    if (targetInRange(response.targetData))
      chrome.extension.sendRequest(laserExtensionId, {activateLasers: true});
  });

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

安全策略

无论是从content script还是从扩展接收消息,你的页面不应该cross-site scripting,特别是避免使用那些不安全的API例如下面的例子:

background.html
===============
chrome.tabs.sendRequest(tab.id, {greeting: "hello"}, function(response) {
  // WARNING! Might be evaluating an evil script!
  var resp = eval("(" + response.farewell + ")");
});

background.html
===============
chrome.tabs.sendRequest(tab.id, {greeting: "hello"}, function(response) {
  // WARNING! Might be injecting a malicious script!
  document.getElementById("resp").innerHTML = response.farewell;
});

相反的,选择更安全的API而不是运行脚本。

background.html
===============
chrome.tabs.sendRequest(tab.id, {greeting: "hello"}, function(response) {
  // JSON.parse does not evaluate the attacker's scripts.
  var resp = JSON.parse(response.farewell);
});

background.html
===============
chrome.tabs.sendRequest(tab.id, {greeting: "hello"}, function(response) {
  // innerText does not let the attacker inject HTML elements.
  document.getElementById("resp").innerText = response.farewell;
});

范例

你可以在这个examples/api/messaging 目录下找到消息传递的例子,也可以找到contentscript_xhr (contents script和扩展之间传递消息的例子),更多的内容和源码,请查看代码范例