API обмена сообщениями позволяют вам обмениваться данными между различными скриптами, работающими в контекстах, связанных с вашим расширением. Это включает в себя взаимодействие между вашим сервис-воркером, chrome-extension://pages и скриптами контента. Например, расширение для чтения RSS-лент может использовать скрипты контента для определения наличия RSS-ленты на странице, а затем уведомлять сервис-воркер о необходимости обновить значок действия для этой страницы.
Существует два API передачи сообщений: один для одноразовых запросов и более сложный для долгосрочных соединений , позволяющий отправлять несколько сообщений.
Информацию об отправке сообщений между расширениями см. в разделе «Сообщения между расширениями» .
Разовые запросы
Чтобы отправить одно сообщение другой части вашего расширения и при необходимости получить ответ, вызовите метод runtime.sendMessage() или tabs.sendMessage() . Эти методы позволяют отправить однократное сообщение, сериализуемое в JSON, из скрипта контента в расширение или из расширения в скрипт контента. Оба API возвращают Promise, который преобразуется в ответ, предоставленный получателем.
Отправка запроса из контент-скрипта выглядит так:
контент-скрипт.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 или вернуть промис.
Возвращает true
Для асинхронного ответа с помощью sendResponse() верните из обработчика событий буквальное true (а не просто истинное значение). Это позволит поддерживать канал связи с другой стороной открытым до вызова sendResponse , что даст вам возможность вызвать его позже.
Верни обещание
Начиная с Chrome 144, вы можете возвращать промис из обработчика сообщений для асинхронного ответа. Если промис разрешается, его разрешенное значение отправляется в качестве ответа.
Если обещание отклонено, вызов функции sendMessage() отправителя будет отклонен с сообщением об ошибке. Дополнительные сведения и примеры см. в разделе обработки ошибок .
Пример, демонстрирующий возврат промиса, который может быть разрешен или отклонен:
// 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 , чтобы он возвращал промис:
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};
});
Возврат промиса: особенности async функций
Следует помнить, что async функция в качестве слушателя всегда возвращает промис, даже без оператора return . Если async слушатель не возвращает значение, его промис неявно разрешается в 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 144, если обработчик onMessage генерирует ошибку (синхронно или асинхронно, возвращая промис, который отклоняется), промис, возвращаемый функцией sendMessage() в отправителе, будет отклонен с сообщением об ошибке. Это также может произойти, если обработчик пытается вернуть ответ, который не может быть сериализован в JSON, не перехватив при этом возникающую TypeError .
Если слушатель возвращает промис, который отклоняется, он должен отклонить его с экземпляром Error , чтобы отправитель получил это сообщение об ошибке. Если промис отклоняется с любым другим значением (например, null или undefined ), sendMessage() будет отклонен с общим сообщением об ошибке.
Если для onMessage зарегистрировано несколько обработчиков событий, то на отправителя повлияет только первый обработчик, который ответит, отклонит запрос или выдаст ошибку; все остальные обработчики будут запущены, но их результаты будут проигнорированы.
Примеры
Если слушатель возвращает промис, который отклоняется, 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() этого слушателя отклоняется:
// 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()для передачи сообщений со страницы расширения в скрипт содержимого.
Вы можете дать имя своему каналу, передав параметр options с ключом name , чтобы различать разные типы подключений:
const port = chrome.runtime.connect({name: "example"});
Одним из возможных вариантов использования долговременного соединения является расширение для автоматического заполнения форм. Скрипт контента может открыть канал на страницу расширения для конкретного имени пользователя и отправлять расширению сообщение для каждого элемента ввода на странице с запросом данных формы для заполнения. Общее соединение позволяет расширению обмениваться состоянием между компонентами расширения.
При установлении соединения каждому концу назначается объект runtime.Port для отправки и получения сообщений через это соединение.
Используйте следующий код для открытия канала из скрипта контента, а также для отправки и прослушивания сообщений:
контент-скрипт.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 МБ.
Срок службы порта
Порты предназначены для двусторонней связи между различными частями расширения. Когда часть расширения вызывает 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.
});
});
Чтобы отправить сообщение на другой номер, передайте идентификатор этого номера, с которым вы хотите связаться, следующим образом:
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 обмена сообщениями для любой страницы, соответствующей указанным шаблонам URL. Шаблон URL должен содержать как минимум домен второго уровня ; то есть шаблоны имён хостов, такие как «*», «*.com», «*.co.uk» и «*.appspot.com», не поддерживаются. Для доступа ко всем доменам можно использовать <all_urls> .
Используйте API runtime.sendMessage() или runtime.connect() для отправки сообщения на определённый номер. Например:
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);
}
);
}
Прослушивайте сообщения с веб-страниц из своего расширения, используя API runtime.onMessageExternal или runtime.onConnectExternal , как при обмене сообщениями между расширениями . Вот пример:
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; });