Przekazywanie wiadomości

Skrypty treści działają w kontekście strony internetowej, a nie rozszerzenia, które je uruchamia, dlatego często potrzebują możliwości komunikowania się z resztą rozszerzenia. Na przykład rozszerzenie do czytnika RSS może użyć skryptów treści, aby wykryć kanał RSS na stronie, a następnie powiadomić skrypt service worker o wyświetleniu ikony działania dla tej strony.

W ramach tej komunikacji używane są przekazywanie wiadomości, które pozwala zarówno rozszerzeniom, jak i skryptom treści nasłuchiwać wiadomości innych osób i reagować na nie w tym samym kanale. Wiadomość może zawierać dowolny prawidłowy obiekt JSON (null, boolean, number, string, tablicy lub obiekt). Istnieją 2 interfejsy API do przekazywania wiadomości: jeden do żądań jednorazowych i bardziej złożony dla połączeń długotrwałych, które umożliwiają wysyłanie wielu wiadomości. Informacje o wysyłaniu wiadomości między rozszerzeniami znajdziesz w sekcji Wiadomości dotyczące wielu rozszerzeń.

Żądania jednorazowe

Aby wysłać jedną wiadomość do innej części rozszerzenia i opcjonalnie otrzymać odpowiedź, użyj metody runtime.sendMessage() lub tabs.sendMessage(). Te metody umożliwiają wysłanie jednorazowej, możliwej do sserializowania wiadomości w formacie JSON ze skryptu treści do rozszerzenia lub z rozszerzenia do skryptu treści. Do obsługi odpowiedzi użyj zwróconej obietnicy. Aby zapewnić zgodność wsteczną ze starszymi rozszerzeniami, możesz zamiast tego przekazać wywołanie zwrotne jako ostatni argument. W tej samej rozmowie nie możesz użyć obiecacji i oddzwonienia.

Informacje o przekształcaniu wywołań zwrotnych na obietnice i o używaniu ich w rozszerzeniach znajdziesz w przewodniku po migracji do platformy Manifest V3.

Wysyłanie żądania ze skryptu dotyczącego treści wygląda tak:

content-script.js:

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

Aby wysłać żądanie do skryptu dotyczącego treści, określ kartę, której ono dotyczy, zgodnie z opisem poniżej. Ten przykład działa na stronach service worker, wyskakujących okienek i stronach chrome-extension:// otwartych jako karta.

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

Aby odebrać wiadomość, skonfiguruj odbiornik zdarzeń runtime.onMessage. Korzystają one z tego samego kodu zarówno w rozszerzeniach, jak i w skryptach treści:

content-script.js lub 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"});
  }
);

W poprzednim przykładzie funkcja sendResponse() była wywoływana synchronicznie. Aby używać sendResponse() asynchronicznie, dodaj element return true; do modułu obsługi zdarzeń onMessage.

Jeśli wiele stron nasłuchuje zdarzeń onMessage, odpowiedź zostanie wysłana tylko wtedy, gdy pierwsza z nich wywoła metodę sendResponse() w przypadku danego zdarzenia. Wszystkie pozostałe odpowiedzi na to zdarzenie będą ignorowane.

Długotrwałe połączenia

Aby utworzyć długoterminowy kanał przekazu wiadomości wielokrotnego użytku, wywołaj runtime.connect(), aby przekazywać wiadomości ze skryptu treści na stronę rozszerzenia, lub tabs.connect(), aby przekazywać komunikaty ze strony rozszerzenia do skryptu treści. Możesz nazwać swój kanał, aby rozróżnić różne typy połączeń.

Jednym z możliwych zastosowań długotrwałego połączenia jest automatyczne rozszerzenie formularza. W przypadku określonego loginu skrypt treści może otworzyć kanał prowadzący na stronę rozszerzenia, a następnie dla każdego elementu wejściowego na stronie wysłać do rozszerzenia wiadomość z prośbą o uzupełnienie danych formularza. Połączenie współdzielone umożliwia rozszerzeniu współdzielenie stanu między jego komponentami.

Podczas nawiązywania połączenia każdy koniec otrzymuje obiekt runtime.Port do wysyłania i odbierania wiadomości przez to połączenie.

Użyj tego kodu, aby otworzyć kanał ze skryptu tworzenia treści oraz wysyłać i odsłuchiwać wiadomości:

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

Aby wysłać żądanie z rozszerzenia do skryptu dotyczącego treści, zamień wywołanie runtime.connect() w poprzednim przykładzie na tabs.connect().

Aby obsługiwać połączenia przychodzące na potrzeby skryptu treści lub strony rozszerzenia, skonfiguruj odbiornik zdarzeń runtime.onConnect. Gdy inna część rozszerzenia wywoła connect(), aktywuje to zdarzenie i obiekt runtime.Port. Kod odpowiadania na połączenia przychodzące wygląda tak:

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

Czas eksploatacji portu

Porty zaprojektowano jako dwukierunkową komunikację między różnymi częściami rozszerzenia. Ramka najwyższego poziomu to najmniejsza część rozszerzenia, która może korzystać z portu. Gdy część rozszerzenia wywołuje numer tabs.connect(), runtime.connect() lub runtime.connectNative(), tworzony jest port, z którego można natychmiast wysyłać wiadomości przy użyciu numeru postMessage().

Jeśli na karcie znajduje się wiele klatek, wywołanie metody tabs.connect() wywołuje zdarzenie runtime.onConnect raz na każdą klatkę na karcie. I podobnie, jeśli zdarzenie runtime.connect() jest wywoływane, zdarzenie onConnect może być wywołane raz na każdą klatkę w procesie rozszerzenia.

Może Ci się przydać informacje o zamknięciu połączenia, na przykład jeśli używasz osobnego stanu dla każdego otwartego portu. Aby to zrobić, odsłuchaj zdarzenie runtime.Port.onDisconnect. To zdarzenie jest wywoływane, gdy na drugim końcu kanału nie ma prawidłowych portów. Może to mieć jedną z tych przyczyn:

  • Po drugiej stronie nie ma detektorów elementu runtime.onConnect.
  • Karta zawierająca port jest wyładowana (np. gdy użytkownik może ją przeglądać).
  • Ramka, w której miała miejsce wywołanie connect(), została wyładowana z pamięci.
  • Wszystkie klatki, które otrzymały port (za pomocą runtime.onConnect), zostały wyładowane.
  • Narzędzie runtime.Port.disconnect() jest wywoływane przez drugą stronę. Jeśli wywołanie connect() powoduje wiele portów po stronie odbiorcy, a disconnect() jest wywoływany w dowolnym z tych portów, zdarzenie onDisconnect uruchamia się tylko w porcie wysyłającym, a nie w innych portach.

Przesyłanie wiadomości w różnych rozszerzeniach

Oprócz wysyłania komunikatów między różnymi komponentami rozszerzenia możesz używać interfejsu Messaging API do innych rozszerzeń. Dzięki temu możesz udostępnić publiczny interfejs API do użycia przez inne rozszerzenia.

Aby nasłuchiwać żądań przychodzących i połączeń z innych rozszerzeń, użyj metod runtime.onMessageExternal lub runtime.onConnectExternal. Oto przykłady każdego z nich:

service-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.
  });
});

Aby wysłać wiadomość do innego rozszerzenia, podaj identyfikator rozszerzenia, z którym chcesz się komunikować w ten sposób:

service-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(...);

Wysyłanie wiadomości ze stron internetowych

Rozszerzenia mogą też odbierać wiadomości z innych stron internetowych i odpowiadać na nie, ale nie mogą wysyłać wiadomości do tych stron. Aby wysyłać wiadomości ze strony internetowej do rozszerzenia, za pomocą klucza manifestu "externally_connectable" określ w manifest.json, z którymi witrynami chcesz się komunikować. Na przykład:

manifest.json

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

Dzięki temu interfejs API do przesyłania wiadomości będzie widoczny dla każdej strony pasującej do określonych wzorców adresów URL. Wzorzec adresu URL musi zawierać co najmniej domenę drugiego poziomu. Oznacza to, że wzorce nazw hosta, takie jak „*”, „*.com”, „*.co.uk” i „*.appspot.com” nie są obsługiwane. Od Chrome 107 możesz używać <all_urls> do uzyskiwania dostępu do wszystkich domen. Pamiętaj, że ten problem ma wpływ na wszystkie hosty, dlatego sprawdzanie rozszerzeń, które z niego korzystają, może potrwać dłużej w sklepie Chrome Web Store.

Za pomocą interfejsów API runtime.sendMessage() lub runtime.connect() wyślij wiadomość do określonej aplikacji lub rozszerzenia. Na przykład:

webpage.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);
  });

Słuchaj wiadomości ze stron internetowych za pomocą interfejsów API runtime.onMessageExternal lub runtime.onConnectExternal, np. wiadomości z różnymi rozszerzeniami. Oto przykład:

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

Wiadomości natywne

Rozszerzenia mogą wymieniać wiadomości z aplikacjami natywnymi, które są zarejestrowane jako hosty natywnego przesyłania komunikatów. Więcej informacji o tej funkcji znajdziesz w artykule Komunikaty natywne.

Bezpieczeństwo

Oto kilka kwestii dotyczących bezpieczeństwa związanych z przesyłaniem wiadomości.

Skrypty treści są mniej wiarygodne

Skrypty treści są mniej wiarygodne niż skrypt service worker rozszerzenia. Na przykład szkodliwa strona internetowa może naruszyć proces renderowania, w którym uruchamiane są skrypty treści. Załóżmy, że wiadomości ze skryptu treści mogły zostać przygotowane przez atakującego, i zweryfikuj i oczyść wszystkie dane wejściowe. Załóż, że dane wysyłane do skryptu dotyczącego treści mogą wycieknąć na stronę internetową. Ogranicz zakres uprawnień z uprawnieniami, które mogą być aktywowane przez wiadomości otrzymane ze skryptów treści.

Cross-site scripting

Pamiętaj, aby chronić skrypty przed skryptami w witrynach. Gdy odbierasz dane z niezaufanego źródła, np. dane wejściowe użytkownika, inne strony za pomocą skryptu treści lub interfejsu API, unikaj interpretowania ich jako danych HTML i unikania używania ich w sposób, który może umożliwić uruchomienie nieoczekiwanego kodu.

Bezpieczniejsze metody

Gdy to możliwe, używaj interfejsów API, które nie uruchamiają skryptów:

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

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;
});
Niebezpieczne metody

Unikaj tych metod, które sprawią, że rozszerzenie będzie podatne na ataki:

service-worker.js.

chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) {
  // WARNING! Might be evaluating a malicious script!
  var 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;
});