Nachricht wurde weitergegeben

Mit Messaging-APIs können Sie zwischen verschiedenen Skripts kommunizieren, die in Kontexten ausgeführt werden, die mit Ihrer Erweiterung verknüpft sind. Dazu gehört die Kommunikation zwischen Ihrem Service Worker, chrome-extension://pages und Content-Scripts. Eine RSS-Reader-Erweiterung kann beispielsweise Content-Scripts verwenden, um das Vorhandensein eines RSS-Feeds auf einer Seite zu erkennen. Anschließend wird der Service Worker benachrichtigt, das Aktionssymbol für diese Seite zu aktualisieren.

Es gibt zwei APIs für die Nachrichtenübermittlung: eine für einmalige Anfragen und eine komplexere für langlebige Verbindungen, über die mehrere Nachrichten gesendet werden können.

Informationen zum Senden von Nachrichten zwischen Erweiterungen finden Sie im Abschnitt Nachrichten zwischen Erweiterungen.

Einmalige Anfragen

Wenn Sie eine einzelne Nachricht an einen anderen Teil Ihrer Erweiterung senden und optional eine Antwort erhalten möchten, rufen Sie runtime.sendMessage() oder tabs.sendMessage() auf. Mit diesen Methoden können Sie eine einmalige JSON-serialisierbare Nachricht von einem Content-Script an die Erweiterung oder von der Erweiterung an ein Content-Script senden. Beide APIs geben ein Promise zurück, das in die Antwort eines Empfängers aufgelöst wird.

So sieht das Senden einer Anfrage über ein Content-Script aus:

content-script.js:

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

Antworten

Verwenden Sie das chrome.runtime.onMessage-Ereignis, um auf eine Nachricht zu warten:

// 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');

Wenn der Event-Listener aufgerufen wird, wird eine sendResponse-Funktion als dritter Parameter übergeben. Dies ist eine Funktion, die aufgerufen werden kann, um eine Antwort zu geben. Standardmäßig muss der sendResponse-Callback synchron aufgerufen werden.

Wenn Sie sendResponse ohne Parameter aufrufen, wird null als Antwort gesendet.

Wenn Sie eine asynchrone Antwort senden möchten, haben Sie zwei Möglichkeiten: Sie können true zurückgeben oder ein Promise.

true zurückgeben

Wenn Sie asynchron mit sendResponse() antworten möchten, geben Sie aus dem Event-Listener ein Literal true zurück (nicht nur einen „truthy“-Wert). Dadurch bleibt der Nachrichtenkanal für das andere Ende geöffnet, bis sendResponse aufgerufen wird. Sie können ihn also später aufrufen.

Promise zurückgeben

Ab Chrome 144 können Sie ein Promise von einem Nachrichtenlistener zurückgeben, um asynchron zu antworten. Wenn das Promise aufgelöst wird, wird sein aufgelöster Wert als Antwort gesendet.

Wenn das Versprechen abgelehnt wird, wird der sendMessage()-Aufruf des Absenders mit der Fehlermeldung abgelehnt. Weitere Informationen und Beispiele finden Sie im Abschnitt Fehlerbehebung.

Ein Beispiel für die Rückgabe eines Promise, das aufgelöst oder abgelehnt werden kann:

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

Sie können auch einen Listener als async deklarieren, um ein Promise zurückzugeben:

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

Promise zurückgeben: async-Funktionsfallen

Beachten Sie, dass eine async-Funktion als Listener immer ein Promise zurückgibt, auch ohne return-Anweisung. Wenn ein async-Listener keinen Wert zurückgibt, wird sein Promise implizit in undefined aufgelöst und null wird als Antwort an den Absender gesendet. Das kann zu unerwartetem Verhalten führen, wenn mehrere Listener vorhanden sind:

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

Fehlerbehandlung

Ab Chrome 144 wird das vom Absender zurückgegebene Promise, wenn ein onMessage-Listener einen Fehler auslöst (entweder synchron oder asynchron durch Zurückgeben eines Promise, das abgelehnt wird), mit der Fehlermeldung abgelehnt.sendMessage() Das kann auch passieren, wenn ein Listener versucht, eine Antwort zurückzugeben, die nicht JSON-serialisiert werden kann, ohne die resultierende TypeError abzufangen.

Wenn ein Listener ein Promise zurückgibt, das abgelehnt wird, muss es mit einer Error-Instanz abgelehnt werden, damit der Absender diese Fehlermeldung erhält. Wenn das Promise mit einem anderen Wert (z. B. null oder undefined) abgelehnt wird, wird sendMessage() stattdessen mit einer generischen Fehlermeldung abgelehnt.

Wenn mehrere Listener für onMessage registriert sind, wirkt sich nur der erste Listener, der antwortet, ablehnt oder einen Fehler ausgibt, auf den Absender aus. Alle anderen Listener werden ausgeführt, ihre Ergebnisse werden jedoch ignoriert.

Beispiele

Wenn ein Listener ein Promise zurückgibt, das abgelehnt wird, wird sendMessage() abgelehnt:

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

Wenn ein Listener mit einem Wert antwortet, der nicht serialisiert werden kann, wird sendMessage() abgelehnt:

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

Wenn ein Listener synchron einen Fehler auslöst, bevor ein anderer Listener antwortet, wird das sendMessage()-Promise des Listeners abgelehnt:

// 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!');
});

Wenn jedoch ein Listener antwortet, bevor ein anderer einen Fehler auslöst, ist sendMessage() erfolgreich:

// 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!');
});

Langlebige Verbindungen

Rufen Sie Folgendes auf, um einen wiederverwendbaren, langlebigen Channel für die Nachrichtenübergabe zu erstellen:

  • runtime.connect(), um Nachrichten von einem Content-Script an eine Erweiterungsseite zu übergeben
  • tabs.connect(), um Nachrichten von einer Erweiterungsseite an ein Content-Script zu übergeben.

Sie können Ihrem Channel einen Namen geben, indem Sie einen Optionsparameter mit einem name-Schlüssel übergeben, um zwischen verschiedenen Verbindungstypen zu unterscheiden:

const port = chrome.runtime.connect({name: "example"});

Ein möglicher Anwendungsfall für eine langlebige Verbindung ist eine Erweiterung zum automatischen Ausfüllen von Formularen. Das Content-Script öffnet möglicherweise einen Channel zur Erweiterungsseite für eine bestimmte Anmeldung und sendet für jedes Eingabeelement auf der Seite eine Nachricht an die Erweiterung, um die auszufüllenden Formulardaten anzufordern. Über die freigegebene Verbindung kann die Erweiterung den Status zwischen Erweiterungskomponenten freigeben.

Beim Herstellen einer Verbindung wird jedem Ende ein runtime.Port-Objekt zum Senden und Empfangen von Nachrichten über diese Verbindung zugewiesen.

Verwenden Sie den folgenden Code, um einen Kanal über ein Content-Script zu öffnen und Nachrichten zu senden und zu empfangen:

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

Wenn Sie eine Anfrage von der Erweiterung an ein Content-Script senden möchten, ersetzen Sie den Aufruf von runtime.connect() im vorherigen Beispiel durch tabs.connect().

Um eingehende Verbindungen für ein Content-Script oder eine Erweiterungsseite zu verarbeiten, richten Sie einen runtime.onConnect-Ereignis-Listener ein. Wenn ein anderer Teil Ihrer Erweiterung connect() aufruft, wird dieses Ereignis und das runtime.Port-Objekt aktiviert. Der Code für die Reaktion auf eingehende Verbindungen sieht so aus:

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

Serialisierung

In Chrome verwenden die APIs für die Nachrichtenübermittlung die JSON-Serialisierung. Das unterscheidet sich von anderen Browsern, die dieselben APIs mit dem Structured Clone-Algorithmus implementieren.

Das bedeutet, dass eine Nachricht (und die von Empfängern bereitgestellten Antworten) einen beliebigen gültigen JSON.stringify()-Wert enthalten kann. Andere Werte werden in serialisierbare Werte umgewandelt (insbesondere wird undefined als null serialisiert).

Größenbeschränkungen für Nachrichten

Die maximale Größe einer Nachricht beträgt 64 MiB.

Port-Lebensdauer

Ports sind als bidirektionaler Kommunikationsmechanismus zwischen verschiedenen Teilen einer Erweiterung konzipiert. Wenn ein Teil einer Erweiterung tabs.connect(), runtime.connect() oder runtime.connectNative() aufruft, wird ein Port erstellt, über den sofort Nachrichten mit postMessage() gesendet werden können.

Wenn es mehrere Frames auf einem Tab gibt, wird beim Aufrufen von tabs.connect() das Ereignis runtime.onConnect einmal für jeden Frame auf dem Tab aufgerufen. Wenn runtime.connect() aufgerufen wird, kann das Ereignis onConnect einmal für jeden Frame im Erweiterungsprozess ausgelöst werden.

Möglicherweise möchten Sie herausfinden, wann eine Verbindung geschlossen wird, z. B. wenn Sie für jeden geöffneten Port separate Status beibehalten. Dazu müssen Sie auf das Ereignis runtime.Port.onDisconnect warten. Dieses Ereignis wird ausgelöst, wenn am anderen Ende des Channels keine gültigen Ports vorhanden sind. Das kann folgende Ursachen haben:

  • Am anderen Ende gibt es keine Listener für runtime.onConnect.
  • Der Tab mit dem Port wird entladen, z. B. wenn auf dem Tab navigiert wird.
  • Der Frame, in dem connect() aufgerufen wurde, wurde entladen.
  • Alle Frames, die den Port (über runtime.onConnect) erhalten haben, wurden entladen.
  • runtime.Port.disconnect() wird von der anderen Seite aufgerufen. Wenn ein connect()-Anruf auf Empfängerseite zu mehreren Ports führt und disconnect() an einem dieser Ports aufgerufen wird, wird das onDisconnect-Ereignis nur am Senderport ausgelöst, nicht an den anderen Ports.

Nachrichtenübermittlung zwischen Erweiterungen

Neben dem Senden von Nachrichten zwischen verschiedenen Komponenten in Ihrer Erweiterung können Sie die Messaging API verwenden, um mit anderen Erweiterungen zu kommunizieren. So können Sie eine öffentliche API für andere Erweiterungen bereitstellen.

Verwenden Sie die Methoden runtime.onMessageExternal oder runtime.onConnectExternal, um auf eingehende Anfragen und Verbindungen von anderen Erweiterungen zu warten. Hier ein Beispiel für jede Option:

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

Wenn Sie eine Nachricht an eine andere Erweiterung senden möchten, übergeben Sie die ID der Erweiterung, mit der Sie kommunizieren möchten, so:

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

Nachrichten von Webseiten senden

Erweiterungen können auch Nachrichten von Webseiten empfangen und darauf reagieren. Wenn Sie Nachrichten von einer Webseite an eine Erweiterung senden möchten, geben Sie in Ihrem manifest.json mit dem Manifestschlüssel "externally_connectable" an, von welchen Websites Nachrichten zulässig sein sollen. Beispiel:

manifest.json

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

Dadurch wird die Messaging API für alle Seiten verfügbar gemacht, die den von Ihnen angegebenen URL-Mustern entsprechen. Das URL-Muster muss mindestens eine Second-Level-Domain enthalten. Hostnamenmuster wie „*“, „*.com“, „*.co.uk“ und „*.appspot.com“ werden nicht unterstützt. Mit <all_urls> können Sie auf alle Domains zugreifen.

Verwenden Sie die APIs runtime.sendMessage() oder runtime.connect(), um eine Nachricht an eine bestimmte Erweiterung zu senden. Beispiel:

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

Hören Sie in Ihrer Erweiterung auf Nachrichten von Webseiten, indem Sie die APIs runtime.onMessageExternal oder runtime.onConnectExternal wie beim Nachrichtenaustausch zwischen Erweiterungen verwenden. Beispiel:

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

Es ist nicht möglich, eine Nachricht von einer Erweiterung an eine Webseite zu senden.

Natives Messaging

Erweiterungen können Nachrichten mit nativen Anwendungen austauschen, die als Host für native Nachrichten registriert sind. Weitere Informationen zu dieser Funktion finden Sie unter Native Messaging.

Sicherheitsaspekte

Hier sind einige Sicherheitsaspekte im Zusammenhang mit Messaging.

Inhaltskripte sind weniger vertrauenswürdig

Content-Scripts sind weniger vertrauenswürdig als der Service Worker der Erweiterung. Beispielsweise kann eine schädliche Webseite den Rendering-Prozess beeinträchtigen, in dem die Inhaltsskripts ausgeführt werden. Gehen Sie davon aus, dass Nachrichten von einem Content-Script von einem Angreifer erstellt wurden, und validieren und bereinigen Sie alle Eingaben. Gehen Sie davon aus, dass alle Daten, die an das Inhaltsscript gesendet werden, an die Webseite weitergegeben werden könnten. Beschränken Sie den Umfang privilegierter Aktionen, die durch Nachrichten ausgelöst werden können, die von Inhaltsskripts empfangen werden.

Cross-Site-Scripting

Schützen Sie Ihre Skripts vor Cross-Site-Scripting. Wenn Sie Daten aus einer nicht vertrauenswürdigen Quelle wie Nutzereingaben, anderen Websites über ein Inhaltsskript oder einer API empfangen, sollten Sie darauf achten, dass diese nicht als HTML interpretiert oder auf eine Weise verwendet werden, die die Ausführung unerwarteten Codes ermöglicht.

Sicherere Methoden

Verwenden Sie nach Möglichkeit APIs, mit denen keine Skripts ausgeführt werden:

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

Vermeiden Sie die Verwendung der folgenden Methoden, die Ihre Erweiterung anfällig machen:

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