由于内容脚本在网页(而非扩展程序)的上下文中运行,因此通常需要某种方式与扩展程序的其余部分进行通信。例如,RSS 阅读器扩展程序可以使用内容脚本来检测网页上是否存在 RSS Feed,然后通知后台页面,以便为该网页显示网页操作图标。
扩展程序及其内容脚本之间的通信通过消息传递来实现。任何一方都可以监听另一端发送的消息,并在同一渠道上做出响应。消息可以包含任何有效的 JSON 对象(null、布尔值、数字、字符串、数组或对象)。有一个简单的 API 用于一次性请求,还有一个更复杂的 API,可让您建立长期连接,以便在共享上下文中交换多条消息。如果您知道其他扩展程序的 ID,也可以向其发送消息,这在跨扩展程序消息部分中进行了介绍。
简单的一次性请求
如果您只需要向扩展程序的另一部分发送一条消息(并且可以选择性地接收响应),则应使用简化的 runtime.sendMessage 或 tabs.sendMessage。这样一来,您就可以分别从内容脚本向扩展程序发送一次性 JSON 可序列化消息,或从扩展程序向内容脚本发送一次性 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 事件处理脚本。
sendResponse 回调时,或者事件处理脚本返回 true 以指示它将异步响应时,该回调才有效。如果没有处理程序返回 true,或者 sendResponse 回调被垃圾回收,系统会自动调用 sendMessage 函数的回调。长期连接
有时,进行持续时间超过一次请求和响应的对话会很有用。在这种情况下,您可以使用 runtime.connect 或 tabs.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."});
});
});
端口生命周期
端口旨在作为扩展程序不同部分之间的双向通信方法,其中(顶级)框架被视为最小的部分。调用 tabs.connect、runtime.connect 或 runtime.connectNative 后,系统会创建一个 Port。此端口可立即用于通过 postMessage 向另一端发送消息。
如果某个标签页中有多个框架,则调用 tabs.connect 会导致多次调用 runtime.onConnect 事件(标签页中的每个框架各调用一次)。同样,如果使用 runtime.connect,则 onConnect 事件可能会多次触发(扩展程序进程中的每个帧触发一次)。
您可能需要了解连接何时关闭,例如,如果您要为每个打开的端口维护单独的状态。为此,您可以监听 runtime.Port.onDisconnect 事件。当通道另一侧没有有效端口时,会触发此事件。在以下情况下会发生这种情况:
- 另一端没有 runtime.onConnect 的监听器。
- 包含该端口的标签页已卸载(例如,如果该标签页被导航)。
- 调用
connect的帧已卸载。 - 接收到端口(通过 runtime.onConnect)的所有框架都已卸载。
- runtime.Port.disconnect 由另一端调用。请注意,如果
connect调用在接收端产生多个端口,并且在其中任何一个端口上调用了disconnect(),则onDisconnect事件仅在发送端的端口上触发,而不会在其他端口上触发。
跨扩展程序的消息传递
除了在扩展程序中的不同组件之间发送消息之外,您还可以使用 Messaging API 与其他扩展程序进行通信。这样一来,您就可以公开一个公共 API,供其他扩展程序使用。
监听传入请求和连接与内部情况类似,只不过您需要使用 runtime.onMessageExternal 或 runtime.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) {
// 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(...);
从网页发送消息
与跨扩展程序消息传递类似,您的应用或扩展程序可以接收来自常规网页的消息并做出响应。如需使用此功能,您必须先在 manifest.json 中指定要与之通信的网站。例如:
"externally_connectable": {
"matches": ["*://*.example.com/*"]
}
这会将消息传递 API 公开给与您指定的网址模式匹配的任何网页。网址格式必须至少包含一个二级网域,也就是说,禁止使用“*”“*.com”“*.co.uk”和“*.appspot.com”之类的主机名格式。在网页中,使用 runtime.sendMessage 或 runtime.connect API 向特定应用或扩展程序发送消息。例如:
// 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);
});
在应用或扩展程序中,您可以通过 runtime.onMessageExternal 或 runtime.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);
});
原生消息传递
扩展程序和应用可以与注册为原生消息传递主机的原生应用交换消息。如需详细了解此功能,请参阅原生消息传递。
安全注意事项
内容脚本的可信度较低
与扩展程序后台网页相比,内容脚本的信任度较低(例如,恶意网页可能能够入侵内容脚本运行的渲染器进程)。假设来自内容脚本的消息可能由攻击者精心设计,并确保验证和清理所有输入。假设发送到内容脚本的任何数据都可能会泄露到网页。 限制可由从内容脚本接收的消息触发的特权操作的范围。
跨站点脚本攻击
当从内容脚本或其他扩展程序接收消息时,您的脚本应注意不要成为跨站脚本攻击的受害者。此建议适用于在扩展程序后台页面中运行的脚本,以及在其他 Web 源中运行的内容脚本。具体而言,请避免使用以下危险的 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,这些 API 不会运行脚本:
chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) {
// JSON.parse does not evaluate the attacker's scripts.
var resp = JSON.parse(response.farewell);
});
chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) {
// innerText does not let the attacker inject HTML elements.
document.getElementById("resp").innerText = response.farewell;
});
示例
您可以在 examples/api/messaging 目录中找到通过消息进行通信的简单示例。原生消息传递示例演示了 Chrome 应用如何与原生应用通信。如需查看更多示例并获取有关查看源代码的帮助,请参阅示例。