由于内容脚本是在网页的上下文(而不是运行它们的扩展程序)中运行的,因此它们通常需要一些方法来与扩展程序的其余部分进行通信。例如,RSS 阅读器扩展程序可能会使用内容脚本来检测页面上是否存在 RSS Feed,然后通知 Service Worker 显示该页面的操作图标。
这种通信采用消息传递方式,可让扩展程序和内容脚本监听彼此的消息,并在同一通道上进行响应。消息可以包含任何有效的 JSON 对象(null、布尔值、数字、字符串、数组或对象)。有两种消息传递 API:一种适用于一次性请求,另一种适用于长期连接(允许发送多条消息)。如需了解如何在扩展程序之间发送消息,请参阅跨扩展程序消息部分。
一次性请求
如需将单条消息发送到扩展程序的其他部分并选择性地接收响应,请调用 runtime.sendMessage()
或 tabs.sendMessage()
。通过这些方法,您可以从内容脚本向扩展程序发送一次性 JSON 可序列化消息,或者从扩展程序向内容脚本发送。如需处理响应,请使用返回的 promise。为了向后兼容旧版扩展程序,您可以改为将回调作为最后一个参数传递。您不能在同一调用中使用 promise 和回调。
如需了解如何将回调转换为 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);
})();
如需向内容脚本发送请求,请指定请求应用于哪个标签页,如下所示。此示例适用于 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
事件只会在发送端口触发,而不会在其他端口上触发。
跨附加信息消息传递
除了在扩展程序的不同组件之间发送消息之外,您还可以使用 Messaging API 与其他扩展程序进行通信。这样,您就可以公开公共 API 以供其他扩展程序使用。
如需监听传入请求和来自其他扩展程序的连接,请使用 runtime.onMessageExternal
或 runtime.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 simple 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.
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 监听网页中的消息,就像在跨扩展程序消息传递中一样。示例如下:
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);
});
原生消息传递
扩展程序可与注册为原生消息传递主机的原生应用交换消息。如需详细了解此功能,请参阅原生即时通讯。
安全注意事项
以下是一些与消息功能相关的安全注意事项。
内容脚本的可信度较低
内容脚本不如扩展 Service Worker 可信。 例如,恶意网页可能会破坏运行内容脚本的渲染进程。假设来自内容脚本的消息可能是由攻击者精心制作,并确保验证并清理所有输入。 假设发送给内容脚本的所有数据都可能泄露到网页。 限制从内容脚本接收的消息触发的特权操作的范围。
跨站点脚本攻击
请务必保护您的脚本免受跨站脚本攻击的侵害。从不受信任的来源(例如用户输入、通过内容脚本或 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; });