由于extension中包含的content scripts运行在web page的上下文(context),而不是extension的上下文,所以,content scripts经常需要与extension的其他组件通信(communicating)。例如一个RSS阅读器extension可能使用content script去网页中探测现有的RSS feed,再通知后台页,就可以展现对应的page action图标。

在content scripts和extension之间通信,就是通过message passing实现的。通信双方可以监听(listen)对方发送过来的消息(message),并可以使用同一渠道(channel)返回结果(respond)。一个message可以包含任何有效的JSON对象。

一次性请求(on-time requests)

如果只需要向对方发送一个简单的message,应该使用runtime.sendMessage()或tabs.sendMessage()方法,这两个方法发送一次性的JSON序列化后的message给对方,如果需要获得返回结果(response back),可以使用callback参数。

从一个content script发送请求,如下:

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

从extension向content script发送请求的代码类似,除了需要确定请求发到哪个tab。

1
2
3
4
5
6
//发送请求到当前选中的tab
chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
  chrome.tabs.sendMessage(tabs[0].id, {greeting: "hello"}, function(response) {
    console.log(response.farewell);
  });
});

在接受一端,需要设定一个message事件监听器来处理message请求,使用方法在content script和extension page是相同的。

1
2
3
4
5
6
7
8
9
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是同步执行的,如果需要异步执行,在方法结尾添加return true。

注意:如果多个page都在监听onMessage事件,同一个事件,只有第一个调用sendResponse()的会发送response成功。其他的responses都会被忽略。

注意:如果没有message处理器(handlers)返回true,或者sendResponse回调函数被垃圾回收,sendMessage函数的callback会自动执行。

持续性连接(Long-lived connections)

有时候,比起单次请求通信,更需要持续较长时间的通信对话。这时,你可以打开content script和extension page的持续性通信渠道(long-lived channel)。使用runtime.connet()或tabs.connect()方法。为了区分不同类型的连接,可以为通信渠道指定名字(name)。

一个应用实例是自动添加表单的extension。

content script可以打开一个channel与extension page通信,通过向extension发送message,把页面中每个输入元素的内容发送过去,并获取响应的返回数据来自动填充。这个共享的连接允许extension保持共享状态,链接来自content script的若干信息(messages)。

在建立连接的时候,每个终端获得一个runtime.Port对象,用来通过连接发送和接受message。

1
2
3
4
5
6
7
8
9
//以下代码可以在content script中打开一个channel,发送和监听message
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"});
});

从extension向content script发送请求的代码与上面代码相似,除了需要指定连接哪个tab。只需要把上面代码中建立连接的方法换成tabs.connect()方法。

为了处理传入的连接,需要设置一个runtime.onConnect事件监听器。这个方法的使用,在content script和extension page使用方法是一样的。当extension的一个组件调用了connect()方法,这个事件就会被触发,可以通过这个连接,使用runtime.Port对象来发送和接受message。示例代码如下。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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."});
  });
});

Port对象的生命周期

Port被设计为extension的不同部分之间双向通信方案,其中extension的最小部分单位是一个frame。在调用tabs.connect,runtime.connect,rutime.connectNative方法时,会创建Port对象。这个Port对象可以马上用来通过postMessage向extension另一部分发送消息。

如果一个tab中包含多个frame,当调用tabs.connect方法,会导致触发多次runtime.onConnect事件。同样的,如果runtime.connect被调用,onConnect事件也会触发多次。

可能有时需要知道一个connection是何时关闭的,例如当你为每个打开的Port单独维护它的状态时。这时,可以通过监听runtime.Port.onDisconnect事件来达到目的。当channel的另一端没有有效Port时,就会触发这个事件。通常在以下情况发生:

  • 在另一端没有runtime.onConnect的监听器;
  • 包含port的tab处于未载入状态,即tab导航到了其他页面;
  • 调用connect方法的frame已经处于未载入状态;
  • 所有接受这个port消息的frame都处于未载入状态;
  • 另一端调用了runtime.Port.disconnect方法。注意:如果一次connect方法调用的接收方有多个port,当任意一个port调用了disconnect方法,那么onDisconnect事件只会在这个port触发,其他port不会触发。

扩展之间的通信(Cross-extension Messaging)

除了在扩展中的不同组件之间发送消息外,还可以使用消息传递 API 与其他扩展进行通信。这样可以把自己的API作为公共API给其他扩展利用。

监听消息请求和连接与扩展内部通信类似,不同之处在于调用的方法不同。需要调用runtime.onMessageExternal和runtime.onConnectExternal方法。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// 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.
  });
});

向另一个extension发送消息类似于在extension内部发送消息。唯一的区别是必须要传递与之通信的扩展程序ID。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// 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(...);

网页中向extension发送消息

与跨extension通信类似,extension还可以接收来自常规网页的消息。要使用这个功能,必须在manifest.json中指定要与之通信的网站。

1
2
3
"externally_connectable": {
  "matches": ["https://*.example.com/*"]
}

加入以上代码,就会将消息传递API暴露给指定的URL正则表达式对应页面。URL正则表达式中必须包含一个二级域名,即禁止使用“*”、“*.com”、“*.co.uk”和“*.appspot.com”等域名。在网页中,使用runtime.sendMessage或runtime.connect方法向特定app或extension发送message。

1
2
3
4
5
6
7
8
9
// 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);
  });

在扩展中,监听来自网页的message与扩展之间传递message的方法一样,使用runtime.onMessageExternal或runtime.onConnectExternal方法。

这种连接,只有从网页端发起才能建立。

原生应用消息(Native messaging)

Extensions can exchange messages with native applications that are registered as a native messaging host.