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

API обмена сообщениями позволяют вам обмениваться данными между различными скриптами, работающими в контекстах, связанных с вашим расширением. Это включает в себя взаимодействие между вашим сервис-воркером, chrome-extension://pages и скриптами контента. Например, расширение для чтения RSS-лент может использовать скрипты контента для определения наличия RSS-ленты на странице, а затем уведомлять сервис-воркер о необходимости обновить значок действия для этой страницы.

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

Информацию об отправке сообщений между расширениями см. в разделе «Сообщения между расширениями» .

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

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

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

контент-скрипт.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 144, вы можете возвращать промис из обработчика сообщений для асинхронного ответа. Если промис разрешается, его разрешенное значение отправляется в качестве ответа.

Если обещание отклонено, вызов функции 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 144, если обработчик onMessage генерирует ошибку (синхронно или асинхронно, возвращая промис, который отклоняется), промис, возвращаемый функцией sendMessage() в отправителе, будет отклонен с сообщением об ошибке. Это также может произойти, если обработчик пытается вернуть ответ, который не может быть сериализован в JSON, не перехватив при этом возникающую 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 для отправки и получения сообщений через это соединение.

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

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

Невозможно отправить сообщение из расширения на веб-страницу.

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

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

Вопросы безопасности

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

Скрипты контента менее надежны

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

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

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