ホームページ >ウェブフロントエンド >jsチュートリアル >Next.js から Cloudflare ワーカーを使用した React Edge へ: 解放の物語

Next.js から Cloudflare ワーカーを使用した React Edge へ: 解放の物語

Susan Sarandon
Susan Sarandonオリジナル
2024-11-20 14:26:17917ブラウズ

クイックインデックス

  • 最後のわら
  • Cloudflare の代替手段?
  • React Edge: すべての開発者の苦悩から (またはほぼ) 誕生した React フレームワーク
    • 型付き RPC の魔法
    • useFetch の力: 魔法が起こる場所
  • useFetch を超えて: 完全なアーセナル
    • RPC: クライアントサーバー通信の技術
    • 理にかなった i18n システム
    • 「正しく機能する」JWT 認証
    • 共有ストア
    • エレガントなルーティング
    • エッジキャッシュを使用した分散キャッシュ
  • リンク: 先を考えるコンポーネント
  • app.useContext: エッジへのポータル
  • app.useUrlState: URL と同期された状態
  • app.useStorageState: 永続状態
  • app.useDebounce: 周波数制御
  • app.useDistinct: 重複のない状態
  • React Edge CLI: 指先ひとつでパワーを発揮
  • 結論

最後のわら

すべては Vercel の請求書から始まりました。いいえ、実際には、それはずっと前から始まり、小さなフラストレーションが積み重なっていました。 DDoS 保護、より詳細なログ、さらには適切なファイアウォール、ビルド キューなどの基本機能に料金を支払う必要性。ますます高価になるベンダー ロックインに閉じ込められている感覚。

「そして最悪のことに、私たちの貴重な SEO ヘッダーが、ページ ルーターを使用するアプリケーションのサーバー上でレンダリングを停止してしまいました。開発者にとっては本当に頭の痛い問題です!?」

しかし、本当にすべてを考え直させたのは、Next.js が目指している方向性でした。 use client、use server ディレクティブの導入により、理論的には開発が簡素化されるはずですが、実際には管理がさらに複雑になります。それは PHP の時代に戻って、どこで実行するかを指示するディレクティブでファイルにマークを付けるようなものでした。

これで終わりではありません。 App Router — 興味深いアイデアですが、Next.js 内にほぼまったく新しいフレームワークを作成する方法で実装されました。突然、同じことを行うのに 2 つの完全に異なる方法 (「古い」方法と「新しい」) が存在し、微妙に異なる動作と隠れた落とし穴が存在しました。

Cloudflareの代替案?

そこで思いつきました。ワーカーがエッジで実行され、ストレージには R2、分散データには KV を備えた Cloudflare の素晴らしいインフラストラクチャを活用してはいかがでしょうか... もちろん、驚くべき DDoS 保護、グローバル CDN、ファイアウォールとともに、ページルールとルーティング、その他Cloudflareが提供するすべてのもの。

そして最も優れている点は、使用した分だけ料金を支払う公正な価格モデルです。

こうして React Edge が誕生しました。車輪の再発明を目的とするのではなく、真にシンプルでモダンな開発エクスペリエンスを提供するフレームワークです。

React Edge: すべての開発者の苦悩から (またはほぼ) 誕生した React フレームワーク

React Edge の開発を始めたとき、私には明確な目標がありました。それは、意味のあるフレームワークを作成することです。紛らわしいディレクティブに悩まされることも、基本機能に法外な料金を支払うことも、そして最も重要なことに、クライアント/サーバーの分離によって引き起こされる人為的な複雑さに対処することももうありません。私はスピード、つまりシンプルさを犠牲にすることなくパフォーマンスを実現するフレームワークを求めていました。 React の API に関する知識と、JavaScript および Golang 開発者としての長年の経験を活用して、ストリームと多重化を処理してレンダリングとデータ管理を最適化する方法を正確に知っていました。

Cloudflare Workers は、強力なインフラストラクチャと世界的なプレゼンスを備えており、これらの可能性を探るための完璧な環境を提供しました。私は真のハイブリッドなものを望んでいました。そして、このツールと経験の組み合わせが、現実世界の問題を最新の効率的なソリューションで解決するフレームワークである React Edge に命を吹き込みました。

React Edge は、React 開発に革新的なアプローチを導入します。サーバー上にクラスを作成し、それをクライアントから直接呼び出すことを想像してみてください。完全な型安全性と構成は不要です。タグやプレフィックスによる無効化を可能にし、「正常に機能する」分散キャッシュ システムを想像してください。サーバーとクライアント間で状態をシームレスかつ安全に共有できることを想像してください。簡素化された認証、効率的な国際化システム、CLI などを追加します。

RPC 通信はほとんど魔法のように感じられます。クラス内にメソッドを記述し、ローカルであるかのようにクライアントからメソッドを呼び出します。インテリジェントな多重化システムにより、複数のコンポーネントが同じ呼び出しを行った場合でも、サーバーに送信されるリクエストは 1 つだけになります。一時的なキャッシュにより、不要なリクエストの繰り返しが回避され、すべてがサーバーとクライアントの両方でシームレスに動作します。

最も強力な機能の 1 つは、データ取得エクスペリエンスを統合する app.useFetch フックです。サーバー上では、SSR 中にデータがプリロードされます。クライアントでは、それらのデータが自動的にハイドレートされ、オンデマンドの更新がサポートされます。自動ポーリングと依存関係ベースの反応性のサポートにより、動的インターフェースの作成がかつてないほど簡単になりました。

しかし、それだけではありません。このフレームワークは、強力なルーティング システム (素晴らしい Hono からインスピレーションを得た)、Cloudflare R2 による統合資産管理、および HttpError クラスを介してエラーを処理するエレガントな方法を提供します。ミドルウェアは共有ストアを通じてクライアントにデータを簡単に送信でき、セキュリティのためにすべてが自動的に難読化されます。

最も印象に残っている部分は何ですか?フレームワークのコードのほぼすべてがハイブリッドです。 「クライアント」バージョンと「サーバー」バージョンはありません。同じコードが両方の環境で動作し、コンテキストに自動的に適応します。クライアントは必要なものだけを受け取り、最終的なバンドルが非常に最適化されます。

そしておまけに、これらはすべて Cloudflare Workers エッジインフラストラクチャ上で実行され、適正なコストで優れたパフォーマンスを提供します。驚くべき請求書や、高価なエンタープライズ プランの背後にロックされている基本機能はありません。本当に重要なこと、つまり素晴らしいアプリケーションの構築に集中できる強固なフレームワークがあるだけです。さらに、React Edge は、キュー、デュラブル オブジェクト、KV ストレージなどを含む Cloudflare のエコシステムを活用し、アプリケーションに堅牢でスケーラブルな基盤を提供します。

Vite は、開発環境、テスト、ビルド プロセスのベースとして使用されました。 Vite は、その驚異的なスピードと最新のアーキテクチャにより、機敏で効率的なワークフローを実現します。開発を加速するだけでなく、ビルドプロセスを最適化し、高速かつ正確なコンパイルを保証します。疑いもなく、Vite は React Edge にとって完璧な選択でした。

エッジ コンピューティング時代に向けた React 開発の再考

クライアント/サーバーの壁を気にせずに React アプリケーションを開発できたらどうなるだろうかと考えたことはありますか? use client や use server などの何十ものディレクティブを覚えなくても?さらに良いことに、サーバー関数をローカルであるかのように、完全な型付けと設定なしで呼び出すことができたらどうでしょうか?

React Edge を使用すると、次のことは必要なくなります。

  • 個別の API ルートを作成する
  • 読み込み/エラー状態を手動で管理する
  • デバウンスを自分で実装する
  • シリアル化/逆シリアル化が心配
  • CORS に対処する
  • クライアント/サーバー間の入力を管理
  • 認証ルールを手動で処理する
  • 国際化設定に苦労しています

そして最も重要な点は、これらすべてが、クライアント使用またはサーバー使用として何もマークすることなく、サーバーとクライアントの両方でシームレスに機能することです。フレームワークはコンテキストに基づいて何をすべきかを自動的に認識します。飛び込んでみませんか?

型付き RPC の魔法

これができると想像してみてください:

// On the server
class UserAPI extends Rpc {
  async searchUsers(query: string, filters: UserFilters) {
    // Validation with Zod
    const validated = searchSchema.parse({ query, filters });
    return this.db.users.search(validated);
  }
}

// On the client
const UserSearch = () => {
  const { rpc } = app.useContext<App.Context>();

  // TypeScript knows exactly what searchUsers accepts and returns!
  const { data, loading, error, fetch: retry } = app.useFetch(
    async (ctx) => ctx.rpc.searchUsers('John', { age: '>18' })
  );
};

これを Next.js/Vercel と比較してください。

// pages/api/search.ts
export default async handler = (req, res) => {
  // Configure CORS
  // Validate request
  // Handle errors
  // Serialize response
  // ...100 lines later...
}

// app/search/page.tsx
'use client';
import { useEffect, useState } from 'react';

export default const SearchPage = () => {
  const [search, setSearch] = useState('');
  const [filters, setFilters] = useState({});
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    let timeout;
    const doSearch = async () => {
      setLoading(true);
      try {
        const res = await fetch('/api/search?' + new URLSearchParams({
          q: search,
          ...filters
        }));
        if (!res.ok) throw new Error('Search failed');
        setData(await res.json());
      } catch (err) {
        console.error(err);
      } finally {
        setLoading(false);
      }
    };

    timeout = setTimeout(doSearch, 300);
    return () => clearTimeout(timeout);
  }, [search, filters]);

  // ... rest of the component
}

useFetchの力: 魔法が起こる場所

データ取得の再考

React でのデータ取得について知っていることはすべて忘れてください。 React Edge の app.useFetch フックは、まったく新しい強力なアプローチを導入します。次のようなフックを想像してください:

  • SSR 中にサーバーにデータをプリロードします。
  • ちらつきなく、クライアント上のデータを自動的にハイドレートします。
  • クライアントとサーバーの間で完全な型付けを維持します。
  • インテリジェントなデバウンスにより反応性をサポートします。
  • 同一の通話を自動的に多重化します。
  • プログラムによる更新とポーリングを有効にします。

実際に動作を見てみましょう:

// On the server
class UserAPI extends Rpc {
  async searchUsers(query: string, filters: UserFilters) {
    // Validation with Zod
    const validated = searchSchema.parse({ query, filters });
    return this.db.users.search(validated);
  }
}

// On the client
const UserSearch = () => {
  const { rpc } = app.useContext<App.Context>();

  // TypeScript knows exactly what searchUsers accepts and returns!
  const { data, loading, error, fetch: retry } = app.useFetch(
    async (ctx) => ctx.rpc.searchUsers('John', { age: '>18' })
  );
};

多重化の魔法

上記の例では、インテリジェントな多重化という強力な機能が隠されています。 ctx.rpc.batch を使用すると、React Edge は呼び出しをグループ化するだけでなく、同一の呼び出しを自動的に重複排除します。

// pages/api/search.ts
export default async handler = (req, res) => {
  // Configure CORS
  // Validate request
  // Handle errors
  // Serialize response
  // ...100 lines later...
}

// app/search/page.tsx
'use client';
import { useEffect, useState } from 'react';

export default const SearchPage = () => {
  const [search, setSearch] = useState('');
  const [filters, setFilters] = useState({});
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    let timeout;
    const doSearch = async () => {
      setLoading(true);
      try {
        const res = await fetch('/api/search?' + new URLSearchParams({
          q: search,
          ...filters
        }));
        if (!res.ok) throw new Error('Search failed');
        setData(await res.json());
      } catch (err) {
        console.error(err);
      } finally {
        setLoading(false);
      }
    };

    timeout = setTimeout(doSearch, 300);
    return () => clearTimeout(timeout);
  }, [search, filters]);

  // ... rest of the component
}

SSR パーフェクトハイドレーション

// First, define your API on the server
class PropertiesAPI extends Rpc {
  async searchProperties(filters: PropertyFilters) {
    const results = await this.db.properties.search(filters);
    // Automatic caching for 5 minutes
    return this.createResponse(results, {
      cache: { ttl: 300, tags: ['properties'] }
    });
  }

  async getPropertyDetails(ids: string[]) {
    return Promise.all(
      ids.map(id => this.db.properties.findById(id))
    );
  }
}

// Now, on the client, the magic happens
const PropertySearch = () => {
  const [filters, setFilters] = useState<PropertyFilters>({
    price: { min: 100000, max: 500000 },
    bedrooms: 2
  });

  // Reactive search with intelligent debounce
  const {
    data: searchResults,
    loading: searchLoading,
    error: searchError
  } = app.useFetch(
    async (ctx) => ctx.rpc.searchProperties(filters),
    {
      // Re-fetch when filters change
      deps: [filters],
      // Wait 300ms of "silence" before fetching
      depsDebounce: {
        filters: 300
      }
    }
  );

  // Fetch property details for the found results
  const {
    data: propertyDetails,
    loading: detailsLoading,
    fetch: refreshDetails
  } = app.useFetch(
    async (ctx) => {
      if (!searchResults?.length) return null;

      // This looks like multiple calls, but...
      return ctx.rpc.batch([
        // Everything is multiplexed into a single request!
        ...searchResults.map(result =>
          ctx.rpc.getPropertyDetails(result.id)
        )
      ]);
    },
    {
      // Refresh when searchResults change
      deps: [searchResults]
    }
  );

  // A beautiful and responsive interface
  return (
    <div>
      <FiltersPanel
        value={filters}
        onChange={setFilters}
        disabled={searchLoading}
      />

      {searchError && (
        <Alert status='error'>
          Search error: {searchError.message}
        </Alert>
      )}

      <PropertyGrid
        items={propertyDetails || []}
        loading={detailsLoading}
        onRefresh={() => refreshDetails()}
      />
    </div>
  );
};

useFetch を超えて: 完全なアーセナル

RPC: クライアントサーバー通信の技術

セキュリティとカプセル化

React Edge の RPC システムは、セキュリティとカプセル化を念頭に置いて設計されています。 RPC クラス内のすべてが自動的にクライアントに公開されるわけではありません:

const PropertyListingPage = () => {
  const { data } = app.useFetch(async (ctx) => {
    // Even if you make 100 identical calls...
    return ctx.rpc.batch([
      ctx.rpc.getProperty('123'),
      ctx.rpc.getProperty('123'), // same call
      ctx.rpc.getProperty('456'),
      ctx.rpc.getProperty('456'), // same call
    ]);
  });

  // Behind the scenes:
  // 1. The batch groups all calls into ONE single HTTP request.
  // 2. Identical calls are deduplicated automatically.
  // 3. Results are distributed correctly to each position in the array.
  // 4. Typing is maintained for each individual result!

  // Actual RPC calls:
  // 1. getProperty('123')
  // 2. getProperty('456')
  // Results are distributed correctly to all callers!
};

RPC API 階層

RPC の最も強力な機能の 1 つは、API を階層に編成する機能です。

One of the most impressive parts is how useFetch handles SSR:

const ProductPage = ({ productId }: Props) => {
  const { data, loaded, loading, error } = app.useFetch(
    async (ctx) => ctx.rpc.getProduct(productId),
    {
      // Fine-grained control over when to fetch
      shouldFetch: ({ worker, loaded }) => {
        // On the worker (SSR): always fetch
        if (worker) return true;
        // On the client: fetch only if no data is loaded
        return !loaded;
      }
    }
  );

  // On the server:
  // 1. `useFetch` makes the RPC call.
  // 2. Data is serialized and sent to the client.
  // 3. Component renders with the data.

  // On the client:
  // 1. Component hydrates with server data.
  // 2. No new call is made (shouldFetch returns false).
  // 3. If necessary, you can re-fetch with `data.fetch()`.

  return (
    <Suspense fallback={<ProductSkeleton />}>
      <ProductView
        product={data}
        loading={loading}
        error={error}
      />
    </Suspense>
  );
};

階層の利点

API を階層に編成すると、次のような利点があります。

  • 論理的な構成: 関連する機能を直感的にグループ化します。
  • 自然な名前空間: 明確なパス (users.preferences.getTheme など) との名前の競合を回避します。
  • カプセル化: 各レベルでヘルパー メソッドをプライベートに保ちます。
  • 保守性: 各サブクラスは個別に保守およびテストできます。
  • 完全な型付け: TypeScript は階層全体を理解します。

React Edge の RPC システムにより、クライアントとサーバー間の通信が非常に自然になるため、リモート呼び出しを行っていることをほとんど忘れてしまいます。 API を階層に編成する機能により、コードをクリーンで安全に保ちながら複雑な構造を構築できます。

理にかなった i18n のシステム

React Edge は、重いライブラリに依存せずに変数補間と複雑な書式設定をサポートする、エレガントで柔軟な国際化システムを導入しています。

class PaymentsAPI extends Rpc {
  // Properties are never exposed
  private stripe = new Stripe(process.env.STRIPE_KEY);

  // Methods starting with $ are private
  private async $validateCard(card: CardInfo) {
    return await this.stripe.cards.validate(card);
  }

  // Methods starting with _ are also private
  private async _processPayment(amount: number) {
    return await this.stripe.charges.create({ amount });
  }

  // This method is public and accessible via RPC
  async createPayment(orderData: OrderData) {
    // Internal validation using a private method
    const validCard = await this.$validateCard(orderData.card);
    if (!validCard) {
      throw new HttpError(400, 'Invalid card');
    }

    // Processing using another private method
    const payment = await this._processPayment(orderData.amount);
    return payment;
  }
}

// On the client:
const PaymentForm = () => {
  const { rpc } = app.useContext<App.Context>();

  // ✅ This works
  const handleSubmit = () => rpc.createPayment(data);

  // ❌ These do not work - private methods are not exposed
  const invalid1 = () => rpc.$validateCard(data);
  const invalid2 = () => rpc._processPayment(100);

  // ❌ This also does not work - properties are not exposed
  const invalid3 = () => rpc.stripe;
};

コード内での使用法:

// Nested APIs for better organization
class UsersAPI extends Rpc {
  // Subclass to manage preferences
  preferences = new UserPreferencesAPI();
  // Subclass to manage notifications
  notifications = new UserNotificationsAPI();

  async getProfile(id: string) {
    return this.db.users.findById(id);
  }
}

class UserPreferencesAPI extends Rpc {
  async getTheme(userId: string) {
    return this.db.preferences.getTheme(userId);
  }

  async setTheme(userId: string, theme: Theme) {
    return this.db.preferences.setTheme(userId, theme);
  }
}

class UserNotificationsAPI extends Rpc {
  // Private methods remain private
  private async $sendPush(userId: string, message: string) {
    await this.pushService.send(userId, message);
  }

  async getSettings(userId: string) {
    return this.db.notifications.getSettings(userId);
  }

  async notify(userId: string, notification: Notification) {
    const settings = await this.getSettings(userId);
    if (settings.pushEnabled) {
      await this.$sendPush(userId, notification.message);
    }
  }
}

// On the client:
const UserProfile = () => {
  const { rpc } = app.useContext<App.Context>();

  const { data: profile } = app.useFetch(
    async (ctx) => {
      // Nested calls are fully typed
      const [user, theme, notificationSettings] = await ctx.rpc.batch([
        // Method from the main class
        ctx.rpc.getProfile('123'),
        // Method from the preferences subclass
        ctx.rpc.preferences.getTheme('123'),
        // Method from the notifications subclass
        ctx.rpc.notifications.getSettings('123')
      ]);

      return { user, theme, notificationSettings };
    }
  );

  // ❌ Private methods remain inaccessible
  const invalid = () => rpc.notifications.$sendPush('123', 'hello');
};

ゼロ構成

React Edge は翻訳を自動的に検出して読み込みます。ユーザー設定を Cookie に簡単に保存することもできます。でも、当然、これは予想できますよね?

// translations/fr.ts
export default {
  'Good Morning, {name}!': 'Bonjour, {name}!',
};

「正しく機能する」JWT 認証

Web アプリケーションでは認証が常に問題点でした。 JWT トークン、セキュア Cookie、再検証の管理には、多くの場合、多くの定型コードが必要になります。 React Edge はこれを完全に変えます。

完全な認証システムの実装がいかに簡単かを次に示します:

const WelcomeMessage = () => {
  const userName = 'John';

  return (
    <div>
      {/* Output: Good Morning, John! */}
      <h1>{__('Good Morning, {name}!', { name: userName })}</h1>
    </div>
  );
};

クライアントの使用法: ゼロ構成

// worker.ts
const handler = {
  fetch: async (request: Request, env: types.Worker.Env, context: ExecutionContext) => {
    const url = new URL(request.url);

    const lang = (() => {
      const lang =
        url.searchParams.get('lang') || worker.cookies.get(request.headers, 'lang') || request.headers.get('accept-language') || '';

      if (!lang || !i18n[lang]) {
        return 'en-us';
      }

      return lang;
    })();

    const workerApp = new AppWorkerEntry({
      i18n: {
        en: await import('./translations/en'),
        pt: await import('./translations/pt'),
        es: await import('./translations/es')
      }
    });

    const res = await workerApp.fetch();

    if (url.searchParams.has('lang')) {
      return new Response(res.body, {
        headers: worker.cookies.set(res.headers, 'lang', lang)
      });
    }

    return res;
  }
};

なぜこれが革命的なのでしょうか?

  1. 定型文ゼロ

    • 手動による Cookie 管理は不要
    • インターセプターは必要ありません
    • 手動更新トークンはありません
  2. デフォルトのセキュリティ

    • トークンは自動的に暗号化されます
    • Cookie は安全で httpOnly です
    • 自動再検証
  3. 完全な入力

    • JWT ペイロードが型指定されています
    • 統合された Zod 検証
    • 入力された認証エラー
  4. シームレスな統合

// On the server
class UserAPI extends Rpc {
  async searchUsers(query: string, filters: UserFilters) {
    // Validation with Zod
    const validated = searchSchema.parse({ query, filters });
    return this.db.users.search(validated);
  }
}

// On the client
const UserSearch = () => {
  const { rpc } = app.useContext<App.Context>();

  // TypeScript knows exactly what searchUsers accepts and returns!
  const { data, loading, error, fetch: retry } = app.useFetch(
    async (ctx) => ctx.rpc.searchUsers('John', { age: '>18' })
  );
};

共有ストア

React Edge の最も強力な機能の 1 つは、ワーカーとクライアントの間で状態を安全に共有できる機能です。仕組みは次のとおりです:

ミドルウェアとストアの利用例

// pages/api/search.ts
export default async handler = (req, res) => {
  // Configure CORS
  // Validate request
  // Handle errors
  // Serialize response
  // ...100 lines later...
}

// app/search/page.tsx
'use client';
import { useEffect, useState } from 'react';

export default const SearchPage = () => {
  const [search, setSearch] = useState('');
  const [filters, setFilters] = useState({});
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    let timeout;
    const doSearch = async () => {
      setLoading(true);
      try {
        const res = await fetch('/api/search?' + new URLSearchParams({
          q: search,
          ...filters
        }));
        if (!res.ok) throw new Error('Search failed');
        setData(await res.json());
      } catch (err) {
        console.error(err);
      } finally {
        setLoading(false);
      }
    };

    timeout = setTimeout(doSearch, 300);
    return () => clearTimeout(timeout);
  }, [search, filters]);

  // ... rest of the component
}

仕組み

  • パブリック データ: パブリックとしてマークされたデータはクライアントと安全に共有され、コンポーネントから簡単にアクセスできるようになります。
  • プライベート データ: 機密データは作業者の環境内に残り、クライアントに公開されることはありません。
  • ミドルウェアとの統合: ミドルウェアはストアにパブリック データとプライベート データの両方を入力できるため、サーバー側のロジックとクライアント側のレンダリングの間のシームレスな情報の流れが確保されます。

利点

  1. セキュリティ: パブリック データ スコープとプライベート データ スコープを分けることで、機密情報が確実に保護されます。
  2. 利便性: ストア データへの透過的なアクセスにより、ワーカーとクライアント全体の状態管理が簡素化されます。
  3. 柔軟性: ストアはミドルウェアと簡単に統合できるため、リクエスト処理に基づいて動的な状態を更新できます。

エレガントなルーティング

React Edge のルーティング システムは Hono からインスピレーションを受けていますが、SSR 用に強化された機能が備えられています。

// First, define your API on the server
class PropertiesAPI extends Rpc {
  async searchProperties(filters: PropertyFilters) {
    const results = await this.db.properties.search(filters);
    // Automatic caching for 5 minutes
    return this.createResponse(results, {
      cache: { ttl: 300, tags: ['properties'] }
    });
  }

  async getPropertyDetails(ids: string[]) {
    return Promise.all(
      ids.map(id => this.db.properties.findById(id))
    );
  }
}

// Now, on the client, the magic happens
const PropertySearch = () => {
  const [filters, setFilters] = useState<PropertyFilters>({
    price: { min: 100000, max: 500000 },
    bedrooms: 2
  });

  // Reactive search with intelligent debounce
  const {
    data: searchResults,
    loading: searchLoading,
    error: searchError
  } = app.useFetch(
    async (ctx) => ctx.rpc.searchProperties(filters),
    {
      // Re-fetch when filters change
      deps: [filters],
      // Wait 300ms of "silence" before fetching
      depsDebounce: {
        filters: 300
      }
    }
  );

  // Fetch property details for the found results
  const {
    data: propertyDetails,
    loading: detailsLoading,
    fetch: refreshDetails
  } = app.useFetch(
    async (ctx) => {
      if (!searchResults?.length) return null;

      // This looks like multiple calls, but...
      return ctx.rpc.batch([
        // Everything is multiplexed into a single request!
        ...searchResults.map(result =>
          ctx.rpc.getPropertyDetails(result.id)
        )
      ]);
    },
    {
      // Refresh when searchResults change
      deps: [searchResults]
    }
  );

  // A beautiful and responsive interface
  return (
    <div>
      <FiltersPanel
        value={filters}
        onChange={setFilters}
        disabled={searchLoading}
      />

      {searchError && (
        <Alert status='error'>
          Search error: {searchError.message}
        </Alert>
      )}

      <PropertyGrid
        items={propertyDetails || []}
        loading={detailsLoading}
        onRefresh={() => refreshDetails()}
      />
    </div>
  );
};

主な特長

  • グループ化されたルート: 共有パスとミドルウェアの下で関連するルートを論理的にグループ化します。
  • 柔軟なハンドラー: ページを返すハンドラー、または API 応答を指示するハンドラーを定義します。
  • ルートごとのヘッダー: 個々のルートの HTTP ヘッダーをカスタマイズします。
  • 組み込みキャッシュ: ttl とタグを使用してキャッシュ戦略を簡素化します。

利点

  1. 一貫性: 関連するルートをグループ化することで、一貫したミドルウェア アプリケーションとコード構成が確保されます。
  2. スケーラビリティ: システムは、大規模アプリケーション向けにネストされたモジュール型ルーティングをサポートします。
  3. パフォーマンス: キャッシュのネイティブ サポートにより、手動構成を行わなくても最適な応答時間が確保されます。

エッジキャッシュを使用した分散キャッシュ

React Edge には、JSON データとページ全体の両方に対してシームレスに動作する強力なキャッシュ システムが含まれています。このキャッシュ システムは、インテリジェントなタグ付けとプレフィックス ベースの無効化をサポートしているため、幅広いシナリオに適しています。

例: タグを使用した API 応答のキャッシュ

// On the server
class UserAPI extends Rpc {
  async searchUsers(query: string, filters: UserFilters) {
    // Validation with Zod
    const validated = searchSchema.parse({ query, filters });
    return this.db.users.search(validated);
  }
}

// On the client
const UserSearch = () => {
  const { rpc } = app.useContext<App.Context>();

  // TypeScript knows exactly what searchUsers accepts and returns!
  const { data, loading, error, fetch: retry } = app.useFetch(
    async (ctx) => ctx.rpc.searchUsers('John', { age: '>18' })
  );
};

主な特長

  • タグベースの無効化: キャッシュ エントリはタグを使用してグループ化できるため、データ変更時に簡単かつ選択的に無効化できます。
  • プレフィックス マッチング: 共通のプレフィックスを使用して複数のキャッシュ エントリを無効にし、検索クエリや階層データなどのシナリオに最適です。
  • 存続時間 (TTL): キャッシュ エントリの有効期限を設定して、高いパフォーマンスを維持しながらデータの鮮度を確保します。

利点

  1. パフォーマンスの向上: 頻繁にアクセスされるデータに対してキャッシュされた応答を提供することで、API の負荷を軽減します。
  2. スケーラビリティ: 分散キャッシュ システムにより、大規模なデータセットと高トラフィックを効率的に処理します。
  3. 柔軟性: キャッシュをきめ細かく制御できるため、開発者はデータの精度を犠牲にすることなくパフォーマンスを最適化できます。

リンク: 先を考えるコンポーネント

リンク コンポーネントは、クライアント側リソースをプリロードするためのインテリジェントでパフォーマンス指向のソリューションであり、ユーザーにとってよりスムーズで高速なナビゲーション エクスペリエンスを保証します。そのプリフェッチ機能は、ユーザーがリンク上にマウスを移動するとトリガーされ、アイドル状態の時間を利用して宛先データを事前にリクエストします。

仕組み

  1. 条件付きプリフェッチ: プリフェッチ属性 (デフォルトで有効) は、プリロードを実行するかどうかを制御します。
  2. インテリジェント キャッシュ: セットは、すでにプリフェッチされたリンクを保存し、冗長なフェッチ呼び出しを回避するために使用されます。
  3. Mouse Enter イベント: ユーザーがリンク上にマウスを移動すると、handleMouseEnter 関数はプリロードが必要かどうかを確認し、必要な場合は宛先へのフェッチ リクエストを開始します。
  4. エラー耐性: リクエスト中のあらゆる障害が抑制され、コンポーネントの動作が一時的なネットワークの問題による影響を受けないようになります。

使用例

// pages/api/search.ts
export default async handler = (req, res) => {
  // Configure CORS
  // Validate request
  // Handle errors
  // Serialize response
  // ...100 lines later...
}

// app/search/page.tsx
'use client';
import { useEffect, useState } from 'react';

export default const SearchPage = () => {
  const [search, setSearch] = useState('');
  const [filters, setFilters] = useState({});
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    let timeout;
    const doSearch = async () => {
      setLoading(true);
      try {
        const res = await fetch('/api/search?' + new URLSearchParams({
          q: search,
          ...filters
        }));
        if (!res.ok) throw new Error('Search failed');
        setData(await res.json());
      } catch (err) {
        console.error(err);
      } finally {
        setLoading(false);
      }
    };

    timeout = setTimeout(doSearch, 300);
    return () => clearTimeout(timeout);
  }, [search, filters]);

  // ... rest of the component
}

ユーザーが「About Us」リンクの上にマウスを移動すると、コンポーネントは /about ページのデータのプリロードを開始し、ほぼ瞬時に移行します。天才的なアイデアですね。 React.dev ドキュメントからインスピレーションを受けています。

app.useContext: エッジへのポータル

app.useContext フックは React Edge の基礎であり、ワーカーのコンテキスト全体へのシームレスなアクセスを許可します。ルーティング、状態、RPC 呼び出しなどを管理するための強力なインターフェイスを提供します。

例: ダッシュボードでの app.useContext の使用

// First, define your API on the server
class PropertiesAPI extends Rpc {
  async searchProperties(filters: PropertyFilters) {
    const results = await this.db.properties.search(filters);
    // Automatic caching for 5 minutes
    return this.createResponse(results, {
      cache: { ttl: 300, tags: ['properties'] }
    });
  }

  async getPropertyDetails(ids: string[]) {
    return Promise.all(
      ids.map(id => this.db.properties.findById(id))
    );
  }
}

// Now, on the client, the magic happens
const PropertySearch = () => {
  const [filters, setFilters] = useState<PropertyFilters>({
    price: { min: 100000, max: 500000 },
    bedrooms: 2
  });

  // Reactive search with intelligent debounce
  const {
    data: searchResults,
    loading: searchLoading,
    error: searchError
  } = app.useFetch(
    async (ctx) => ctx.rpc.searchProperties(filters),
    {
      // Re-fetch when filters change
      deps: [filters],
      // Wait 300ms of "silence" before fetching
      depsDebounce: {
        filters: 300
      }
    }
  );

  // Fetch property details for the found results
  const {
    data: propertyDetails,
    loading: detailsLoading,
    fetch: refreshDetails
  } = app.useFetch(
    async (ctx) => {
      if (!searchResults?.length) return null;

      // This looks like multiple calls, but...
      return ctx.rpc.batch([
        // Everything is multiplexed into a single request!
        ...searchResults.map(result =>
          ctx.rpc.getPropertyDetails(result.id)
        )
      ]);
    },
    {
      // Refresh when searchResults change
      deps: [searchResults]
    }
  );

  // A beautiful and responsive interface
  return (
    <div>
      <FiltersPanel
        value={filters}
        onChange={setFilters}
        disabled={searchLoading}
      />

      {searchError && (
        <Alert status='error'>
          Search error: {searchError.message}
        </Alert>
      )}

      <PropertyGrid
        items={propertyDetails || []}
        loading={detailsLoading}
        onRefresh={() => refreshDetails()}
      />
    </div>
  );
};

app.useContext の主な機能

  • ルート管理: 一致したルート、そのパラメータ、クエリ文字列に簡単にアクセスできます。
  • RPC 統合: 追加の構成を行わずに、クライアントから直接、型指定された安全な RPC 呼び出しを実行します。
  • 共有ストア アクセス: 可視性 (パブリック/プライベート) を完全に制御して、ワーカーとクライアントの共有状態で値を取得または設定します。
  • ユニバーサル URL アクセス: 動的レンダリングとインタラクションのための現在のリクエストの完全な URL に簡単にアクセスします。

なぜ強力なのか

app.useContext フックは、ワーカーとクライアントの間のギャップを橋渡しします。これにより、ボイラープレートを使用せずに、共有状態、安全なデータ取得、コンテキスト レンダリングに依存する機能を構築できます。これにより、複雑なアプリケーションが簡素化され、保守が容易になり、開発が迅速化されます。

app.useUrlState: URLと同期された状態

app.useUrlState フックは、アプリケーションの状態を URL クエリ パラメーターと同期させて、URL に含まれる内容、状態のシリアル化方法、更新時期をきめ細かく制御できます。

// On the server
class UserAPI extends Rpc {
  async searchUsers(query: string, filters: UserFilters) {
    // Validation with Zod
    const validated = searchSchema.parse({ query, filters });
    return this.db.users.search(validated);
  }
}

// On the client
const UserSearch = () => {
  const { rpc } = app.useContext<App.Context>();

  // TypeScript knows exactly what searchUsers accepts and returns!
  const { data, loading, error, fetch: retry } = app.useFetch(
    async (ctx) => ctx.rpc.searchUsers('John', { age: '>18' })
  );
};

パラメータ

  1. 初期状態

    • 状態のデフォルトの構造と値を定義するオブジェクト。
  2. オプション:

    • debounce: 状態が変化した後に URL を更新する速度を制御します。過剰なアップデートを防ぐのに役立ちます。
    • kebabCase: URL にシリアル化するときに状態キーを kebab-case に変換します (例: filter.locations → filter-locations)。
    • omitKeys: URL から除外するキーを指定します。たとえば、機密データや大きなオブジェクトは省略できます。
    • omitValues: 存在する場合、関連するキーを URL から除外する値。
    • pickKeys: 指定されたキーのみが含まれるようにシリアル化された状態を制限します。
    • プレフィックス: 名前空間のすべてのクエリ パラメーターにプレフィックスを追加します。
    • url: 同期するベース URL。通常はアプリのコンテキストから派生します。

利点

  • 同一の useState API: 既存のコンポーネントと簡単に統合できます。
  • SEO フレンドリー: 状態に依存するビューが共有可能およびブックマーク可能な URL に確実に反映されます。
  • 更新のデバウンス: スライダーやテキスト ボックスなど、急速に変化する入力に対する過剰なクエリ更新を防ぎます。
  • クリーンな URL: kebabCase やomitKeys などのオプションにより、クエリ文字列が読みやすく関連性のある状態に保たれます。
  • 状態のハイドレーション: コンポーネントのマウント時に URL から状態を自動的に初期化し、ディープリンクをシームレスにします。
  • どこでも動作: サーバー側のレンダリングとクライアント側のナビゲーションをサポートし、アプリケーション全体で一貫した状態を保証します。

実用的なアプリケーション

  • プロパティ リストのフィルター: 共有可能な検索のために、listingTypes などのユーザーが適用したフィルターを同期し、境界を URL にマップします。
  • 動的ビュー: 地図のズーム、中心点、その他のビュー設定がページの更新やリンク後も維持されるようにします。
  • ユーザー設定: ユーザーが選択した設定を URL に保存すると、簡単に共有したりブックマークしたりできます。

app.useStorageState: 永続的な状態

app.useStorageState フックを使用すると、TypeScript を完全にサポートし、localStorage または sessionStorage を使用してブラウザーで状態を保持できます。

// On the server
class UserAPI extends Rpc {
  async searchUsers(query: string, filters: UserFilters) {
    // Validation with Zod
    const validated = searchSchema.parse({ query, filters });
    return this.db.users.search(validated);
  }
}

// On the client
const UserSearch = () => {
  const { rpc } = app.useContext<App.Context>();

  // TypeScript knows exactly what searchUsers accepts and returns!
  const { data, loading, error, fetch: retry } = app.useFetch(
    async (ctx) => ctx.rpc.searchUsers('John', { age: '>18' })
  );
};

永続化オプション

  • デバウンス: ストレージへの保存の頻度を制御します。
  • ストレージ: localStorage と sessionStorage のどちらかを選択します。
  • omitKeys/pickKeys: どのデータを保持するかをきめ細かく制御します。

パフォーマンス

  • デバウンスによる最適化されたアップデート。
  • 自動シリアル化/逆シリアル化。
  • メモリ内キャッシュ。

一般的な使用例

  • 検索履歴
  • お気に入りリスト
  • ユーザー設定
  • フィルター状態
  • 一時的なショッピングカート
  • ドラフトフォーム

app.useDebounce: 周波数制御

リアクティブ値を簡単にデバウンスします:

// pages/api/search.ts
export default async handler = (req, res) => {
  // Configure CORS
  // Validate request
  // Handle errors
  // Serialize response
  // ...100 lines later...
}

// app/search/page.tsx
'use client';
import { useEffect, useState } from 'react';

export default const SearchPage = () => {
  const [search, setSearch] = useState('');
  const [filters, setFilters] = useState({});
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    let timeout;
    const doSearch = async () => {
      setLoading(true);
      try {
        const res = await fetch('/api/search?' + new URLSearchParams({
          q: search,
          ...filters
        }));
        if (!res.ok) throw new Error('Search failed');
        setData(await res.json());
      } catch (err) {
        console.error(err);
      } finally {
        setLoading(false);
      }
    };

    timeout = setTimeout(doSearch, 300);
    return () => clearTimeout(timeout);
  }, [search, filters]);

  // ... rest of the component
}

app.useDistinct: 重複のない状態

型の安全性を維持しながら、一意の値を持つ配列を保持します。

app.useDistinct フックは、値が実際に変更されたときの検出に特化しており、詳細な比較とデバウンスをサポートしています。

// On the server
class UserAPI extends Rpc {
  async searchUsers(query: string, filters: UserFilters) {
    // Validation with Zod
    const validated = searchSchema.parse({ query, filters });
    return this.db.users.search(validated);
  }
}

// On the client
const UserSearch = () => {
  const { rpc } = app.useContext<App.Context>();

  // TypeScript knows exactly what searchUsers accepts and returns!
  const { data, loading, error, fetch: retry } = app.useFetch(
    async (ctx) => ctx.rpc.searchUsers('John', { age: '>18' })
  );
};

主な特長

  1. 個別値の検出:
    • 現在の値と以前の値を追跡します。
    • 基準に基づいて、変更が意味のあるものであるかどうかを自動的に検出します。
  2. 詳しい比較:
    • 複雑なオブジェクトに対して深いレベルでの値の等価性チェックを有効にします。
  3. カスタム比較:
    • 「個別の」変更を構成する内容を定義するカスタム関数をサポートします。
  4. デバウンス:
    • 変更が頻繁に発生する場合、不必要な更新を減らします。

利点

  • 同一の useState API: 既存のコンポーネントと簡単に統合できます。
  • 最適化されたパフォーマンス: 値が大幅に変更されていない場合、不必要な再フェッチや再計算を回避します。
  • UX の強化: 過剰な UI 更新を防ぎ、よりスムーズなインタラクションを実現します。
  • ロジックの簡略化: 状態管理における同等性または重複の手動チェックを排除します。

React Edge のフックは調和して動作するように設計されており、流動的で厳密に型指定された開発エクスペリエンスを提供します。これらを組み合わせることで、はるかに少ないコードで複雑でリアクティブなインターフェイスを作成できます。

React Edge CLI: 指先ひとつでパワーを発揮

React Edge の CLI は、重要なツールを 1 つの直感的なインターフェイスに集めて開発者の作業を簡素化するように設計されました。初心者でも経験豊富な開発者でも、CLI を使用すると、プロジェクトを効率的かつ簡単に構成、開発、テスト、デプロイできます。

主な特長

モジュール式で柔軟なコマンド:

  • build: 環境とモード (開発または運用) を指定するオプションを使用して、アプリとワーカーの両方をビルドします。
  • dev: ローカルまたはリモートの開発サーバーを起動し、アプリまたはワーカーでの個別の作業を可能にします。
  • デプロイ: Cloudflare Workers と Cloudflare R2 の総合力を活用して高速かつ効率的なデプロイを可能にし、エッジ インフラストラクチャのパフォーマンスとスケーラビリティを確保します。
  • logs: ワーカーのログをターミナルで直接監視します。
  • lint: 自動修正のサポートにより、Prettier および ESLint の実行を自動化します。
  • test: Vitest を使用してオプションのカバレッジでテストを実行します。
  • type-check: プロジェクト全体で TypeScript の入力を検証します。

実際の使用例

React Edge を使用した最初の運用アプリケーションがすでに公開されていることを誇りに思います。これはブラジルの不動産会社、Lopes Imóveis であり、フレームワークのパフォーマンスと柔軟性の恩恵をすでに享受しています。

Web サイトでは、検索を最適化し、よりスムーズなユーザー エクスペリエンスを提供するために、プロパティがキャッシュに読み込まれます。非常に動的なサイトであるため、ルート キャッシュでは stale-while-revalidate 戦略と組み合わせて、わずか 10 秒の TTL が使用されます。これにより、バックグラウンドでの再検証中であっても、サイトは優れたパフォーマンスで更新されたデータを配信できるようになります。

さらに、同様のプロパティの推奨事項がバックグラウンドで効率的かつ非同期的に計算され、統合された RPC キャッシュ システムを使用して Cloudflare のキャッシュに直接保存されます。このアプローチにより、後続のリクエストの応答時間が短縮され、推奨事項のクエリがほぼ瞬時に行われます。すべてのイメージは Cloudflare R2 に保存され、外部プロバイダーに依存せずにスケーラブルな分散ストレージを提供します。

From Next.js to React Edge with Cloudflare Workers: A Story of Liberation
From Next.js to React Edge with Cloudflare Workers: A Story of Liberation

近いうちに、Easy Auth の大規模な自動マーケティング プロジェクトも立ち上げ、このテクノロジーの可能性をさらに実証する予定です。

結論

それで、親愛なる読者の皆さん、React Edge の世界を巡るこの旅も終わりに達しました! Basic や Bearer などのシンプルな認証オプションや、開発者の一日をより幸せにするその他のトリックなど、探索すべき素晴らしい機能がまだたくさんあることはわかっています。でもちょっと待ってください!将来的には、これらの各機能を詳しく掘り下げる詳細な記事を提供する予定です。

ネタバレ注意: もうすぐ、React Edge はオープンソースになり、適切に文書化されます。開発、仕事、執筆、そして少しの社会生活のバランスをとるのは簡単ではありませんが、特に Cloudflare のインフラストラクチャによって提供される途方もないスピードで、この驚異が実際に動作しているのを見るときの興奮が、私を前進させる原動力です。最高のものはまだ来ていないので、楽しみにしていてください! ?

それまでの間、今すぐ探索とテストを開始したい場合は、パッケージはすでに NPM で利用可能です: React Edge on NPM..

私のメールアドレスは feliperohdee@gmail.com です。いつでもフィードバックをお待ちしています。これはこの旅の始まりにすぎません。提案や建設的な批判は大歓迎です。読んだ内容が気に入ったら、友人や同僚と共有し、さらなる最新情報をお待ちください。ここまで読んでいただきありがとうございました。また次回お会いしましょう! ???

以上がNext.js から Cloudflare ワーカーを使用した React Edge へ: 解放の物語の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。