メッセージ受け渡し

メッセージング API を使用すると、拡張機能に関連付けられたコンテキストで実行されているさまざまなスクリプト間で通信できます。これには、サービス ワーカー、chrome-extension://ページ、コンテンツ スクリプト間の通信が含まれます。たとえば、RSS リーダー拡張機能は、コンテンツ スクリプトを使用してページに RSS フィードがあるかどうかを検出し、サービス ワーカーに通知してそのページのアクション アイコンを更新します。

メッセージ パッシング API は 2 つあります。1 つは単発のリクエスト用、もう 1 つは複数のメッセージを送信できる長期間の接続用です。

拡張機能間でメッセージを送信する方法については、拡張機能間のメッセージのセクションをご覧ください。

1 回限りのリクエスト

拡張機能の別の部分に単一のメッセージを送信し、必要に応じてレスポンスを取得するには、runtime.sendMessage() または tabs.sendMessage() を呼び出します。これらのメソッドを使用すると、コンテンツ スクリプトから拡張機能へ、または拡張機能からコンテンツ スクリプトへ、1 回限りの JSON シリアル化可能なメッセージを送信できます。どちらの API も、受信者から提供されたレスポンスに解決される Promise を返します。

コンテンツ スクリプトからリクエストを送信するコードは次のようになります。

content-script.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 関数が 3 番目のパラメータとして渡されます。これは、レスポンスを提供するために呼び出すことができる関数です。デフォルトでは、sendResponse コールバックは同期的に呼び出す必要があります。

パラメータなしで sendResponse を呼び出すと、null がレスポンスとして送信されます。

非同期でレスポンスを送信するには、true を返すか、Promise を返すかの 2 つの方法があります。

true を返す

sendResponse() を使用して非同期で応答するには、イベント リスナーからリテラル true(真値だけでなく)を返します。これにより、sendResponse が呼び出されるまでメッセージ チャネルが相手側に開いたままになり、後で呼び出すことができます。

Promise を返す

Chrome 144 以降では、メッセージ リスナーから Promise を返して非同期で応答できます。Promise が解決されると、その解決値がレスポンスとして送信されます。

Promise が拒否されると、送信者の sendMessage() 呼び出しはエラー メッセージとともに拒否されます。詳細と例については、エラー処理のセクションをご覧ください。

解決または拒否される可能性のある Promise を返す例:

// 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 として宣言して、Promise を返すこともできます。

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 を返す: async 関数の落とし穴

リスナーとしての async 関数は、return ステートメントがなくても、常に Promise を返すことに注意してください。async リスナーが値を返さない場合、その Promise は暗黙的に 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 リスナーがエラーをスローした場合(同期的に、または拒否される Promise を返すことで非同期的に)、送信側の sendMessage() によって返される Promise はエラー メッセージで拒否されます。リスナーが、結果の TypeError をキャッチせずに JSON シリアル化できないレスポンスを返そうとした場合にも、このエラーが発生する可能性があります。

リスナーがリジェクトされる Promise を返す場合、送信者がエラー メッセージを受信できるように、Error インスタンスでリジェクトする必要があります。プロミスが他の値(nullundefined など)で拒否された場合、sendMessage() は代わりに一般的なエラー メッセージで拒否されます。

onMessage に複数のリスナーが登録されている場合、送信者に影響するのは、最初にレスポンス、拒否、エラーのスローを行ったリスナーのみです。他のリスナーは実行されますが、その結果は無視されます。

リスナーが拒否される Promise を返した場合、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() Promise が拒否されます。

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

name キーを含むオプション パラメータを渡して、さまざまな種類の接続を区別することで、チャンネルに名前を付けることができます。

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

長時間接続のユースケースの 1 つとして、フォームの自動入力拡張機能があります。コンテンツ スクリプトは、特定のログイン用の拡張機能ページへのチャネルを開き、ページ上の各入力要素の拡張機能にメッセージを送信して、入力するフォームデータをリクエストする場合があります。共有接続により、拡張機能は拡張機能コンポーネント間で状態を共有できます。

接続を確立するときに、各エンドに runtime.Port オブジェクトが割り当てられ、その接続を介してメッセージの送受信が行われます。

次のコードを使用して、コンテンツ スクリプトからチャネルを開き、メッセージを送信してリッスンします。

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

拡張機能からコンテンツ スクリプトにリクエストを送信するには、前の例の 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() 値を含めることができます。他の値はシリアル化可能な値に強制変換されます(特に undefinednull としてシリアル化されます)。

メッセージ サイズの上限

メッセージの最大サイズは 64 MiB です。

ポートの有効期間

ポートは、拡張機能の異なる部分間の双方向通信メカニズムとして設計されています。拡張機能の一部が tabs.connect()runtime.connect()、または runtime.connectNative() を呼び出すと、postMessage() を使用してすぐにメッセージを送信できる Port が作成されます。

タブに複数のフレームがある場合、tabs.connect() を呼び出すと、タブ内のフレームごとに runtime.onConnect イベントが 1 回呼び出されます。同様に、runtime.connect() が呼び出されると、拡張機能プロセス内のフレームごとに 1 回 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.
  });
});

別の拡張機能にメッセージを送信するには、次のように通信する拡張機能の ID を渡します。

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

ウェブページからメッセージを送信する

拡張機能は、ウェブページからのメッセージを受信して応答することもできます。ウェブページから拡張機能にメッセージを送信するには、"externally_connectable" マニフェスト キーを使用して、メッセージを許可するウェブサイトを manifest.json で指定します。次に例を示します。

manifest.json

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

これにより、指定した URL パターンに一致するすべてのページでメッセージング API が公開されます。URL パターンには、少なくともセカンドレベル ドメインを含める必要があります。つまり、「*」、「*.com」、「*.co.uk」、「*.appspot.com」などのホスト名パターンはサポートされていません。<all_urls> を使用して、すべてのドメインにアクセスできます。

runtime.sendMessage() API または runtime.connect() API を使用して、特定の拡張機能にメッセージを送信します。次に例を示します。

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

拡張機能から、拡張機能間のメッセージングと同様に、runtime.onMessageExternal または runtime.onConnectExternal API を使用してウェブページからのメッセージをリッスンします。次の例をご覧ください。

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