借助消息传递 API,您可以在与扩展程序关联的上下文中运行的不同脚本之间进行通信。这包括 Service Worker、chrome-extension://pages 和内容脚本之间的通信。例如,RSS 阅读器扩展程序可能会使用内容脚本来检测网页上是否存在 RSS Feed,然后通知 Service Worker 更新该网页的操作图标。
消息传递 API 有两种:一种用于 一次性请求,另一种用于允许发送多条消息的 长期连接,这种 API 更复杂。
如需了解如何在扩展程序之间发送消息,请参阅跨扩展程序消息部分。
一次性请求
如需向扩展程序的另一部分发送一条消息,并选择性地获取
获取响应,请调用 runtime.sendMessage() 或 tabs.sendMessage()。
借助这些方法,您可以将一次性 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) {
if (message !== 'get-status') return;
fetch('https://example.com')
.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('get-status');
调用事件监听器时,系统会将 sendResponse 函数作为第三个形参传递。这是一个可以调用以提供响应的函数。默认情况下,必须同步调用
sendResponse 回调。
如果您在不带任何形参的情况下调用 sendResponse,系统会发送 null 作为响应。
如需异步发送响应,您有两种选择:返回 true 或返回 Promise。
返回 true
如需使用 sendResponse() 异步响应,请从事件监听器返回字面量 true(而不仅仅是真值)。这样做会使消息通道对另一端保持开放状态,直到调用
sendResponse 为止,从而允许您稍后调用它。
返回 Promise
从 Chrome 146 开始,您可以从消息监听器返回 Promise 以异步响应。此更新正在逐步推出,因此您可能会发现它尚未在所有用户的浏览器中提供。您应确保您的扩展程序可以处理此功能是否已启用。无论此功能是否已启用,使用
return true; 都会继续适用于异步响应。
如果 Promise 解析成功,其解析值将作为响应发送。
如果 Promise 被拒绝,发送方的 sendMessage() 调用将被拒绝,并显示错误消息。如需了解详情和示例,请参阅
错误处理部分。
以下示例展示了如何返回可能会解析或拒绝的 Promise:
// Event listener
function handleMessages(message, sender, sendResponse) {
// Return a promise that wraps fetch
// If the response is OK, resolve with the status. If it's not OK then reject
// with the network error that prevents the fetch from completing.
return new Promise((resolve, reject) => {
fetch('https://example.com')
.then(response => {
if (!response.ok) {
reject(response);
} else {
resolve(response.status);
}
})
.catch(error => {
reject(error);
});
});
}
chrome.runtime.onMessage.addListener(handleMessages);
您还可以将监听器声明为 async 以返回 Promise:
chrome.runtime.onMessage.addListener(async function(message, sender) {
const response = await fetch('https://example.com');
if (!response.ok) {
// rejects the promise returned by `async function`.
throw new Error(`Fetch failed: ${response.status}`);
}
// resolves the promise returned by `async function`.
return {statusCode: response.status};
});
返回 Promise:async 函数注意事项
请注意,作为监听器的 async 函数将始终返回 Promise,即使没有 return 语句也是如此。 如果
async 监听器未返回值,其 Promise 会隐式解析为 undefined,并且系统会向发送方发送 null
作为响应。当有多个监听器时,这可能会导致意外行为:
// content_script.js
function handleResponse(message) {
// The first listener promise resolves to `undefined` before the second
// listener can respond. When a listener responds with `undefined`, Chrome
// sends null as the response.
console.assert(message === null);
}
function notifyBackgroundPage() {
const sending = chrome.runtime.sendMessage('test');
sending.then(handleResponse);
}
notifyBackgroundPage();
// background.js
chrome.runtime.onMessage.addListener(async (message) => {
// This just causes the function to pause for a millisecond, but the promise
// is *not* returned from the listener so it doesn't act as a response.
await new Promise(resolve => {
setTimeout(resolve, 1, 'OK');
});
// `async` functions always return promises. So once we
// reach here there is an implicit `return undefined;`. Chrome translates
// `undefined` responses to `null`.
});
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
return new Promise((resolve) => {
setTimeout(resolve, 1000, 'response');
});
});
错误处理
从 Chrome 146 开始,如果 onMessage 监听器抛出错误(同步抛出,或通过返回拒绝的 Promise 异步抛出),发送方
sendMessage() 中返回的 Promise 将被拒绝,并显示错误消息。此更新正在逐步推出,因此您可能会发现此更改尚未在所有用户的浏览器中生效。您应确保您的扩展程序可以处理此更改是否已启用。
如果监听器尝试返回无法
序列化的响应(而不捕获生成的 TypeError),这
也将被视为监听器抛出错误。
如果监听器返回拒绝的 Promise,则必须使用 Error 实例拒绝,以便发送方收到该错误消息。如果 Promise 被拒绝并显示任何其他值(例如
null 或 undefined),sendMessage() 将被拒绝,并显示通用错误消息。
如果为 onMessage 注册了多个监听器,则只有第一个响应、拒绝或抛出错误的监听器会影响发送方;所有其他监听器都会运行,但其结果将被忽略。
示例
如果监听器返回拒绝的 Promise,则 sendMessage() 会被拒绝:
// sender.js
try {
await chrome.runtime.sendMessage('test');
} catch (e) {
console.log(e.message); // "some error"
}
// listener.js
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
return Promise.reject(new Error('some error'));
});
如果监听器使用无法序列化的值进行响应,则 sendMessage() 会被拒绝:
// sender.js
try {
await chrome.runtime.sendMessage('test');
} catch (e) {
console.log(e.message); // "Error: Could not serialize message."
}
// listener.js
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
sendResponse(() => {}); // Functions are not serializable
return true; // Keep channel open for async sendResponse
});
如果监听器在任何其他监听器响应之前同步抛出错误,则监听器的 sendMessage() Promise 会被拒绝:
// sender.js
try {
await chrome.runtime.sendMessage('test');
} catch (e) {
console.log(e.message); // "error!"
}
// listener.js
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
throw new Error('error!');
});
但是,如果一个监听器在另一个监听器抛出错误之前响应,则 sendMessage() 会成功:
// sender.js
const response = await chrome.runtime.sendMessage('test');
console.log(response); // "OK"
// listener.js
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
sendResponse('OK');
});
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
throw new Error('error!');
});
长期连接
如需创建可重复使用的长期消息传递通道,请调用:
runtime.connect(),用于将消息从内容脚本传递到扩展程序页面tabs.connect(),用于将消息从扩展程序页面传递到内容脚本。
您可以通过传递带有 name 键的选项形参来为通道命名,以区分不同类型的连接:
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 序列化。值得注意的是,这与其他 浏览器不同,后者使用相同的 API 和 结构化克隆算法。
这意味着消息(以及收件人提供的响应)可以包含任何
有效的
JSON.stringify()
值。其他值将被强制转换为可序列化的值(值得注意的是,undefined
将被序列化为 null);
消息大小限制
消息的最大大小为 64 MiB。
端口生命周期
端口旨在作为扩展程序不同部分之间的双向通信机制。当扩展程序的某个部分调用
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.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 公开给与您指定的 匹配模式匹配的任何页面。
使用 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);
});
无法从扩展程序向网页发送消息。
原生消息传递
扩展程序可以与注册为 原生消息传递主机的原生应用交换消息。如需详细了解此功能,请参阅原生消息传递。
安全注意事项
以下是与消息传递相关的一些安全注意事项。
内容脚本的信任度较低
内容脚本的信任度低于扩展程序 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. 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; });