Przekazywanie wiadomości

Ponieważ skrypty treści działają w kontekście strony internetowej, a nie rozszerzenia, które je uruchamia, często potrzebują sposobu na komunikację z resztą rozszerzenia. Na przykład rozszerzenie czytnika RSS może używać skryptów treści do wykrywania obecności kanału RSS na stronie, a potem powiadamiać usługę w tle o wyświetleniu ikony działania na tej stronie.

Komunikacja ta wykorzystuje przekazywanie wiadomości, co pozwala zarówno rozszerzeniom, jak i skryptom treści na nasłuchiwanie wiadomości od siebie nawzajem i na odpowiadanie na tym samym kanale. Wiadomość może zawierać dowolny prawidłowy obiekt JSON (null, boolean, number, string, array lub object). Istnieją 2 interfejsy API przekazujące wiadomości: jeden do żądań jednorazowych i bardziej złożony dla długotrwałych połączeń, które umożliwiają wysyłanie wielu wiadomości. Informacje o wysyłaniu wiadomości między rozszerzeniami znajdziesz w sekcji Wiadomości z rozszerzeniami różnych rozszerzeń.

Jednorazowe żądania

Aby wysłać pojedynczą wiadomość do innej części rozszerzenia i opcjonalnie uzyskać odpowiedź, zadzwoń pod numer runtime.sendMessage() lub tabs.sendMessage(). Te metody umożliwiają wysyłanie jednorazowej wiadomości podlegającej serializacji w formacie JSON ze skryptu treści do rozszerzenia lub z rozszerzenia do skryptu treści. Aby obsłużyć odpowiedź, użyj zwróconego obiecania. Aby zapewnić zgodność wsteczną ze starszymi rozszerzeniami, możesz zamiast tego przekazać wywołanie zwrotne jako ostatni argument. W ramach tej samej rozmowy nie można używać obietnicy i wywołania zwrotnego.

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

Wysyłanie żądania z poziomu skryptu 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);
})();

Jeśli chcesz odpowiedzieć na wiadomość w sposób synchroniczny, po otrzymaniu odpowiedzi wywołaj funkcję sendResponse, a potem false, aby wskazać, że to wszystko. Aby odpowiedzieć asynchronicznie, zwracaj true, aby zachować aktywne wywołanie zwrotne sendResponse do momentu, gdy będziesz gotowy do jego użycia. Funkcje asynchroniczne nie są obsługiwane, ponieważ zwracają obietnicę, która nie jest obsługiwana.

Aby wysłać żądanie do skryptu treści, określ, do którego z tych poniższych kart się odnosi: Ten przykład działa w skryptach service worker, wyskakujących okienkach i stronach chrome-extension:// otwartych jako karty.

(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 otrzymywać te wiadomości, 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 metoda sendResponse() była wywoływana synchronicznie. Aby używać funkcji sendResponse() asynchronicznie, dodaj return true; do modułu obsługi zdarzenia onMessage.

Jeśli wiele stron nasłuchuje zdarzeń onMessage, tylko pierwsza z nich, która wywoła funkcję sendResponse() dla danego zdarzenia, będzie mogła wysłać odpowiedź. Wszystkie inne odpowiedzi na to zdarzenie zostaną zignorowane.

Długotrwałe połączenia

Aby utworzyć długotrwały kanał przekazywania wiadomości, wywołaj funkcję runtime.connect(), aby przekazywać wiadomości ze skryptu treści do strony rozszerzenia, lub funkcję tabs.connect(), aby przekazywać wiadomości ze strony rozszerzenia do skryptu treści. Możesz nazwać kanał, aby odróżnić różne typy połączeń.

Jednym z możliwych zastosowań długotrwałego połączenia jest rozszerzenie do automatycznego wypełniania formularzy. Skrypt treści może otworzyć kanał do strony rozszerzenia dla konkretnego logowania i wysłać do rozszerzenia wiadomość dotyczącą każdego elementu wprowadzania danych na stronie, aby poprosić o dane do wypełnienia formularza. Udostępnione połączenie umożliwia udostępnianie stanu między komponentami rozszerzenia.

Podczas nawiązywania połączenia każdemu jego końcowi przypisywany jest obiekt runtime.Port do wysyłania i odbierania wiadomości za pomocą tego połączenia.

Aby otworzyć kanał z poziomu skryptu treści oraz wysyłać i odsłuchiwać wiadomości, użyj tego kodu:

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 treści, w poprzednim przykładzie zastąp wywołanie funkcji runtime.connect() kodem tabs.connect().

Aby obsługiwać przychodzące połączenia w przypadku skryptu treści lub strony rozszerzenia, skonfiguruj odbiornik zdarzeń runtime.onConnect. Gdy inna część rozszerzenia wywołuje funkcję connect(), powoduje to aktywację tego zdarzenia i obiektu 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 są zaprojektowane jako dwukierunkowa metoda komunikacji 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 funkcję tabs.connect(), runtime.connect() lub runtime.connectNative(), tworzy port, który może natychmiast wysyłać wiadomości za pomocą postMessage().

Jeśli na karcie jest kilka klatek, wywołanie tabs.connect() powoduje wywołanie zdarzenia runtime.onConnect po jednej na każdą klatka na karcie. Podobnie, jeśli wywoływane jest zdarzenie runtime.connect(), zdarzenie onConnect może zostać uruchomione raz na każdą klatkę w procesie rozszerzenia.

Możesz chcieć wiedzieć, kiedy połączenie jest zamknięte, np. jeśli chcesz zachować osobne stany dla każdego otwartego portu. Aby to zrobić, nasłuchuj zdarzenia runtime.Port.onDisconnect. To zdarzenie jest wywoływane, gdy po drugiej stronie kanału nie ma prawidłowych portów. Może to być spowodowane przez:

  • Na drugim końcu nie ma żadnych odbiorców runtime.onConnect.
  • Karta zawierająca port jest wczytywana (na przykład, gdy użytkownik przewija kartę).
  • Ramka, z którą wywołano element connect(), została wyładowana z pamięci.
  • Wszystkie ramki, które otrzymały port (przez runtime.onConnect), zostały wczytane.
  • runtime.Port.disconnect() jest wywoływany przez drugą stronę. Jeśli wywołanie connect() powoduje wywołanie kilku portów po stronie odbiorcy i wywołanie disconnect() na dowolnym z tych portów, zdarzenie onDisconnect uruchamia się tylko w porcie wysyłającym, a nie w pozostałych portach.

Wiadomości między rozszerzeniami

Oprócz wysyłania wiadomości między różnymi komponentami w rozszerzeniu możesz też używać interfejsu API wiadomości do komunikacji z innymi rozszerzeniami. Dzięki temu możesz udostępnić publiczny interfejs API do użytku innych rozszerzeń.

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

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, prześlij 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ć i odpowiadać na wiadomości z innych stron internetowych, ale nie mogą wysyłać wiadomości do stron internetowych. Aby wysyłać wiadomości ze strony internetowej do rozszerzenia, w manifest.json określ, z którymi witrynami chcesz się komunikować za pomocą klucza manifestu "externally_connectable". Na przykład:

manifest.json

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

Udostępni to interfejs do przesyłania wiadomości na każdej stronie, która pasuje do podanych wzorców adresów URL. Wzór adresu URL musi zawierać co najmniej domenę drugiego poziomu. Nie są obsługiwane wzorce nazw hosta takie jak „*”, „*.com”, „*.co.uk” i „*.appspot.com”. Począwszy od Chrome 107 możesz używać opcji <all_urls>, aby uzyskać dostęp do wszystkich domen. Ponieważ dotyczy ona wszystkich hostów, oceny rozszerzeń w Chrome Web Store, które z niej korzystają, mogą być dłuższe.

Aby wysłać wiadomość do konkretnej aplikacji lub konkretnego rozszerzenia, użyj interfejsów API runtime.sendMessage() lub runtime.connect(). 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);
  });

Z poziomu rozszerzenia słuchaj wiadomości ze stron internetowych za pomocą interfejsów API runtime.onMessageExternal lub runtime.onConnectExternal, tak jak w przypadku przesyłania wiadomości między 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);
  });

Natywne aplikacje do obsługi wiadomości

Rozszerzenia mogą wymieniać wiadomości z natywnymi aplikacjami zarejestrowanymi jako host wiadomości natywnych. Więcej informacji o tej funkcji znajdziesz w artykule Komunikaty natywne.

Zagadnienia związane z bezpieczeństwem

Oto kilka kwestii związanych z bezpieczeństwem wiadomości.

Skrypty dotyczące zawartości są mniej wiarygodne

Skrypty treści są mniej godne zaufania niż skrypt usługi rozszerzenia. Na przykład złośliwa strona internetowa może być w stanie zakłócić proces renderowania, który uruchamia skrypty treści. Zakładaj, że wiadomości z skryptu treści mogły zostać spreparowane przez osobę przeprowadzającą atak, i sprawdzaj oraz sterylizuj wszystkie dane wejściowe. Załóż, że wszelkie dane wysyłane do skryptu treści mogą wyciec na stronę internetową. Ogranicz zakres działań uprzywilejowanych, które mogą być wywoływane przez wiadomości otrzymane z skryptów treści.

Cross-site scripting

Zadbaj o ochronę swoich skryptów przed skryptami między witrynami. Gdy otrzymujesz dane z niezaufanego źródła, np. danych wejściowych użytkownika lub innych witryn przy użyciu skryptu treści lub interfejsu API, unikaj interpretowania tych danych jako kodu HTML i używania ich w sposób, który mógłby umożliwić uruchomienie nieoczekiwanego kodu.

Bezpieczniejsze metody

W miarę możliwości 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 stosowania tych metod, które mogą narazić Twoje rozszerzenie na ryzyko:

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