GraphQL Mocker - Chrome Web Store
Capture, edit, and mock GraphQL responses in real-time. Perfect for testing edge cases without backend changes.
https://chromewebstore.google.com/detail/graphql-mocker/olfpbidhiphfmjiegcennnbmponlpbac
概要
アーキテクチャ
核心実装の詳細
1. Fetch APIインターセプト
2. Content Script
3. Background Service Worker
ユースケース
GraphQL MockerはGraphQLリクエストをリアルタイムでキャプチャし、レスポンスを編集してMockデータを返すことができるChrome拡張機能です。フロントエンド開発者がバックエンドAPIなしでも、もしくはAPI変更も待たずに様々なシナリオをテストできるようサポートします。
主な機能
GraphQL MockerはChrome拡張機能の3つの核心コンテキストを活用します。
┌─────────────┐
│ Web Page │
│ (GraphQL) │
└──────┬──────┘
│ 1. Intercept (fetch/XHR monkey patch)
▼
┌─────────────┐
│ appHook.js │ ← ページコンテキストに注入
└──────┬──────┘
│ 2. window.postMessage
▼
┌─────────────┐
│ content.js │ ← Content Script(ブリッジ役割)
└──────┬──────┘
│ 3. chrome.runtime.connect
▼
┌─────────────┐
│background.js│ ← Service Worker(状態管理)
└──────┬──────┘
│ 4. Storage & sync
▼
┌─────────────┐
│ Popup UI │ ← React UI
└─────────────┘なぜこのような構造が必要なのか?
Chrome拡張機能でウェブページのfetchやXMLHttpRequestを直接インターセプトするにはページコンテキストにスクリプトを注入する必要があります。しかし、ページコンテキストはChrome APIにアクセスできないため、以下のようなメッセージパッシングチェーンが必要です。
appHook.tsでブラウザのネイティブfetchをオーバーライドします。
const originalFetch = window.fetch;
window.fetch = async function (input: RequestInfo | URL, init?: RequestInit): Promise<Response> {
const url = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url;
// GraphQLエンドポイントかどうか確認
if (url.includes('/graphql')) {
// リクエストボディからoperationName、query、variablesをパース
if (init?.body && typeof init.body === 'string') {
const body = JSON.parse(init.body);
if (body.query || body.operationName) {
isGraphQL = true;
operationName = body.operationName || 'unknown';
}
}
}
// Mockが有効化されている場合、カスタムレスポンスを返却
const customResponse = customGraphQLResponses.get(operationName);
if (customResponse?.activated && mockerSettings.globalMockEnabled) {
// 遅延を適用
if (customResponse.delay && customResponse.delay > 0) {
await delay(customResponse.delay);
}
// カスタムResponseオブジェクトを生成して返却
return new Response(JSON.stringify(customResponse.response), {
status: 200,
statusText: 'OK',
headers: new Headers({ 'Content-Type': 'application/json' }),
});
}
// 元のfetchを実行後、レスポンスをキャプチャ
const response = await originalFetch.call(this, input, init);
// ... レスポンスキャプチャロジック
return response;
};ポイント
content.tsはページと拡張機能間のブリッジ役割
// ページ → 拡張機能
window.addEventListener("message", (event) => {
if (event.data?.source === "graphql-mocker-web-page") {
port.postMessage(event.data.data);
}
});
// 拡張機能 → ページ
port.onMessage.addListener((message) => {
window.postMessage({
source: "graphql-mocker-content-script",
message,
type: "pass-through-to-app",
}, "*");
});
// appHook.jsをページコンテキストに注入
function injectAppHook() {
const script = document.createElement("script");
script.src = chrome.runtime.getURL("js/appHook.js");
document.head.appendChild(script);
}background.tsはタブごとの状態を管理する中央ストレージ
const stores: Map<number, Store> = new Map();
chrome.runtime.onConnect.addListener((port) => {
const tabId = port.sender?.tab?.id;
port.onMessage.addListener((message: Payload) => {
const store = getStore(tabId);
if (message.msg.type === MessageType.GraphQLResponseCaptured) {
store.updateFrom(message.msg);
saveStoresToStorage();
}
// Custom Response更新時にcontent scriptに伝達
if (message.msg.type === MessageType.GraphQLCustomResponseUpdate) {
ports.get(tabId)?.get('content-script')?.postMessage({
from: 'datastore',
to: 'content-script',
msg: {
type: MessageType.Snapshot,
graphqlCustomResponses: store.graphqlCustomResponses(),
settings: globalSettings,
},
});
}
});
});