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

## Chrome拡張機能のAPI

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3621281/9666c1ae-7822-40d6-b1bc-22983069c485.png)


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

たとえば：

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

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

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

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3621281/2b5e5036-4583-41b8-a8a3-97f8f122beca.png)


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

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

相互通信には「メッセージイベント」を用いる必要があります。
## 落とし穴①：API呼び出しは background で！

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


結論から言えば

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


```ts
// 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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3621281/58083a6e-3556-41e9-833b-4df2d9836507.png)

```ts
// 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を指定することで、そのドメインに対する通信を許可することができます。

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

#### サーバーと拡張機能の信頼関係を築きやすい
拡張IDは manifest.json に "key" を指定することで固定化できます。

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

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

https://developer.chrome.com/docs/extensions/reference/manifest/key?hl=ja

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

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

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

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


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

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

```
```ts
// 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を取得後の処理は、コールバック関数に記載する仕組みですので、どんどんネストが深くなっていきます。

```ts
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 化します。
```ts
// 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ごとのメッセージを呼び出すラッパーを作ります。

🐶 例：犬の名前を取得するメッセージ
```ts
// 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,
  });
};
```

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

```ts
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`でラップし、責務を整理した構成にすることで、  
**保守性・見通しの良さ・再利用性**が大きく向上します。

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