Menu

Nexty Serverによるブラウザ拡張機能開発ガイド

概要

ブラウザ拡張機能を開発する際、通常はサーバーサイドコンポーネントが必要です。Nextyには、Supabase認証、Stripe決済など、SaaS製品に不可欠なサーバーサイド機能が付属しています。Nextyを使用してサーバーサイド機能を構築することで、ブラウザ拡張機能の開発も高速化できます。

この記事では、Nextyのサーバーサイド機能を使用してブラウザ拡張機能のログインとビジネスデータ同期を実装する方法について詳しく説明します。

以下の内容は主に3つの要件に対応しています:

  • ブラウザ拡張機能のパブリックデータリクエスト
  • ブラウザ拡張機能のユーザーログイン
  • ブラウザ拡張機能の認証済みプライベートデータリクエスト

テスト方法

この記事では主にアプローチを説明し、コードは完全ではありません。以下の完全なソースコードをクローンしてローカルテストを行うことができます:

git clone https://github.com/WeNextDev/nexty.dev.git
cd nexty.dev
git checkout extension-request-demo
 
pnpm install
pnpm dev
git clone https://github.com/WeNextDev/nexty-extension-request-demo.git
cd nexty-extension-request-demo
 
pnpm install
pnpm dev

要件1:パブリックデータリクエスト

サーバーにパブリックデータを返すエンドポイントがあるとします。リクエストパスは次のようになります:

Loading diagram...

まず、認証を必要としないパブリックエンドポイントをNextyコードで作成します:

app/api/extension-demo/public-data/route.ts
import { apiResponse } from "@/lib/api-response";
 
export async function GET() {
  const publicData = {
    latestAnnouncement:
      "🎉 Nexty.devへようこそ!これはパブリックエンドポイントです。",
  };
 
  try {
    const response = apiResponse.success(publicData);
    response.headers.set(
      "Cache-Control",
      "public, s-maxage=86400, stale-while-revalidate=43200", // 頻繁なリクエストを避け、サーバーの負荷を軽減するためにキャッシュヘッダーを追加
    );
  } catch (error) {
    console.error("パブリックデータの取得エラー:", error);
    return apiResponse.serverError();
  }
} 

ブラウザ拡張機能のフロントエンドからリクエストを送信:

popup.tsx
useEffect(() => {
  chrome.runtime.sendMessage({ type: "GET_PUBLIC_DATA" }, (response) => {
    if (response.success) {
      setPublicDataState({ data: response.data, error: null, isLoading: false });
    } else {
      setPublicDataState({ data: null, error: response.error, isLoading: false });
    }
  });
}, []);

ブラウザ拡張機能のバックグラウンドでリクエストを転送:

background/index.ts
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
  if (request.type === "GET_PUBLIC_DATA") {
    apiFetch("/api/extension-demo/public-data", false, "public_data_cache").then(sendResponse);
    return true;
  }
 
  return false;
});

これでブラウザ拡張機能のフロントエンドがパブリックエンドポイントからデータを受信できるようになります。

要件2:拡張機能ログイン認証とログイン状態同期

認証フロー図

まず認証フロー図を見てみましょう

Loading diagram...

コアとなる原則は3つです:

  1. Nextyウェブサイトが唯一の認証センター:ブラウザ拡張機能はログイン認証情報を処理せず、ウェブサイトのログイン状態を「借りる」だけです
  2. サーバーサイド検証:すべての権限チェックはNextyバックエンドで実行され、データセキュリティを確保します
  3. リアルタイム同期:ブラウザ拡張機能は開くたびに最新のユーザーステータスを検証し、情報の正確性を保証します

実装手順

Nextyプロジェクトでapp/api/user-status/route.tsにユーザーログイン状態を取得するAPIを作成します。ここで非常に重要なステップがあります:拡張機能のログイン状態が持続できるように、完全なCookieを返します:

app/api/user-status/route.ts
// ... その他のコード ...
 
    // ログインしていない場合の情報を返す:
    if (!user) {
      return apiResponse.success({ isLoggedIn: false })
    }
 
    // ログインしている場合の情報を返す
    const responseData = {
      isLoggedIn: true, // ログイン識別子
      id: user.id,
      email: user.email,
      plan: profile?.role || 'user',
    }
 
    const finalResponse = apiResponse.success(responseData)
    // 重要!Cookieを返す
    response.cookies.getAll().forEach((cookie) => {
      finalResponse.cookies.set(cookie)
    })
    return finalResponse
 
// ... その他のコード ...

ブラウザ拡張機能のフロントエンドからリクエストを送信:

popup.tsx
useEffect(() => {
  chrome.runtime.sendMessage({ type: "GET_USER_STATUS" }, (response) => {
    if (response.success) {
      setUserStatusState({ data: response.data, error: null, isLoading: false });
    } else {
      if (!cachedStatus) setUserStatusState({ data: null, error: response.error, isLoading: false });
    }
  });
}, []);

ブラウザ拡張機能のバックグラウンドでリクエストを転送:

background/index.ts
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
  if (request.type === "GET_USER_STATUS") {
    apiFetch("/api/extension-demo/user-status", true, "user_status_cache").then(sendResponse);
    return true;
  }
 
  return false;
});

ユーザーがログインしている場合はユーザー情報を表示し、ログインしていない場合はログインボタンを表示します。ボタンをクリックするとchrome.tabs.createメソッドが呼び出され、新しいタブでログインページが開きます:

    const handleLoginClick = () => {
      chrome.tabs.create({ url: process.env.PLASMO_PUBLIC_SITE_LOGIN_URL });
    };
 
    return (
    <div>
      <h2>ユーザー情報</h2>
      {userStatusState.data && (
        userStatusState.data.isLoggedIn ? (
          <div>
            <p>ようこそ、<strong>{userStatusState.data.email}</strong>!</p>
            <p>あなたのユーザーロール:<strong>{userStatusState.data.plan}</strong></p>
          </div>
        ) : (
          <div>
            <h4>使用するにはログインしてください</h4>
            <button onClick={handleLoginClick}>ログインページへ</button>
          </div>
        )
      )}
    </div>
  );

このドキュメントで提供されているブラウザ拡張機能コードでは、楽観的更新も適用されています。これは、まず古いキャッシュデータを表示し、リクエストが完了した後に新しいデータが古いデータを上書きすることを意味します:

popup.tsx
useEffect(() => {
  // まずキャッシュを読み込む
  const cachedStatus = await storage.get<UserStatus>("user_status_cache");
  if (cachedStatus) {
    setUserStatusState({ data: cachedStatus, error: null, isLoading: false });
  }
 
  chrome.runtime.sendMessage({ type: "GET_USER_STATUS" }, (response) => {
    // ... その他のコード ...
  });
}, []);

ユーザーがブラウザ拡張機能を開いたときにすぐにユーザーステータスを確認できるように、拡張機能がインストールされたときとブラウザが起動したときにもリクエストを送信できます:

background/index.ts
// 拡張機能がインストールされたときまたはブラウザが起動したとき
// 初期キャッシュとして一度ステータスを取得
chrome.runtime.onInstalled.addListener(() => {
  apiFetch("/api/extension-demo/user-status", true, "user_status_cache");
});
 
chrome.runtime.onStartup.addListener(() => {
  apiFetch("/api/extension-demo/user-status", true, "user_status_cache");
});

要件3:プライベートデータリクエスト

実際の製品開発では、ユーザーのサブスクリプション状態など、ログインユーザーがアクセスする必要があるデータがあります。

例として、ユーザーのサブスクリプション状態を取得するエンドポイントを作成します:

app/api/extension-demo/user-benefits/route.ts
export async function GET(request: NextRequest) {
 
  try {
    // 1. ログイン状態をチェック
    const {
      data: { user },
    } = await supabase.auth.getUser();
    if (!user) {
      return apiResponse.unauthorized();
    }
 
    // 2. Server Actionsを呼び出し、別の処理ロジックを書く必要がない
    const benefits = await getUserBenefits(user.id);
 
    // 3. 返す
    const finalResponse = apiResponse.success(benefits);
    response.cookies.getAll().forEach((cookie) => {
      finalResponse.cookies.set(cookie);
    });
    return finalResponse;
  } catch (error) {
    console.error("ユーザー特典の取得エラー:", error);
    return apiResponse.serverError();
  }
} 

ブラウザフロントエンドはリクエストを送信する前にログイン状態をチェックする必要があります - ログインユーザーのみがリクエストを送信できます:

popup.tsx
if (!userStatusState.isLoading && userStatusState.data?.isLoggedIn) {
 
  const cachedBenefits = await storage.get<UserBenefits>("user_benefits_cache");
  if (cachedBenefits) {
    setUserBenefitsState({ data: cachedBenefits, error: null, isLoading: false });
  }
 
  chrome.runtime.sendMessage({ type: "GET_USER_BENEFITS" }, (response) => {
    if (response.success) {
      setUserBenefitsState({ data: response.data, error: null, isLoading: false });
    } else {
      if (!cachedBenefits) setUserBenefitsState({ data: null, error: response.error, isLoading: false });
    }
  });
}

ブラウザ拡張機能のバックグラウンドは引き続きリクエストを転送します:

background/index.ts
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
  if (request.type === "GET_USER_BENEFITS") {
    apiFetch("/api/extension-demo/user-benefits", true, "user_benefits_cache").then(sendResponse);
    return true;
  }
 
  return false;
});

その他のベストプラクティス推奨事項

  • 独立したAPIプレフィックスの使用:ブラウザ拡張機能に提供するAPIには独立したプレフィックスを使用し、共通ロジックの処理を容易にします

  • キャッシュを活用してサーバーの負荷を軽減:高いリアルタイム性を必要としないデータについては、Next.jsのキャッシュ機能を使用するか、HTTPレスポンスにキャッシュヘッダーを追加してサーバーの負荷を軽減します

    注目すべき点

    Nextyソースコード拡張機能ブランチのapp/api/extension-demo/public-data/route.tsを参照してください

  • リクエストの暗号化:ブラウザ拡張機能のリクエストには暗号化された署名ヘッダーを追加し、パブリックエンドポイントでも追加の保護層を設けます

    注目すべき点

    デモソースコードの場所:

    • Nextyソースコード拡張機能ブランチのmiddleware.tslib/crypto-utils.ts
    • ブラウザ拡張機能ソースコードのbackground/index.tsbackground/crypto-utils.ts

    crypto-utils.tsの暗号化メソッドは例示目的のものです。より複雑な暗号化メソッドが必要な場合は、AIに作成を依頼できます。暗号化アルゴリズムについては非常に優秀です。