0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Chrome拡張機能 × 既存API連携の設計パターン

Last updated at Posted at 2025-05-06

Chrome拡張機能のAPI

image.png

Chrome拡張機能を開発する中で、既存のAPIサーバーと連携して何か処理を行いたい場面はよくあります。

たとえば:

  • ログイン情報を使ってデータを取得したい
  • ユーザーの操作に応じて商品情報を取得・保存したい
  • セッション管理やCookieに依存するAPIを扱いたい

ところが、Chrome拡張機能には通常のWebアプリとは違う実行環境の壁があり、安易に fetch() を使ったり、状態を共有しようとすると、さまざまな問題に直面します。

本記事では、そうした設計上の落とし穴と、その回避方法を具体例を交えて整理します。

前提:Chrome拡張機能の3つの構成要素

image.png

名称 役割 実行環境
Background script API通信、永続処理、イベント待機 拡張機能の独立プロセス
Content script ページへのDOMアクセス・操作 任意のWebページに埋め込まれる
Popup 拡張ボタン押下時のUI 別ウィンドウとして実行

この3つのプロセスは完全に分離されており、直接関数呼び出しはできません。
例えば、ContentScript(フロント)からBackground(バック)の関数を直接実行したり、参照したりすることは出来ません。

相互通信には「メッセージイベント」を用いる必要があります。

落とし穴①:API呼び出しは background で!

普段の感覚でやってしまいがちなReactやVueからのFetchですが、推奨されていません。

結論から言えば

fetch(や axios)などのAPI通信は、必ず background script で行うべきです。

// content script 内で直接 fetch
fetch('https://my-api.example.com/data', {
  credentials: 'include', // ← つけても意味なし
});

これは設計上の“推奨”ではなく、実際の動作やセキュリティに影響を与える「制約」による必須ルールです。
これが推奨されない理由は:

  • content script はWebページと同じオリジン扱い
  • そのため、拡張機能のセッションやCookieが引き継がれない
  • CORS制限でブロックされることも多い

正しい設計

API通信は background script 側で行うべきです。

図のように、まずContentScriptからは「メッセージイベント」を送り、BackGrondのイベントリスナーに検知させます。
そしてBackGrondからApiサーバーにFetchするという仕組みが適切です。

output (2).png

// background.ts
chrome.runtime.onMessage.addListener((message, _, sendResponse) => {
  if (message.type === 'FETCH_DOG_NAME') {
    fetch('https://api.example.com/dog', {
      method: 'POST',
      body: JSON.stringify(message.payload),
      credentials: 'include',
    })
      .then((res) => res.json())
      .then((data) => {
        sendResponse({ success: true, name: data.name });
      });
    return true; // 非同期応答を明示
  }
});

なぜ content script や popup ではダメなのか?

① クロスオリジン制約(CORS)に引っかかる

Chrome拡張機能は、Webページとは別オリジンの仕組みですが、content script は対象のWebページのコンテキストで動作します。

例えば、Amazonならhttps://https://www.amazon.co.jp/
楽天ならhttps://www.rakuten.co.jp/
であります。

content script 内で fetch を行うと,
これらが「CORSポリシー違反」でブロックされる可能性が高いです。
(一つ一つ許可すれば可能なのですが、都度対応が必要なのと、後述するセッション問題も発生します)

② セッションやCookieが使えない

たとえばログイン済みセッションに依存するAPIでは、cookieやAuthorizationヘッダを使って認証を行うことが多いですよね。

しかし、content scriptやpopupから直接fetchを使うと、

  • Chrome拡張のオリジンと異なるため Cookie が送られない
  • credentials: 'include' を指定しても効かない場合がある
  • サードパーティ扱いになるため SameSite=Lax/Strict のCookieがブロックされる

つまり、ログイン状態が維持されず、APIにアクセスできないという問題が起きます

なぜ background script ならうまくいくのか?

一言で言えば background script は

chrome-extension://<拡張ID>

という独立したオリジン(ドメイン)で動作するため、APIサーバー側でのアクセス制御がしやすく、セッションやCookieも正しく扱えるからです

background script の実行環境は特別

通常の Web ページや content script は、実行先のページと同じオリジンで動作しますが、background script は拡張機能専用のオリジン(chrome-extension://~)で実行されます。

このため、APIサーバー側では次のような対応が可能です:

  • 拡張機能のオリジンだけをホワイトリスト登録できる
  • Origin: chrome-extension://<固定ID> を見て許可/拒否できる
  • セッションCookie(SameSite=Lax/Strict など)も正常に送受信できる

manifest.json で必要な権限を明示できる

拡張機能の manifest.json に permissions として対象APIのURLを指定することで、そのドメインに対する通信を許可することができます。

{
  "permissions": [
    "https://api.example.com/"
  ]
}

サーバーと拡張機能の信頼関係を築きやすい

拡張IDは manifest.json に "key" を指定することで固定化できます。

サーバー側でこの ID を使ってアクセスを制限することで、安全に「この拡張機能だけにアクセスを許可する」ことが可能になります。

Google側でも推奨されています。

落とし穴②: Content Script から Background への通信

前提:なぜ通信が必要なのか?

Content Script は「Webページに埋め込まれて実行される」スクリプトです。
DOM操作やページ上の値の取得には適しているものの、先述したように直接APIを呼び出すことはできません。

一方、Background Script は API通信やセッション維持に適している。

✅ つまり、Content Script は「やりたいこと」を Background に“依頼”し、結果を受け取る必要があります。

Content script から background にアクセスするには、以下のように chrome.runtime.sendMessage() を使います。

// Content Script 側
chrome.runtime.sendMessage(
  {
    type: 'FETCH_DOG_NAME',
    payload: { dogId: 123 },
  },
  (response) => {
    console.log('犬の名前:', response.name);
  },
);

// Background Script 側
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
  if (message.type === 'FETCH_DOG_NAME') {
    // ここで fetch を実行し、sendResponse() で返す
    sendResponse({ name: 'ポチ' });
    return true; // 非同期レスポンスを許可
  }
});

問題点:コールバック地獄と非同期処理の複雑化

chrome.runtime.sendMessage() は非同期ですが、Promiseを返さず、コールバック形式です。

複数のAPIやメッセージを連続して扱おうとすると、以下のようにネストが深くなっていきます

例えば、Dogの名前とIDを取得して、それを元にさらなるリクエストをするAPIがあったとします。

そうすると、Dogの名前とIDを取得後の処理は、コールバック関数に記載する仕組みですので、どんどんネストが深くなっていきます。

chrome.runtime.sendMessage({ type: 'FETCH_A' }, (resA) => {
  chrome.runtime.sendMessage({ type: 'FETCH_B', payload: resA }, (resB) => {
    chrome.runtime.sendMessage({ type: 'FETCH_C', payload: resB }, (resC) => {
      // 🙃 どんどん深くなる
    });
  });
});

これはいわゆる「コールバック地獄」です。
処理の流れが追いにくく、エラーハンドリングも煩雑になります。

解決策:Promiseでラップして await 可能にする!

そこで、以下のような共通関数を用意し、chrome.runtime.sendMessage() を Promise 化します。

// sendMessage.ts
export const sendMessage = <T, R>(payload: T): Promise<R> => {
  return new Promise((resolve) => {
    chrome.runtime.sendMessage<T, R>(payload, (response) => {
      resolve(response);
    });
  });
};

何が嬉しいの?

  • async/await で直列処理が書ける
  • 処理の流れが 上から下へ素直に追える
  • try/catch でのエラーハンドリングも可能に

各メッセージごとにラッパー関数を作る

共通化した sendMessage に対し、APIごとのメッセージを呼び出すラッパーを作ります。

🐶 例:犬の名前を取得するメッセージ

// sendMessageGetDogName.ts
import type { FetchGetDogNameMessage, FetchGetDogNameMessageResponse } from './types';
import { sendMessage } from './sendMessage';

export const sendMessageGetDogName = (
  payload: FetchGetDogNameMessage['payload'],
): Promise<FetchGetDogNameMessageResponse> => {
  return sendMessage<FetchGetDogNameMessage, FetchGetDogNameMessageResponse>({
    type: 'FETCH_GET_DOG_NAME_MESSAGE',
    payload,
  });
};

呼び出し元ではこう書くことができます。

const handleClick = async () => {
  const result = await sendMessageGetDogName({ dogId: 123 });
  console.log('犬の名前:', result.name);
};

この設計のポイント

工夫 説明
共通関数でラップ chrome.runtime.sendMessage() を Promise 化し、非同期処理を扱いやすくする
ラッパー関数を分離 各メッセージごとに処理を分けることで責務が明確になり、import 管理もしやすくなる
async/await 化 ネストせず、処理の流れを直線的に書けるため、可読性と保守性が向上
型引数で安全性も確保 ジェネリクス T, R を使うことで、送信データ・受信データ両方に型安全性と補完が効くようになる

ディレクトリ構成

packages/
└── shared/
    └── lib/
        └── runtimeMessageClient/
            ├── sendMessage.ts
            ├── dog/
            │   └── getName.ts
            └── purchase/
                └── getJanCode.ts

このように「共通処理は共通で」「APIごとに整理する」ことで、大規模になっても保守しやすい設計が実現できます。

まとめ

課題 解決策
クロスオリジンで fetch できない background で API 通信を行う
content script から background にアクセスできない chrome.runtime.sendMessage() を使う
コールバック地獄で見通しが悪い Promise化して共通関数で管理

おわりに

Chrome拡張機能は、実行環境が複数あり、通信の流れが直感的に見えにくいため、適切な設計が非常に重要です。

非同期通信はPromiseでラップし、責務を整理した構成にすることで、
保守性・見通しの良さ・再利用性が大きく向上します。

同じように苦戦している方の助けになれば幸いです!

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?