Поскольку скрипты содержимого выполняются в контексте веб-страницы, а не расширения, им часто требуется какой-то способ взаимодействия с остальной частью расширения. Например, расширение для чтения RSS-лент может использовать скрипты содержимого для обнаружения наличия RSS-ленты на странице, а затем уведомлять фоновую страницу, чтобы отобразить значок действия для этой страницы.
Обмен данными между расширениями и их скриптами контента осуществляется посредством передачи сообщений. Обе стороны могут прослушивать сообщения, отправленные с другой стороны, и отвечать по тому же каналу. Сообщение может содержать любой допустимый JSON-объект (null, boolean, number, string, array или object). Существует простой API для разовых запросов и более сложный API, позволяющий устанавливать долговременные соединения для обмена несколькими сообщениями с общим контекстом. Также можно отправить сообщение другому расширению, если известен его ID, что описано в разделе о межрасширенных сообщениях .
Простые разовые запросы
Если вам нужно отправить только одно сообщение в другую часть вашего расширения (и, при необходимости, получить ответ), следует использовать упрощенные методы runtime.sendMessage или tabs.sendMessage . Это позволяет отправить одноразовое JSON-сериализуемое сообщение из скрипта контента в расширение или наоборот, соответственно. Необязательный параметр обратного вызова позволяет обрабатывать ответ с другой стороны, если таковой имеется.
Отправка запроса из скрипта контента выглядит следующим образом:
chrome.runtime.sendMessage({greeting: "hello"}, function(response) {
console.log(response.farewell);
});
Отправка запроса из расширения в скрипт контента выглядит очень похоже, за исключением того, что необходимо указать, в какую вкладку его отправить. В этом примере демонстрируется отправка сообщения в скрипт контента на выбранной вкладке.
chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
chrome.tabs.sendMessage(tabs[0].id, {greeting: "hello"}, function(response) {
console.log(response.farewell);
});
});
На принимающей стороне необходимо настроить обработчик событий runtime.onMessage для обработки сообщения. Это выглядит одинаково как в скрипте контента, так и на странице расширения.
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 .
sendResponse действительна только при синхронном использовании или если обработчик событий возвращает true , указывая на асинхронный ответ. Функция обратного вызова sendMessage будет вызвана автоматически, если ни один из обработчиков не возвращает true или если функция обратного вызова sendResponse удалена сборщиком мусора.Долгосрочные связи
Иногда полезно вести диалог, который длится дольше, чем один запрос и ответ. В этом случае вы можете открыть долговременный канал из своего скрипта контента на страницу расширения или наоборот, используя runtime.connect или tabs.connect соответственно. Канал может иметь дополнительное имя, позволяющее различать разные типы соединений.
Один из вариантов использования — расширение для автоматического заполнения форм. Скрипт контента может открыть канал связи со страницей расширения для определенного входа в систему и отправить сообщение расширению для каждого элемента ввода на странице, чтобы запросить данные формы для заполнения. Общее соединение позволяет расширению поддерживать общее состояние, связывая несколько сообщений, поступающих от скрипта контента.
При установлении соединения каждому участнику присваивается объект runtime.Port , который используется для отправки и получения сообщений через это соединение.
Вот как открыть канал из скрипта контента, отправлять сообщения и получать их:
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"});
});
Отправка запроса из расширения к скрипту контента выглядит очень похоже, за исключением того, что вам нужно указать, к какой вкладке следует подключиться. Просто замените вызов connect в приведенном выше примере на tabs.connect .
Для обработки входящих соединений необходимо настроить обработчик событий runtime.onConnect . Он выглядит одинаково как в скрипте контента, так и на странице расширения. Когда другая часть вашего расширения вызывает метод "connect()", срабатывает это событие, а вместе с ним и объект runtime.Port , который можно использовать для отправки и получения сообщений через соединение. Вот как выглядит обработка входящих соединений:
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срабатывает только на порту отправителя, а не на других портах.
Обмен сообщениями между различными расширениями
Помимо отправки сообщений между различными компонентами вашего расширения, вы можете использовать API обмена сообщениями для связи с другими расширениями. Это позволяет вам предоставить публичный API, которым могут воспользоваться другие расширения.
Прослушивание входящих запросов и соединений аналогично внутреннему процессу, за исключением того, что используются методы runtime.onMessageExternal или runtime.onConnectExternal . Вот пример каждого из них:
// For simple requests:
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.
});
});
Аналогично, отправка сообщения другому добавочному номеру похожа на отправку сообщения внутри вашего добавочного номера. Единственное отличие заключается в том, что вы должны передать идентификатор добавочного номера, с которым хотите связаться. Например:
// The ID of the extension we want to talk to.
var laserExtensionId = "abcdefghijklmnoabcdefhijklmnoabc";
// Make a simple request:
chrome.runtime.sendMessage(laserExtensionId, {getTargetData: true},
function(response) {
if (targetInRange(response.targetData))
chrome.runtime.sendMessage(laserExtensionId, {activateLasers: true});
}
);
// Start a long-running conversation:
var port = chrome.runtime.connect(laserExtensionId);
port.postMessage(...);
Отправка сообщений с веб-страниц
Подобно обмену сообщениями между расширениями , ваше приложение или расширение может получать сообщения с обычных веб-страниц и отвечать на них. Чтобы использовать эту функцию, необходимо сначала указать в файле manifest.json, с какими веб-сайтами вы хотите взаимодействовать. Например:
"externally_connectable": {
"matches": ["*://*.example.com/*"]
}
Это позволит получить доступ к API обмена сообщениями для любой страницы, соответствующей указанным вами шаблонам URL. Шаблон URL должен содержать как минимум домен второго уровня — то есть, шаблоны имен хостов, такие как "*", "*.com", "*.co.uk" и "*.appspot.com", запрещены. С веб-страницы используйте API runtime.sendMessage или runtime.connect для отправки сообщения в конкретное приложение или расширение. Например:
// 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);
});
Из вашего приложения или расширения вы можете прослушивать сообщения с веб-страниц через API runtime.onMessageExternal или runtime.onConnectExternal , аналогично обмену сообщениями между расширениями . Только веб-страница может инициировать соединение. Вот пример:
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, таких как указанные ниже:
chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) {
// WARNING! Might be evaluating an evil script!
var resp = eval("(" + response.farewell + ")");
});
chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) {
// WARNING! Might be injecting a malicious script!
document.getElementById("resp").innerHTML = response.farewell;
});
Вместо этого отдавайте предпочтение более безопасным API, которые не запускают скрипты:
chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) {
// JSON.parse does not evaluate the attacker's scripts.
var resp = JSON.parse(response.farewell);
});
chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) {
// innerText does not let the attacker inject HTML elements.
document.getElementById("resp").innerText = response.farewell;
});
Примеры
Простые примеры обмена сообщениями можно найти в каталоге examples/api/messaging . Пример нативного обмена сообщениями демонстрирует, как приложение Chrome может взаимодействовать с нативным приложением. Дополнительные примеры и помощь в просмотре исходного кода см. в разделе «Примеры» .