消息传递

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

这种通信使用消息传递,这让扩展程序和内容脚本都可以监听对方的邮件,并在同一渠道上做出响应。消息可以包含任何有效的 JSON 对象(null、布尔值、数字、字符串、数组或对象)。有两个消息传递 API:一个用于一次性请求,另一个用于允许发送多条消息的长连接,该 API 更为复杂。如需了解如何在扩展程序之间发送消息,请参阅跨扩展程序消息部分。

一次性请求

如需向扩展程序的其他部分发送单条消息,并根据需要获取回复,请调用 runtime.sendMessage()tabs.sendMessage()。借助这些方法,您可以将一次性 JSON 可序列化消息从内容脚本发送到扩展程序,或从扩展程序发送到内容脚本。如需处理响应,请使用返回的 Promise。为了与旧版扩展程序向后兼容,您可以改为将回调作为最后一个参数传递。您不能在同一调用中使用 promise 和回调。

当您发送消息时,系统会向处理消息的事件监听器传递一个可选的第三个实参 sendResponse。这是一个接受 JSON 可序列化对象的函数,该对象用作发送消息的函数的返回值。默认情况下,必须同步调用 sendResponse 回调。如果您想执行异步工作以将值传递给 sendResponse,则必须从事件监听器返回字面量 true(而不仅仅是布尔值)。这样做会使消息通道保持对另一端的打开状态,直到调用 sendResponse

// Event listener
function handleMessages(message, sender, sendResponse) {

  fetch(message.url)
    .then((response) => sendResponse({statusCode: response.status}))

  // Since `fetch` is asynchronous, must send an explicit `true`
  return true;
}

// Message sender
  const {statusCode} = await chrome.runtime.sendMessage({
    url: 'https://example.com'
  });

如需了解如何将回调转换为 Promise 并在扩展程序中使用它们,请参阅 Manifest V3 迁移指南

从内容脚本发送请求如下所示:

content-script.js

(async () => {
  const response = await chrome.runtime.sendMessage({greeting: "hello"});
  // do something with response here, not outside the function
  console.log(response);
})();

如果您想对消息进行同步响应,只需在有响应后调用 sendResponse,并返回 false 以指示已完成即可。如需异步响应,请返回 true 以保持 sendResponse 回调处于活动状态,直到您准备好使用它为止。不支持使用异步函数,因为它们会返回 Promise,而 Promise 不受支持。

如需向内容脚本发送请求,请指定请求适用于哪个标签页,如下所示。此示例适用于 Service Worker、弹出式窗口以及作为标签页打开的 chrome-extension:// 网页。

(async () => {
  const [tab] = await chrome.tabs.query({active: true, lastFocusedWindow: true});
  const response = await chrome.tabs.sendMessage(tab.id, {greeting: "hello"});
  // do something with response here, not outside the function
  console.log(response);
})();

如需接收消息,请设置 runtime.onMessage 事件监听器。扩展程序和内容脚本中使用相同的代码:

content-script.js 或 service-worker.js

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() 的网页才能成功发送响应。系统会忽略对该事件的所有其他响应。

长连接

如需创建可重复使用的长效消息传递渠道,请调用 runtime.connect() 将消息从内容脚本传递到扩展程序页面,或调用 tabs.connect() 将消息从扩展程序页面传递到内容脚本。您可以为渠道命名,以区分不同类型的连接。

长连接的一个潜在用例是自动填充表单的扩展程序。内容脚本可能会针对特定登录打开一个通道到扩展程序页面,并针对页面上的每个输入元素向扩展程序发送消息,请求填充表单数据。借助共享连接,扩展程序可以在扩展程序组件之间共享状态。

建立连接时,每个端都会分配一个 runtime.Port 对象,以便通过该连接发送和接收消息。

使用以下代码从内容脚本打开渠道,并发送和监听消息:

content-script.js

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

如需从扩展程序向内容脚本发送请求,请将前面的示例中对 runtime.connect() 的调用替换为 tabs.connect()

如需处理内容脚本或扩展程序页面的传入连接,请设置 runtime.onConnect 事件监听器。当扩展程序的其他部分调用 connect() 时,它会激活此事件和 runtime.Port 对象。用于响应传入连接的代码如下所示:

service-worker.js

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

充电桩生命周期

端口旨在作为扩展程序不同部分之间的双向通信方法。顶级帧是扩展程序中可以使用端口的最小部分。当扩展程序的某个部分调用 tabs.connect()runtime.connect()runtime.connectNative() 时,它会创建一个端口,该端口可以立即使用 postMessage() 发送消息。

如果标签页中有多个帧,则调用 tabs.connect() 会针对标签页中的每个帧调用 runtime.onConnect 事件一次。同样,如果调用 runtime.connect(),则扩展程序进程中的每个帧都可以触发一次 onConnect 事件。

您可能需要了解连接何时关闭,例如,如果您要为每个打开的端口维护单独的状态。为此,请监听 runtime.Port.onDisconnect 事件。当通道另一端没有有效的端口时,系统会触发此事件,这可能由以下任一原因造成:

  • 另一端没有 runtime.onConnect 监听器。
  • 包含相应充电桩的标签页已卸载(例如,如果用户导航到其他标签页)。
  • 调用 connect() 的帧已卸载。
  • 接收了端口(通过 runtime.onConnect)的所有帧均已卸载。
  • 另一端会调用 runtime.Port.disconnect()。如果 connect() 调用导致接收器端有多个端口,并且在其中任一端口上调用了 disconnect(),则 onDisconnect 事件只会在发送端口触发,而不会在其他端口触发。

跨扩展程序消息传递

除了在扩展程序中的不同组件之间发送消息之外,您还可以使用消息传递 API 与其他扩展程序进行通信。这样,您就可以公开一个公共 API 供其他扩展程序使用。

如需监听来自其他扩展程序的传入请求和连接,请使用 runtime.onMessageExternalruntime.onConnectExternal 方法。下面是每种类型的示例:

service-worker.js

// For a single request:
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.
  });
});

如需向其他扩展程序发送消息,请按如下方式传递您要与之通信的扩展程序的 ID:

service-worker.js

// The ID of the extension we want to talk to.
var laserExtensionId = "abcdefghijklmnoabcdefhijklmnoabc";

// For a minimal request:
chrome.runtime.sendMessage(laserExtensionId, {getTargetData: true},
  function(response) {
    if (targetInRange(response.targetData))
      chrome.runtime.sendMessage(laserExtensionId, {activateLasers: true});
  }
);

// For a long-lived connection:
var port = chrome.runtime.connect(laserExtensionId);
port.postMessage(...);

通过网页发送消息

扩展程序还可以接收和回复来自其他网页的消息,但无法向网页发送消息。如需从网页向扩展程序发送消息,请在 manifest.json 中使用 "externally_connectable" 清单键指定要与哪些网站通信。例如:

manifest.json

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

这样,Messaging API 便会向与您指定的网址模式匹配的任何网页公开。网址模式必须至少包含一个二级域名;也就是说,不支持“*”,“*.com”“*.co.uk”和“*.appspot.com”等主机名模式。从 Chrome 107 开始,您可以使用 <all_urls> 访问所有网域。请注意,由于此变更会影响所有托管服务,因此使用该变更的扩展程序在 Chrome 应用商店中的审核可能需要更长时间

使用 runtime.sendMessage()runtime.connect() API 向特定应用或扩展程序发送消息。例如:

webpage.js

// The ID of the extension we want to talk to.
const editorExtensionId = 'abcdefghijklmnoabcdefhijklmnoabc';

// Check if extension is installed
if (chrome && chrome.runtime) {
  // Make a request:
  chrome.runtime.sendMessage(
    editorExtensionId,
    {
      openUrlInEditor: url
    },
    (response) => {
      if (!response.success) handleError(url);
    }
  );
}

在您的扩展程序中,使用 runtime.onMessageExternalruntime.onConnectExternal API 监听网页发送的消息,如跨扩展程序消息传递中所述。示例如下:

service-worker.js

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

原生消息传递

扩展程序可以与注册为原生消息主机的原生应用交换消息。如需详细了解此功能,请参阅原生消息传递

安全注意事项

以下是与消息相关的一些安全注意事项。

内容脚本的可信度较低

与扩展程序服务工件相比,内容脚本的可信度较低。例如,恶意网页可能会破坏运行内容脚本的呈现进程。假设内容脚本中的消息可能是由攻击者伪造的,并确保验证并清理所有输入。假设发送到内容脚本的任何数据都可能会泄露到网页。限制可由从内容脚本收到的消息触发的特权操作的范围。

跨站点脚本攻击

请务必保护您的脚本免受跨网站脚本攻击。从不可信来源(例如用户输入、通过内容脚本或 API 访问的其他网站)接收数据时,请务必避免将其解读为 HTML 或以可能允许运行意外代码的方式使用它。

更安全的方法

请尽可能使用不会运行脚本的 API:

service-worker.js

chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) {
  // JSON.parse doesn't evaluate the attacker's scripts.
  var resp = JSON.parse(response.farewell);
});

service-worker.js

chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) {
  // innerText does not let the attacker inject HTML elements.
  document.getElementById("resp").innerText = response.farewell;
});
不安全的方法

避免使用会使您的扩展程序易受攻击的以下方法:

service-worker.js

chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) {
  // WARNING! Might be evaluating a malicious script!
  var resp = eval(`(${response.farewell})`);
});

service-worker.js

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