Message passing(消息传递)

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

扩展与其内容脚本之间的通信通过使用消息传递进行。任何一方都可以侦听另一端发送的消息,并在同一通道上进行响应。消息可以包含任何有效的 JSON 对象(空值、布尔值、数字、字符串、数组或对象)。有一个用于一次性请求(one-time requests)的简单 API 和一个更复杂的 API,它允许您拥有长期连接(long-lived connections)以使用共享上下文交换多条消息。如果您知道其 ID,也可以将消息发送到另一个分机,这在交叉分机消息(cross-extension messages)部分中进行了介绍。

# Simple one-time requests(简单的一次性请求)

如果您只需要向扩展程序的另一部分发送一条消息(并可选择获得回复),则应使用简化的 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() 的页面才会成功发送响应。对该事件的所有其他响应都将被忽略。

注意:sendResponse 回调仅在同步使用时有效,或者如果事件处理程序返回 true 以指示它将异步响应。如果没有处理程序返回 true 或者如果 sendResponse 回调被垃圾收集,将自动调用 sendMessage 函数的回调。

# Long-lived connections(长期连接)

有时,对话的持续时间比单个请求和响应的时间长是有用的。在这种情况下,您可以分别使用 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"});
});

从扩展程序向内容脚本发送请求看起来非常相似,不同之处在于您需要指定要连接到哪个选项卡。只需将上面示例中的连接调用替换为 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."});
  });
});

# Port lifetime(端口寿命)

端口被设计为扩展的不同部分之间的双向通信方法,其中(顶级)帧( (top-level) frame)被视为最小的部分。在调用 tabs.connectruntime.connectruntime.connectNative 时,会创建一个端口Port。该端口可立即用于通过 postMessage 向另一端发送消息。

如果选项卡中有多个框架,则调用 tabs.connect 会导致多次调用 runtime.onConnect 事件(针对选项卡中的每个框架调用一次)。类似地,如果使用 runtime.connect,则 onConnect 事件可能会被触发多次(对于扩展过程中的每一帧一次)。

您可能想知道连接何时关闭,例如,如果您为每个打开的端口维护单独的状态。为此,您可以侦听 runtime.Port.onDisconnect 事件。当通道的另一端没有有效端口时会触发此事件。在以下情况下会发生这种情况:

  • 另一端没有 runtime.onConnect 的侦听器。
  • 包含端口的选项卡被卸载(例如,如果导航选项卡)。
  • 调用 connect 的框架已卸载。
  • 接收到端口(通过 runtime.onConnect)的所有帧都已卸载。
  • runtime.Port.disconnect 被另一端调用。请注意,如果连接调用导致接收方端有多个端口,并且在这些端口中的任何一个上调用了 disconnect() ,则 onDisconnect 事件仅在发送方的端口上触发,而不会在其他端口上触发。

# Cross-extension 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) {
    // 请参阅示例 onMessage 处理程序的其他示例。
  });
});

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

// 我们要与之交谈的扩展程序的 ID.
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(...);

# Sending messages from web pages(从网页发送消息)

与跨分机消息传递(cross-extension messaging)类似,您的分机可以接收和响应来自常规网页的消息。要使用此功能,您必须首先在 manifest.json 中指定要与之通信的网站。例如:

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

这会将消息传递 API 暴露给与您指定的 URL 模式匹配的任何页面。 URL 模式必须至少包含一个二级域 (second-level domain)- 即禁止使用“”、“.com”、“.co.uk”和“.appspot.com”等主机名模式。在网页上,使用 runtime.sendMessageruntime.connect API 向特定应用程序或扩展程序发送消息。例如:

// 我们要与之交谈的扩展程序的 ID。
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);
  });

# Native messaging(本地消息传递)

扩展可以与注册为本地消息传递主机(native messaging host)的本地应用程序交换消息(can exchange messages)。要了解有关此功能的更多信息,请参阅本机消息传递(Native messaging)。

# Security considerations(安全考虑)

# Content scripts are less trustworthy(内容脚本不太可信)

内容脚本不如扩展后台页面可信(Content scripts are less trustworthy)(例如,恶意网页可能会破坏运行内容脚本的渲染器进程)。假设来自内容脚本的消息可能是由攻击者制作的,并确保验证和清理所有输入(validate and sanitize all input)。假设发送到内容脚本的任何数据都可能泄漏到网页。限制可以由从内容脚本接收的消息触发的特权操作的范围。

# Cross-site scripting(跨站脚本)

当从内容脚本或其他扩展程序接收消息时,您的脚本应该小心不要成为跨站点脚本(cross-site scripting)的受害者。此建议适用于在扩展后台页面内运行的脚本以及在其他 Web 源内运行的内容脚本。具体来说,避免使用危险的 API,例如以下 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(例子)

您可以在 examples/api/messaging 目录中找到通过消息进行通信的简单示例。

By.一粒技术服务

results matching ""

    No results matching ""