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

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

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

Разовые запросы

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

Информацию о преобразовании обратных вызовов в обещания и их использовании в расширениях см. в руководстве по миграции Manifest V3 .

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

контент-script.js:

(async () => {
  const response = await chrome.runtime.sendMessage({greeting: "hello"});
  // do something with response here, not outside the function
  console.log(response);
})();

Если вы хотите синхронно ответить на сообщение, просто вызовите sendResponse , как только получите ответ, и верните false чтобы указать, что все готово. Чтобы ответить асинхронно, верните true , чтобы обратный вызов sendResponse оставался активным до тех пор, пока вы не будете готовы его использовать. Асинхронные функции не поддерживаются, поскольку они возвращают обещание, которое не поддерживается.

Чтобы отправить запрос в сценарий содержимого, укажите, к какой вкладке относится запрос, как показано ниже. Этот пример работает в сервис-воркерах, всплывающих окнах и страницах chrome-extension://, открытых как вкладка.

(async () => {
  const [tab] = await chrome.tabs.query({active: true, lastFocusedWindow: true});
  const response = await chrome.tabs.sendMessage(tab.id, {greeting: "hello"});
  // do something with response here, not outside the function
  console.log(response);
})();

Чтобы получить сообщение, настройте прослушиватель событий runtime.onMessage . Они используют один и тот же код как в расширениях, так и в сценариях контента:

content-script.js или service-worker.js:

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() для определенного события, сможет отправить ответ. Все остальные ответы на это событие будут игнорироваться.

Долгосрочные связи

Чтобы создать многоразовый долгоживущий канал передачи сообщений, вызовите runtime.connect() для передачи сообщений из сценария содержимого на страницу расширения или tabs.connect() для передачи сообщений со страницы расширения в сценарий содержимого. Вы можете назвать свой канал, чтобы различать разные типы подключений.

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

При установке соединения каждому концу назначается объект runtime.Port для отправки и получения сообщений через это соединение.

Используйте следующий код, чтобы открыть канал из сценария содержимого, а также отправлять и прослушивать сообщения:

контент-script.js:

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"});
});

Чтобы отправить запрос от расширения в скрипт содержимого, замените вызов runtime.connect() в предыдущем примере на tabs.connect() .

Чтобы обрабатывать входящие соединения для сценария содержимого или страницы расширения, настройте прослушиватель событий runtime.onConnect . Когда другая часть вашего расширения вызывает connect() , она активирует это событие и объект runtime.Port . Код ответа на входящие соединения выглядит следующим образом:

сервис-worker.js:

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 . Вот пример каждого:

сервис-worker.js

// For a single request:
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.
  });
});

Чтобы отправить сообщение на другое расширение, передайте идентификатор расширения, с которым вы хотите связаться, следующим образом:

сервис-worker.js

// The ID of the extension we want to talk to.
var laserExtensionId = "abcdefghijklmnoabcdefhijklmnoabc";

// For a simple request:
chrome.runtime.sendMessage(laserExtensionId, {getTargetData: true},
  function(response) {
    if (targetInRange(response.targetData))
      chrome.runtime.sendMessage(laserExtensionId, {activateLasers: true});
  }
);

// For a long-lived connection:
var port = chrome.runtime.connect(laserExtensionId);
port.postMessage(...);

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

Расширения также могут получать сообщения с других веб-страниц и отвечать на них, но не могут отправлять сообщения на веб-страницы. Чтобы отправлять сообщения с веб-страницы в расширение, укажите в manifest.json с какими веб-сайтами вы хотите взаимодействовать, используя ключ манифеста "externally_connectable" . Например:

манифест.json

"externally_connectable": {
  "matches": ["https://*.example.com/*"]
}

При этом API обмена сообщениями будет доступен для любой страницы, соответствующей указанным вами шаблонам URL-адресов. Шаблон URL-адреса должен содержать как минимум домен второго уровня ; то есть шаблоны имен хостов, такие как «*», «*.com», «*.co.uk» и «*.appspot.com», не поддерживаются. Начиная с Chrome 107, вы можете использовать <all_urls> для доступа ко всем доменам. Обратите внимание: поскольку это затрагивает все хосты, проверка в интернет-магазине Chrome расширений, которые его используют, может занять больше времени .

Используйте API-интерфейсы runtime.sendMessage() или runtime.connect() для отправки сообщения в определенное приложение или расширение. Например:

веб-страница.js

// 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 как при обмене сообщениями между расширениями . Вот пример:

сервис-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, которые не запускают скрипты:

сервис-worker.js

chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) {
  // JSON.parse doesn't evaluate the attacker's scripts.
  var resp = JSON.parse(response.farewell);
});

сервис-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;
});
Небезопасные методы

Избегайте использования следующих методов, которые делают ваше расширение уязвимым:

сервис-worker.js

chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) {
  // WARNING! Might be evaluating a malicious script!
  var resp = eval(`(${response.farewell})`);
});

сервис-worker.js

chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) {
  // WARNING! Might be injecting a malicious script!
  document.getElementById("resp").innerHTML = response.farewell;
});