Przekazywanie wiadomości

Skrypty dotyczące zawartości działają w kontekście strony internetowej, a nie rozszerzenia, które je uruchamia, dlatego często potrzebują sposobów na komunikowanie się 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 (pusty, logiczny, liczbowy, ciąg znaków, tablica lub obiekt). Dostępne są 2 interfejsy API do przesyłania wiadomości: jeden do jednorazowych żądań i bardziej złożony do 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 między rozszerzeniami.

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 jednorazowych wiadomości w formacie JSON z 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. Nie możesz używać obietnicy i połączenia zwrotnego w tym samym wywołaniu.

Gdy wysyłasz wiadomość, detektorowi zdarzeń, który ją obsługuje, przekazywany jest opcjonalny trzeci argument sendResponse. Jest to funkcja, która przyjmuje obiekt możliwy do serializacji w formacie JSON. Jest on używany jako wartość zwracana przez funkcję, która wysłała wiadomość. Domyślnie wywołanie zwrotne sendResponse musi być wywoływane synchronicznie. Jeśli chcesz wykonać asynchroniczne działanie w celu uzyskania wartości przekazanej do sendResponse, musisz zwrócić literał true (a nie tylko wartość logiczną) z odbioru zdarzeń. W ten sposób kanał wiadomości pozostanie otwarty do momentu wywołania funkcji sendResponse.

// Event listener
function handleMessages(message, sender, sendResponse) {

  fetch(message.url)
    .then((response) => sendResponse({statusCode: response.status}))

  // Since `fetch` is asynchronous, must send an explicit `true`
  return true;
}

// Message sender
  const {statusCode} = await chrome.runtime.sendMessage({
    url: 'https://example.com'
  });

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. Te elementy używają tego samego kodu w rozszerzeniach i 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 funkcję return true; do modułu obsługi zdarzenia onMessage.

Jeśli wiele stron nasłuchuje zdarzeń onMessage, tylko pierwsza strona, 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, które można wielokrotnie używać, wywołaj funkcję runtime.connect(), aby przekazywać wiadomości ze skryptu treści na stronę 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 potencjalnych zastosowań długotrwałego połączenia jest automatyczne wypełnianie formularzy przez rozszerzenie. 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 detektor zdarzeń runtime.onConnect. Gdy inna część rozszerzenia wywołuje funkcję connect(), powoduje to aktywację tego zdarzenia i obiektu runtime.Port. Kod odpowiadający na przychodzące połączenia 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 trwania 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 używać 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 być wywoływane raz na każdy kadr w procesie rozszerzania.

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 zdarzenie 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 wyładowana (na przykład, gdy użytkownik przejdzie na inną kartę).
  • Ramka, w której wywołano funkcję connect(), została wyładowana.
  • Wszystkie ramki, które otrzymały port (przez runtime.onConnect), zostały usunięte.
  • runtime.Port.disconnect() jest wywoływany przez drugą stronę. Jeśli wywołanie connect() powoduje wywołanie wielu portów po stronie odbiorcy, a wywołanie disconnect() jest wywoływane na dowolnym z tych portów, zdarzenie onDisconnect jest wywoływane tylko na porcie wysyłania, a nie na innych 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 do obsługi wiadomości, aby komunikować się 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ład 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, 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 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:
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/*"]
}

Dzięki temu interfejs Messaging API będzie dostępny na każdej stronie, która pasuje do podanych przez Ciebie wzoró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ć <all_urls> do uzyskiwania dostępu 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.
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);
    }
  );
}

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ć się wiadomościami z natywnymi aplikacjami zarejestrowanymi jako natywczy host wiadomości. Więcej informacji o tej funkcji znajdziesz w artykule Natywna obsługa wiadomości.

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 wiarygodne niż worker rozszerzenia. Na przykład złośliwa strona internetowa może 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. Zakładamy, że wszystkie 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

Zabezpiecz skrypty przed atakami typu cross-site scripting. Podczas otrzymywania danych z niezaufanego źródła, np. z danych wprowadzanych przez użytkownika, z innych witryn za pomocą skryptu treści lub interfejsu API, należy unikać interpretowania tych danych jako kodu HTML lub 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 poniższych metod, które powodują podatność na zagrożenia:

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