【译】chrome扩展---Message Passing

【译】chrome扩展---Message Passing

原文 developer.chrome.com/ex

译者 :百度外卖FE - 程亚杰


正文:

由于content scripts 运行在web页面的上下文中,而不是在扩展中,它们经常需要某种能实现与其他扩展通信的方式,例如,一个RSS reader扩展可能需要使用content scripts来检测页面中一个RSS feed的存在,然后通知background页需要为该页展示一个页面操作按钮。

我们使用message passing来实现扩展与content scripts之间的通信,它们之间都能够监听从对方发送的消息,并且在同一个消息通道中进行响应。一个消息可以包含任何可用的JSON对象(null,boolean,number,string,array,or object)。chrome不仅提供了完成一次性请求的简单api,还有可以通过保持长时间生效的连接API,能够在共享上下文条件下交换多条消息,并且如果你能够获取扩展ID时,就可以在扩展之间传递消息,这将会在跨扩展消息章节中包含。

简单的一次性请求

如果你只需要向你的扩展的另一部分发送单条信息(且可以获得响应),就应该使用简单的runtime.sendMessage或tabs.sendMessage.它允许你将一条单次的json序列化的消息从一个content script发送到扩展,反之亦然。并且会有一个可选的回调参数允许你处理来自接收端的响应。
从content script发送一个请求是这样的:

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


从扩展发送一条请求至content script的方式看上去非常相似,你需要指定往哪一个tab去发送。下面的例子说明了如何向选中tab的content scripts发送一条消息。

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事件监听器去处理消息。这在content script或扩展页中是相同的。

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 ,需要在 onMessage 事件处理器中加上 return true

注意: 如果多个页面同时在监听 onMessage事件,对同一个事件调用的sendResponse中只有第一个能够成功发送响应,其他的响应将被忽略。
注意:sendResponse调用只会在同步调用中,或者在事件处理器中返回true说明它将异步响应时才能够成功。sendMessage函数将在没有处理器返回true或sendResponse回调被回收时自动调起。

长时效连接

有时我们会需要比单个请求响应更长时间的会话连接,这时你可以在你的contents scripts 与扩展之间开启一个长时效的连接通道,调用函数是runtime.connect或tabs.connect。通道可以通过设置名称来区分不同的连接。

一种场景是一个自动填充表单的扩展,content script 会对扩展页创建一个通道用来登录,并且为页面上的每个input元素发送消息去请求填充表单的数据。一个共享的连接允许扩展对不同content scripts之间的消息保持共享状态。

当建立一个连接时,各个端都将基于一个runtime.Port对象,通过它能够使用连接发送,接收消息。

以下是如何在content script上建立一个连接,并发送和监听消息的方式:

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


从扩展向content script发送消息方式十分相似,不同的只有你需要指定向哪一个tab建立连接。在上面例子中建立连接处使用 tabs.connect即可。

你需要设置runtime.onConnect事件处理器来处理传入连接。这在content script 或者扩展中都相同。当你的扩展中的另一部分调起了 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."});
});


Port生命周期

Port 是为了在扩展中不同部分之间进行双向通信而设计,这里一个 frame 被视为最小的单元。一个Port创建于调用 tab.connect,runtime.connect或者runtime.connectNative时。通过port可以调用postMessage立即对其他端发送消息。

如果一个tab有多个frames,调用tabs.connect将导致多个runtime.onConnect被调起。同样的,如果runtime.connect被调用,那么onConnect 将被触发多次(每次针对扩展处理的所有frame)

你可能向知道连接什么时候关闭,例如你希望对每一个开启的port 保持不同的状态。你可以监听runtime.Port.onDisconnect事件,它会在连接的其他端都没有可用的Port时触发,这会发生在以下情况时:

  • 另一端没有runtime.onConnect监听器时
  • 包含port的tab被关闭
  • connect所在的frame被销毁
  • 所有接收connect的frame被销毁
  • 在另一端调用了runtime.Port.disconnect.请注意,一个connect调用会在多个接收端产生port,而一次disconnect调用,只会在port的发送端触发onDisconnect事件,而不会在其他port上触发

跨扩展通信

为了补充扩展中不同组件间的通信,你可以使用messageing API与其他扩展进行通信。你也可以通过API暴露一个公用API供其他扩展利用。

通信方式与传入请求和连接十分相似,不同只有你需要使用runtime.onMessageExternal 或runtime.onConnectExternal方法。以下是例子:

// For simple requests:
chrome.runtime.onMessageExternal.addListener(
function(request, sender, sendResponse) {
if (sender.id == blacklistedExtension)
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(...);


从web页面中发送消息

与跨扩展通信相同,你的扩展可以在普通的web页面中发送接收消息。要启用这个特性,你需要在你的manifest.json中设置哪个web页需要通信,例如

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


这就会对所有匹配了你设置的url模式的页面暴露message API。url 模式必须至少包含二级域名,也就是说像” “,” .com”,” .co.uk”,” .appspot”是禁止的。在web页面上使用runtime.sendMessage或者runtime.connectAPI 对指定app或扩展发送消息,例如:

// 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);
});


在你app或扩展中,你需要通过runtime.onMessageExternal或者runtime.onConnectExternal API 来监听来自web页的消息,这与跨扩展通信相似。只有web页可以初始化一个连接。例子:

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


原生消息

扩展与应用能够与原生应用进行消息通讯,前提是应用被注册为原生消息宿主(native messaging host)。

安全考虑

当你的background页接收到来自content script或其他应用的消息时,你需要注意不要成为跨域脚本的牺牲品。具体上,要避免使用有风险的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.