Если вы хотите создать расширение Chrome со сложными функциями, скорее всего, вам нужно, чтобы ваши фоновые скрипты (background), скрипты контента (content) и скрипты всплывающих окон (popup) работали вместе и реагировали друг на друга.

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

Прежде чем читать дальше и экспериментировать, вы должны понимать, как создавать расширение Chrome и загружать его в браузере для тестирования.

Примеры написаны для расширения, использующее Manifest v3. Его большое отличие от v2 - возможность использовать async/await. Однако если вы используете v2, просто применяйте обратные вызовы вместо async/await.

Краткий обзор архитектуры расширения

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

В расширении нам будут важны 3 основных компонента:

  • Элементы пользовательского интерфейса (popup.html + popup.js)
  • Скрипт background
  • Скрипт content

архитектура расширения

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

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

Наконец, скрипт background.js (он же сервис-воркер в v3) в основном используется для обработки событий. Он загружается один раз и остается бездействующим, если не происходит никакого интересного события. Он не может напрямую обращаться к DOM, но может быть очень полезен для таких целей, как перехват исходящих и входящих запросов к сайту.

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

Последняя ключевая вещь, которую следует напомнить, это то, что content-скрипт работает в контексте текущей активной вкладки. Поэтому каждый раз, когда мы хотим связаться со скриптом popup или background, нам нужно указать, на какую вкладку отправлять сообщение, и это касается как разовых сообщений, так и долгоживущих соединений.

Разовые сообщения

Разовые сообщения полезны, если вы хотите отправить одно сообщение в другие части расширения. Вы можете отправить разовый запрос из content-скрипта в popup или наоборот, и отреагировать ответным сообщением.

На высоком уровне идея состоит в том, что один из скриптов будет отправителем сообщения, а получатель настроит прослушиватель любых входящих сообщений. Когда сообщение получено, прослушиватель запускается и может дополнительно отправить ответ отправителю сообщения. Слушатель добавляется с помощью команды chrome.runtime.onMessage.addListener.

В зависимости от получателя, Chrome предоставляет нам 2 типа методов: chrome.runtime.sendMessage и chrome.tabs.sendMessage. Важно знать, когда какой метод использовать.

Когда использовать chrome.tabs.sendMessage

При отправке сообщения content-скрипту нам нужно указать, на какую вкладку его отправить. Поэтому нам нужно сначала получить информацию об активной вкладке, а затем использовать chrome.tabs.sendMessage. Чтобы использовать API вкладок и иметь доступ к активной вкладке, необходимо добавить поля tabs и activeTab в файле manifest.json.

// popup.js
const sendMessageButton = document.getElementById('sendMessage')
sendMessageButton.onclick = async function(e) {
  let queryOptions = { active: true, currentWindow: true };
  let tab = await chrome.tabs.query(queryOptions);
  let message = {name: "Ivan"};
  chrome.tabs.sendMessage(tab[0].id, message, function(response) {
    console.log(response.status);
  });
}
// content.js
chrome.runtime.onMessage.addListener(
  function(request, sender, sendResponse) {
    if (request.name === "Ivan") {
      let user = document.getElementById("userName");
      user.innerHTML = request.name;
      sendResponse({status: "done"});
    }
  }
);

Приведенный выше код показывает, как скрипт расширения popup может вызвать изменение внешнего вида сайта. Когда пользователь нажимает sendMessageButton, обработчик запрашивает текущую активную вкладку и отправляет сообщение в скрипт content для этой вкладки с помощью chrome.tabs.sendMessage.

Метод принимает 3 параметра:

  • ID активной вкладки
  • Сообщение (может быть объектом JSON)
  • Функция, которая запускается после ответа получателя

На стороне скрипта content, который является получателем, необходимо прослушивать входящие сообщения. Это делается с помощью команды chrome.runtime.onMessage.addListener. Функция слушателя - это функция, которая имеет три параметра: request, sender, sendResponse.

Параметр request - это отправленное сообщение, sender - это объект, содержащий информацию о контексте скрипта, который отправил сообщение, а sendResponse - это функция, которая принимает JSON-объект для ответа отправителю.

Когда использовать chrome.runtime.sendMessage

Если вы отправляете сообщения из content-скрипта в popup-скрипт, достаточно будет использовать chrome.runtime.sendMessage. Этот метод не принимает идентификатор вкладки в качестве первого параметра, но остальная часть сигнатуры функции такая же, как у chrome.tabs.sendMessage. Код стороны получателя остается прежним: chrome.runtime.onMessage.addListener.

У вас также может быть несколько элементов в popup.js, отправляющих разные сообщения получателю для различных действий на активном в данный момент сайте.

Долгоживущие соединения

Долгоживущие соединения позволяют открывать соединение, которое длится дольше, чем одиночный запрос. Это можно осуществить, используя chrome.runtime.connect и chrome.tabs.connect.

Пример открытия соединения из popup в content:

// popup.js
const sendMessageButton = document.getElementById('sendMessage');
sendMessageButton.onclick = async function(e) {
  let queryOptions = { active: true, currentWindow: true };
  let tabs = await chrome.tabs.query(queryOptions);
  
  // Открываем соединение
  const port = chrome.tabs.connect(tabs[0].id, {
    name: "qwerty",
  });

  // Возьмем значение input и отправим его
  const inputText = document.getElementById('input')
  port.postMessage({
    text: inputText.value
  });

  port.onMessage.addListener(function(msg) {
    if (msg.exists) {
      console.log('В слове больше 5ти букв');
    } else {
      console.log('В слове не больше 5ти букв');
    }
  })
}

Как видим, мы открываем соединение с content-скриптом активной вкладки и передаем объект с полем name. Это поле позволяет нам различать несколько открытых соединений (если они есть у расширения). Метод connect возвращает объект port и мы можем его использовать для отправки сообщений с помощью port.postMessage и, при желании, добавить прослушиватель onMessage для ответов.

Далее, мы отправляем сообщение принимающей стороне (content-скрипту). Сообщение представляет собой объект с любыми данными, которые вы хотите. В нашем случае это значение поля inputText.

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

// content.js
chrome.runtime.onConnect.addListener(function (port) {
  port.onMessage.addListener(function (msg) {
    if (port.name === "qwerty") {
      const text = msg.text;
      if (text.length > 5) {
        port.postMessage({
          exists: true,
        });
      } else {
        port.postMessage({
          exists: false,
        });
      }
    }
  });
});

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

Когда в скрипте popup запускается метод .connect, в скрипте content отрабатывает событие onConnect и объект port включается в прослушиватель. А его мы можем использовать для добавления прослушивателей сообщений с помощью chrome.onMessage.addListener или для ответа с помощью chrome.port.postMessage.

Также мы проверяем ключ name объекта port, поскольку у нас могут быть другие соединения с другими значениями name, и в зависимости от того, какой нам нужен, определяем дальнейшие действия.

В нашем примере, мы берем значение параметра text из принятого объекта и определяем количество его символов. В зависимости от этого, отправляем соответствующее сообщение обратно отправителю. Это запустит прослушиватель onMessage, определенный в popup.js, завершив цикл связи.