API обмена сообщениями позволяют взаимодействовать между различными скриптами, работающими в контексте, связанном с вашим расширением. Это включает в себя взаимодействие между вашим сервис-воркером, chrome-extension://pages и скриптами контента. Например, расширение для чтения RSS-лент может использовать скрипты контента для обнаружения наличия RSS-ленты на странице, а затем уведомить сервис-воркер о необходимости обновить значок действия для этой страницы.
Существует два API для передачи сообщений: один для одноразовых запросов , а другой, более сложный, для долговременных соединений , позволяющих отправлять несколько сообщений.
Информацию об отправке сообщений между добавочными номерами см. в разделе «Сообщения между добавочными номерами» .
Разовые запросы
Чтобы отправить одно сообщение в другую часть вашего расширения и, при необходимости, получить ответ, вызовите runtime.sendMessage() или tabs.sendMessage() . Эти методы позволяют отправить одноразовое JSON-сериализуемое сообщение из скрипта контента в расширение или из расширения в скрипт контента. Оба API возвращают 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 или вернуть промис.
Возвращает true
Для асинхронного ответа с помощью sendResponse() верните из обработчика событий буквальное true (а не просто истинное значение). Это позволит поддерживать канал связи с другой стороной открытым до вызова sendResponse , что даст вам возможность вызвать его позже.
Верни обещание
Начиная с Chrome 146, вы можете возвращать промис из обработчика сообщений для асинхронного ответа. Это обновление распространяется постепенно, поэтому оно может быть доступно не во всех браузерах пользователей. Вам следует убедиться, что ваше расширение может обрабатывать как включенную, так и выключенную эту возможность. Использование return true; будет продолжать работать для асинхронных ответов независимо от того, включена эта возможность или нет.
Если обещание выполняется, в качестве ответа отправляется его разрешенное значение.
Если обещание отклонено, вызов функции 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 146, если обработчик onMessage генерирует ошибку (синхронно или асинхронно, возвращая промис, который отклоняется), промис, возвращаемый функцией sendMessage() в отправителе, будет отклонен с сообщением об ошибке. Это обновление распространяется постепенно, поэтому изменение может еще не вступить в силу во всех браузерах пользователей. Вам следует убедиться, что ваше расширение может обрабатывать изменения, независимо от того, включено это изменение или нет.
Если слушатель пытается вернуть ответ, который не может быть сериализован (без обработки возникшей 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 для отправки и получения сообщений через это соединение.
Используйте следующий код для открытия канала из скрипта контента, отправки и прослушивания сообщений:
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 МиБ.
Срок службы порта
Порты предназначены для двусторонней связи между различными частями расширения. Когда часть расширения вызывает 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 , с каких веб-сайтов вы хотите разрешить отправку сообщений, используя ключ 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);
});
Отправить сообщение из расширения на веб-страницу невозможно.
Нативная система обмена сообщениями
Расширения могут обмениваться сообщениями с нативными приложениями, зарегистрированными в качестве хоста нативной системы обмена сообщениями . Подробнее об этой функции см. в разделе «Нативная система обмена сообщениями» .
Вопросы безопасности
Вот несколько моментов, касающихся безопасности обмена сообщениями.
Сценарии контента менее заслуживают доверия.
Скрипты контента менее надежны, чем рабочие процессы служб расширений. Например, вредоносная веб-страница может скомпрометировать процесс рендеринга, который запускает скрипты контента. Следует предполагать, что сообщения от скрипта контента могли быть созданы злоумышленником, и необходимо проверять и очищать все входные данные . Следует также предполагать, что любые данные, отправленные в скрипт контента, могут попасть на веб-страницу. Необходимо ограничить область действия привилегированных действий, которые могут быть запущены сообщениями, полученными от скриптов контента.
Межсайтовый скриптинг
Обязательно защитите свои скрипты от межсайтового скриптинга (XSS) . При получении данных из ненадежного источника, такого как пользовательский ввод, другие веб-сайты через скрипт контента или 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; });