借助消息传递 API,您可以在与扩展程序关联的上下文中运行的不同脚本之间进行通信。这包括服务工作线程、chrome-extension://页面和内容脚本之间的通信。例如,RSS 阅读器扩展程序可以使用内容脚本来检测网页上是否存在 RSS Feed,然后通知服务工作线程更新相应网页的操作图标。
有两种消息传递 API:一种用于一次性请求,另一种更复杂,用于长期连接,可发送多条消息。
如需了解如何在扩展程序之间发送消息,请参阅跨扩展程序消息部分。
一次性请求
如需向扩展程序的另一部分发送单条消息,并选择性地获取响应,请调用 runtime.sendMessage()
或 tabs.sendMessage()
。借助这些方法,您可以从内容脚本向扩展程序发送一次性 JSON 可序列化消息,也可以从扩展程序向内容脚本发送一次性 JSON 可序列化消息。这两个 API 都会返回一个 Promise,该 Promise 会解析为收件人提供的响应。
从内容脚本发送请求如下所示:
content-script.js:
(async () => { const response = await chrome.runtime.sendMessage({greeting: "hello"}); // do something with response here, not outside the function console.log(response); })();
响应
如需监听消息,请使用 chrome.runtime.onMessage
事件:
// Event listener function handleMessages(message, sender, sendResponse) { fetch(message.url) .then((response) => sendResponse({statusCode: response.status})) // Since `fetch` is asynchronous, must return an explicit `true` return true; } chrome.runtime.onMessage.addListener(handleMessages); // From the sender's context... const {statusCode} = await chrome.runtime.sendMessage({ url: 'https://example.com' });
当调用事件监听器时,系统会传递一个 sendResponse
函数作为第三个参数。这是一个可调用的函数,用于提供响应。默认情况下,必须同步调用 sendResponse
回调。如果您想执行异步工作来获取传递给 sendResponse
的值,则必须从事件监听器返回字面值 true
(而不仅仅是真值)。这样做会使消息通道对另一端保持开放状态,直到调用 sendResponse
。
如果您在不带任何参数的情况下调用 sendResponse
,系统会发送 null
作为响应。
如果多个网页都在监听 onMessage
事件,则只有第一个针对特定事件调用 sendResponse()
的网页才能成功发送响应。系统会忽略对该活动的所有其他回复。
长期连接
如需创建可重复使用的长期消息传递渠道,请调用:
- 使用
runtime.connect()
将消息从内容脚本传递到扩展程序页面 tabs.connect()
,用于将消息从扩展程序页面传递到内容脚本。
您可以通过传递带有 name
键的 options 参数来命名频道,以区分不同类型的连接:
const port = chrome.runtime.connect({name: "example"});
长效连接的一个潜在用例是自动表单填充扩展程序。内容脚本可能会为特定登录会话打开与扩展程序页面的通道,并为页面上的每个输入元素向扩展程序发送消息,以请求要填充的表单数据。共享连接允许扩展程序在扩展程序组件之间共享状态。
建立连接时,系统会为每个端点分配一个 runtime.Port
对象,用于通过该连接发送和接收消息。
使用以下代码从内容脚本打开渠道,并发送和监听消息:
content-script.js:
const port = chrome.runtime.connect({name: "knockknock"}); 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"}); } }); port.postMessage({joke: "Knock knock"});
如需从扩展程序向内容脚本发送请求,请将上例中对 runtime.connect()
的调用替换为 tabs.connect()
。
如需处理内容脚本或扩展程序网页的传入连接,请设置 runtime.onConnect
事件监听器。当扩展程序的其他部分调用 connect()
时,它会激活此事件和 runtime.Port
对象。用于响应传入连接的代码如下所示:
service-worker.js:
chrome.runtime.onConnect.addListener(function(port) { if (port.name !== "knockknock") { return; } 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."}); } }); });
序列化
在 Chrome 中,消息传递 API 使用 JSON 序列化。这意味着,消息(以及收件人提供的回复)可以包含任何有效的 JSON 值(null、布尔值、数字、字符串、数组或对象)。其他值将被强制转换为可序列化的值。
值得注意的是,这与其他使用结构化克隆算法实现相同 API 的浏览器不同。
端口生命周期
端口旨在作为扩展程序不同部分之间的双向通信机制。当扩展程序的一部分调用 tabs.connect()
、runtime.connect()
或 runtime.connectNative()
时,它会创建一个 Port,该 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
方法。以下是每种类型的示例:
service-worker.js
// For a single request: chrome.runtime.onMessageExternal.addListener( function(request, sender, sendResponse) { if (sender.id !== allowlistedExtension) { return; // don't allow this extension access } if (request.getTargetData) { sendResponse({ targetData: targetData }); } else if (request.activateLasers) { const 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. const 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: const port = chrome.runtime.connect(laserExtensionId); port.postMessage(...);
从网页发送消息
扩展程序还可以接收和回复来自网页的消息。如需从网页向扩展程序发送消息,请在 manifest.json
中使用 "externally_connectable"
清单键指定您要允许哪些网站发送消息。例如:
manifest.json
"externally_connectable": { "matches": ["https://*.example.com/*"] }
这会将消息传递 API 公开给与您指定的网址模式匹配的任何网页。网址模式必须至少包含一个二级网域;也就是说,不支持“*”“*.com”“*.co.uk”和“*.appspot.com”等主机名模式。您可以使用 <all_urls>
访问所有网域。
使用 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.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); });
无法从扩展程序 向网页 发送消息。
原生消息传递
扩展程序可以与注册为原生消息传递主机的原生应用交换消息。如需详细了解此功能,请参阅原生消息传递。
安全注意事项
以下是与消息传递相关的一些安全注意事项。
内容脚本的可信度较低
与扩展程序服务工作线程相比,内容脚本的可信度较低。例如,恶意网页可能能够入侵运行内容脚本的渲染进程。假设来自内容脚本的消息可能由攻击者精心制作,并确保验证和清理所有输入。假设发送到内容脚本的任何数据都可能会泄露到网页。 限制可由从内容脚本接收的消息触发的特权操作的范围。
跨站点脚本攻击
请务必保护您的脚本免受跨站脚本攻击。从不可信的来源(例如用户输入、通过内容脚本访问的其他网站或 API)接收数据时,请注意避免将这些数据解读为 HTML 或以可能导致意外代码运行的方式使用这些数据。
尽可能使用不运行脚本的 API:
service-worker.js
chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) { // JSON.parse doesn't evaluate the attacker's scripts. const 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! const 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; });