Передача сообщений

Поскольку сценарии содержимого выполняются в контексте веб-страницы, а не расширения, им часто требуется какой-то способ взаимодействия с остальной частью расширения. Например, расширение для чтения RSS может использовать сценарии содержимого для обнаружения присутствия RSS-канала на странице, а затем уведомлять фоновую страницу, чтобы отобразить значок действия на этой странице.

Связь между расширениями и их сценариями содержимого осуществляется с помощью передачи сообщений. Любая сторона может прослушивать сообщения, отправленные с другого конца, и отвечать на том же канале. Сообщение может содержать любой допустимый объект JSON (нулевое значение, логическое значение, число, строку, массив или объект). Существует простой API для одноразовых запросов и более сложный API, позволяющий иметь долгоживущие соединения для обмена несколькими сообщениями с общим контекстом. Также можно отправить сообщение на другое расширение, если вы знаете его идентификатор, который описан в разделе сообщений между расширениями .

Простые разовые запросы

Если вам нужно отправить только одно сообщение в другую часть вашего расширения (и при необходимости получить ответ обратно), вам следует использовать упрощенный runtime.sendMessage или tabs.sendMessage . Это позволяет вам отправить одноразовое сериализуемое в формате JSON сообщение из скрипта контента в Extension или наоборот соответственно. Необязательный параметр обратного вызова позволяет обрабатывать ответ с другой стороны, если таковой имеется.

Отправка запроса из контент-скрипта выглядит так:

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 .

Примечание. Если несколько страниц прослушивают события onMessage, только первая, вызвавшая sendResponse() для определенного события, сможет отправить ответ. Все остальные ответы на это событие будут игнорироваться.
Примечание. Обратный вызов 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"});
});

Отправка запроса от расширения к контент-скрипту выглядит очень похоже, за исключением того, что вам нужно указать, к какой вкладке подключаться. Просто замените вызов подключения в приведенном выше примере на 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(...);

Отправка сообщений с веб-страниц

Подобно обмену сообщениями между расширениями , ваше приложение или расширение может получать сообщения с обычных веб-страниц и отвечать на них. Чтобы использовать эту функцию, вы должны сначала указать в манифесте.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);
  });

Собственный обмен сообщениями

Расширения и приложения могут обмениваться сообщениями с собственными приложениями, зарегистрированными в качестве собственного узла обмена сообщениями . Дополнительные сведения об этой функции см. в разделе Собственный обмен сообщениями .

Соображения безопасности

Сценарии контента менее заслуживают доверия

Сценарии содержимого менее надежны , чем фоновая страница расширения (например, вредоносная веб-страница может поставить под угрозу процесс рендеринга, в котором выполняются сценарии содержимого). Предположим, что сообщения из сценария содержимого могли быть созданы злоумышленником, и обязательно проверяйте и очищайте все вводимые данные . Предположим, что любые данные, отправленные в сценарий содержимого, могут попасть на веб-страницу. Ограничьте объем привилегированных действий, которые могут быть инициированы сообщениями, полученными из сценариев содержимого.

Межсайтовый скриптинг

При получении сообщения от скрипта контента или другого расширения ваши скрипты должны быть осторожны, чтобы не стать жертвой межсайтового скриптинга . Этот совет применим к сценариям, выполняемым внутри фоновой страницы расширения, а также к сценариям содержимого, выполняемым внутри других веб-источников. В частности, избегайте использования опасных 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;
});

Примеры

Простые примеры общения через сообщения вы можете найти в каталоге example/api/messaging . Пример собственного обмена сообщениями демонстрирует, как приложение Chrome может взаимодействовать с собственным приложением. Дополнительные примеры и помощь в просмотре исходного кода см. в разделе «Примеры» .