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

Next.js から Cloudflare Workers を使用した React Edge へ: 解放ストーリー

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

最後の一滴

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

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

しかし、私にすべてを本当に考え直させたのは、Next.js が取っている方向性でした。使用クライアント、使用サーバーの導入により、理論上は開発が簡素化されるはずですが、実際には管理がさらに複雑になります。まるで 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 をハンドルします
  • クライアント/サーバー間の入力を管理
  • 認証ルールを手動で処理する
  • 国際化の実施方法を管理する

そして最も良い点は、このすべてがサーバーとクライアントの両方で機能することです。use client または use server で何かをマークする必要はありません。フレームワークはコンテキストに基づいて何をすべきかを認識します。行きましょうか?

型付き RPC の魔法

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

// No servidor
class UserAPI extends Rpc {
  async searchUsers(query: string, filters: UserFilters) {
    // Validação com Zod
    const validated = searchSchema.parse({ query, filters });
    return this.db.users.search(validated);
  }
}

// No cliente
const UserSearch = () => {
  const { rpc } = app.useContext<App.Context>();

  // TypeScript sabe exatamente o que searchUsers aceita e retorna!
  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) => {
  // Configurar CORS
  // Validar request
  // Tratar erros
  // Serializar resposta
  // ...100 linhas depois...
}

// 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]);

  // ... resto do componente
}

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

データ取得の再考

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

  • SSR 中にデータをサーバーにプリロードします
  • ちらつきなしでクライアントに自動的に潤いを与えます
  • クライアントとサーバーの間で完全な入力を維持します
  • スマート デバウンスで反応性をサポート
  • 同一の通話を自動的に多重化します
  • プログラムによる更新とポーリングを許可します

これを実際に見てみましょう:

// No servidor
class UserAPI extends Rpc {
  async searchUsers(query: string, filters: UserFilters) {
    // Validação com Zod
    const validated = searchSchema.parse({ query, filters });
    return this.db.users.search(validated);
  }
}

// No cliente
const UserSearch = () => {
  const { rpc } = app.useContext<App.Context>();

  // TypeScript sabe exatamente o que searchUsers aceita e retorna!
  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) => {
  // Configurar CORS
  // Validar request
  // Tratar erros
  // Serializar resposta
  // ...100 linhas depois...
}

// 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]);

  // ... resto do componente
}

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

最も印象的な部分の 1 つは、useFetch による SSR の処理方法です。

// Primeiro, definimos nossa API no servidor
class PropertiesAPI extends Rpc {
 async searchProperties(filters: PropertyFilters) {
   const results = await this.db.properties.search(filters);
   // Cache automático por 5 minutos
   return this.createResponse(results, {
     cache: { ttl: 300, tags: ['properties'] }
   });
 }

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

// Agora, no cliente, a mágica acontece
const PropertySearch = () => {
 const [filters, setFilters] = useState<PropertyFilters>({
   price: { min: 100000, max: 500000 },
   bedrooms: 2
 });

 // Busca reativa com debounce inteligente
 const { 
   data: searchResults,
   loading: searchLoading,
   error: searchError
 } = app.useFetch(
   async (ctx) => ctx.rpc.searchProperties(filters),
   {
     // Quando filters muda, refaz a busca
     deps: [filters],
     // Mas espera 300ms de 'silêncio' antes de buscar
     depsDebounce: {
       filters: 300
     }
   }
 );

 // Agora, vamos buscar os detalhes das propriedades encontradas
 const {
   data: propertyDetails,
   loading: detailsLoading,
   fetch: refreshDetails
 } = app.useFetch(
   async (ctx) => {
     if (!searchResults?.length) return null;

     // Isso parece fazer múltiplas chamadas, mas...
     return ctx.rpc.batch([
       // Na verdade, tudo é multiplexado em uma única requisição!
       ...searchResults.map(result => 
         ctx.rpc.getPropertyDetails(result.id)
       )
     ]);
   },
   {
     // Atualiza sempre que searchResults mudar
     deps: [searchResults]
   }
 );

 // Interface bonita e responsiva
 return (
   <div>
     <FiltersPanel 
       value={filters}
       onChange={setFilters}
       disabled={searchLoading}
     />

     {searchError && (
       <Alert status='error'>
         Erro na busca: {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) => {
    // Mesmo que você faça 100 chamadas idênticas...
    return ctx.rpc.batch([
      ctx.rpc.getProperty('123'),
      ctx.rpc.getProperty('123'), // mesma chamada
      ctx.rpc.getProperty('456'),
      ctx.rpc.getProperty('456'), // mesma chamada
    ]);
  });

  // Mas na realidade:
  // 1. O batch agrupa todas as chamadas em UMA única requisição HTTP
  // 2. Chamadas idênticas são deduplicas automaticamente
  // 3. O resultado é distribuído corretamente para cada posição do array
  // 4. A tipagem é mantida para cada resultado individual!


  // Entao..
  // 1. getProperty('123')
  // 2. getProperty('456')
  // E os resultados são distribuídos para todos os chamadores!
};

RPC API 階層

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

const ProductPage = ({ productId }: Props) => {
  const { data, loaded, loading, error } = app.useFetch(
    async (ctx) => ctx.rpc.getProduct(productId),
    {
      // Controle fino de quando executar
      shouldFetch: ({ worker, loaded }) => {
        // No worker (SSR): sempre busca
        if (worker) return true;
        // No cliente: só busca se não tiver dados
        return !loaded;
      }
    }
  );

  // No servidor:
  // 1. useFetch faz a chamada RPC
  // 2. Dados são serializados e enviados ao cliente
  // 3. Componente renderiza com os dados

  // No cliente:
  // 1. Componente hidrata com os dados do servidor
  // 2. Não faz nova chamada (shouldFetch retorna false)
  // 3. Se necessário, pode refazer a chamada com data.fetch()

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

階層構造の利点

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

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

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

理にかなった i18n システム

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

class PaymentsAPI extends Rpc {
 // Propriedades nunca são expostas
 private stripe = new Stripe(process.env.STRIPE_KEY);

 // Métodos começando com $ são privados
 private async $validateCard(card: CardInfo) {
   return await this.stripe.cards.validate(card);
 }

 // Métodos começando com _ também são privados
 private async _processPayment(amount: number) {
   return await this.stripe.charges.create({ amount });
 }

 // Este método é público e acessível via RPC
 async createPayment(orderData: OrderData) {
   // Validação interna usando método privado
   const validCard = await this.$validateCard(orderData.card);
   if (!validCard) {
     throw new HttpError(400, 'Invalid card');
   }

   // Processamento usando outro método privado
   const payment = await this._processPayment(orderData.amount);
   return payment;
 }
}

// No cliente:
const PaymentForm = () => {
 const { rpc } = app.useContext<App.Context>();

 // ✅ Isso funciona
 const handleSubmit = () => rpc.createPayment(data);

 // ❌ Isso não é possível - métodos privados não são expostos
 const invalid1 = () => rpc.$validateCard(data);
 const invalid2 = () => rpc._processPayment(100);

 // ❌ Isso também não funciona - propriedades não são expostas
 const invalid3 = () => rpc.stripe;
};

コードでの使用法:

// APIs aninhadas para melhor organização
class UsersAPI extends Rpc {
  // Subclasse para gerenciar preferences
  preferences = new UserPreferencesAPI();
  // Subclasse para gerenciar notificações
  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 {
  // Métodos privados continuam privados
  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);
    }
  }
}

// No cliente:
const UserProfile = () => {
  const { rpc } = app.useContext<App.Context>();

  const { data: profile } = app.useFetch(
    async (ctx) => {
      // Chamadas aninhadas são totalmente tipadas
      const [user, theme, notificationSettings] = await ctx.rpc.batch([
        // Método da classe principal
        ctx.rpc.getProfile('123'),
        // Método da subclasse de preferências
        ctx.rpc.preferences.getTheme('123'),
        // Método da subclasse de notificações
        ctx.rpc.notifications.getSettings('123')
      ]);

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

  // ❌ Métodos privados continuam inacessíveis
  const invalid = () => rpc.notifications.$sendPush('123', 'hello');
};

ゼロ構成

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

// No servidor
class UserAPI extends Rpc {
  async searchUsers(query: string, filters: UserFilters) {
    // Validação com Zod
    const validated = searchSchema.parse({ query, filters });
    return this.db.users.search(validated);
  }
}

// No cliente
const UserSearch = () => {
  const { rpc } = app.useContext<App.Context>();

  // TypeScript sabe exatamente o que searchUsers aceita e retorna!
  const { data, loading, error, fetch: retry } = app.useFetch(
    async (ctx) => ctx.rpc.searchUsers('John', { age: '>18' })
  );
};

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

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

完全な認証システムの実装がいかに簡単かをご覧ください:

// pages/api/search.ts
export default async handler = (req, res) => {
  // Configurar CORS
  // Validar request
  // Tratar erros
  // Serializar resposta
  // ...100 linhas depois...
}

// 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]);

  // ... resto do componente
}

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

// Primeiro, definimos nossa API no servidor
class PropertiesAPI extends Rpc {
 async searchProperties(filters: PropertyFilters) {
   const results = await this.db.properties.search(filters);
   // Cache automático por 5 minutos
   return this.createResponse(results, {
     cache: { ttl: 300, tags: ['properties'] }
   });
 }

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

// Agora, no cliente, a mágica acontece
const PropertySearch = () => {
 const [filters, setFilters] = useState<PropertyFilters>({
   price: { min: 100000, max: 500000 },
   bedrooms: 2
 });

 // Busca reativa com debounce inteligente
 const { 
   data: searchResults,
   loading: searchLoading,
   error: searchError
 } = app.useFetch(
   async (ctx) => ctx.rpc.searchProperties(filters),
   {
     // Quando filters muda, refaz a busca
     deps: [filters],
     // Mas espera 300ms de 'silêncio' antes de buscar
     depsDebounce: {
       filters: 300
     }
   }
 );

 // Agora, vamos buscar os detalhes das propriedades encontradas
 const {
   data: propertyDetails,
   loading: detailsLoading,
   fetch: refreshDetails
 } = app.useFetch(
   async (ctx) => {
     if (!searchResults?.length) return null;

     // Isso parece fazer múltiplas chamadas, mas...
     return ctx.rpc.batch([
       // Na verdade, tudo é multiplexado em uma única requisição!
       ...searchResults.map(result => 
         ctx.rpc.getPropertyDetails(result.id)
       )
     ]);
   },
   {
     // Atualiza sempre que searchResults mudar
     deps: [searchResults]
   }
 );

 // Interface bonita e responsiva
 return (
   <div>
     <FiltersPanel 
       value={filters}
       onChange={setFilters}
       disabled={searchLoading}
     />

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

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

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

  1. 定型文ゼロ

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

    • トークンは自動的に暗号化されます
    • Cookie は安全で httpOnly です
    • 自動再検証
  3. 入力を完了してください

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

const PropertyListingPage = () => {
  const { data } = app.useFetch(async (ctx) => {
    // Mesmo que você faça 100 chamadas idênticas...
    return ctx.rpc.batch([
      ctx.rpc.getProperty('123'),
      ctx.rpc.getProperty('123'), // mesma chamada
      ctx.rpc.getProperty('456'),
      ctx.rpc.getProperty('456'), // mesma chamada
    ]);
  });

  // Mas na realidade:
  // 1. O batch agrupa todas as chamadas em UMA única requisição HTTP
  // 2. Chamadas idênticas são deduplicas automaticamente
  // 3. O resultado é distribuído corretamente para cada posição do array
  // 4. A tipagem é mantida para cada resultado individual!


  // Entao..
  // 1. getProperty('123')
  // 2. getProperty('456')
  // E os resultados são distribuídos para todos os chamadores!
};

共有ストア

React Edge の最も強力な機能の 1 つは、ワーカーとクライアントの間で状態を安全に共有する機能です。これがどのように機能するかを見てみましょう:

const ProductPage = ({ productId }: Props) => {
  const { data, loaded, loading, error } = app.useFetch(
    async (ctx) => ctx.rpc.getProduct(productId),
    {
      // Controle fino de quando executar
      shouldFetch: ({ worker, loaded }) => {
        // No worker (SSR): sempre busca
        if (worker) return true;
        // No cliente: só busca se não tiver dados
        return !loaded;
      }
    }
  );

  // No servidor:
  // 1. useFetch faz a chamada RPC
  // 2. Dados são serializados e enviados ao cliente
  // 3. Componente renderiza com os dados

  // No cliente:
  // 1. Componente hidrata com os dados do servidor
  // 2. Não faz nova chamada (shouldFetch retorna false)
  // 3. Se necessário, pode refazer a chamada com data.fetch()

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

仕組み

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

利点

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

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

React Edge のルーティング システムは Hono からインスピレーションを得ていますが、SSR のスーパーパワーを備えています。

// No servidor
class UserAPI extends Rpc {
  async searchUsers(query: string, filters: UserFilters) {
    // Validação com Zod
    const validated = searchSchema.parse({ query, filters });
    return this.db.users.search(validated);
  }
}

// No cliente
const UserSearch = () => {
  const { rpc } = app.useContext<App.Context>();

  // TypeScript sabe exatamente o que searchUsers aceita e retorna!
  const { data, loading, error, fetch: retry } = app.useFetch(
    async (ctx) => ctx.rpc.searchUsers('John', { age: '>18' })
  );
};

主な特長

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

利点

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

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

React Edge には、JSON データとページ全体の両方で機能する強力なキャッシュ システムがあります。

// pages/api/search.ts
export default async handler = (req, res) => {
  // Configurar CORS
  // Validar request
  // Tratar erros
  // Serializar resposta
  // ...100 linhas depois...
}

// 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]);

  // ... resto do componente
}

主な特長

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

利点

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

リンク: 前向きな考え方のコンポーネント

リンク コンポーネントは、クライアント側でリソースをプリロードするためのインテリジェントでパフォーマンスの高いソリューションであり、ユーザーにとってよりスムーズで高速なナビゲーションを保証します。プリフェッチ機能は、リンク上にカーソルを置くとアクティブになり、ユーザーの非アクティブな瞬間を利用して、宛先データを事前にリクエストします。

それはどのように機能しますか?

  1. 条件付きプリフェッチ: プリフェッチ属性 (デフォルトでアクティブ) は、プリロードを実行するかどうかを制御します。

  2. スマート キャッシュ: セットは、すでにプリロードされたリンクを保存するために使用され、冗長な呼び出しを回避します。

  3. Mouse Enter: ユーザーがリンク上にカーソルを置くと、handleMouseEnter 関数はプリロードが必要かどうかをチェックし、必要な場合は宛先へのフェッチ リクエストを開始します。

  4. エラー セーフ: リクエスト内の失敗はすべて抑制され、コンポーネントの動作が一時的なネットワーク エラーによって影響を受けないことが保証されます。

// No servidor
class UserAPI extends Rpc {
  async searchUsers(query: string, filters: UserFilters) {
    // Validação com Zod
    const validated = searchSchema.parse({ query, filters });
    return this.db.users.search(validated);
  }
}

// No cliente
const UserSearch = () => {
  const { rpc } = app.useContext<App.Context>();

  // TypeScript sabe exatamente o que searchUsers aceita e retorna!
  const { data, loading, error, fetch: retry } = app.useFetch(
    async (ctx) => ctx.rpc.searchUsers('John', { age: '>18' })
  );
};

ユーザーが「About Us」リンクの上にマウスを置くと、コンポーネントは /about ページからデータのプリロードを開始し、ほぼ瞬時に移行します。素晴らしいアイデアですよね?しかし、私はそれをreact.devドキュメントで見ました。

app.useContext: Edge へのゲートウェイ

app.useContext は React Edge の基本的なフックであり、ワーカー コンテキスト全体へのアクセスを提供します。

// pages/api/search.ts
export default async handler = (req, res) => {
  // Configurar CORS
  // Validar request
  // Tratar erros
  // Serializar resposta
  // ...100 linhas depois...
}

// 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]);

  // ... resto do componente
}

app.useContext の主な機能

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

なぜ強力なのか

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

app.useUrlState: URLと同期した状態

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

// Primeiro, definimos nossa API no servidor
class PropertiesAPI extends Rpc {
 async searchProperties(filters: PropertyFilters) {
   const results = await this.db.properties.search(filters);
   // Cache automático por 5 minutos
   return this.createResponse(results, {
     cache: { ttl: 300, tags: ['properties'] }
   });
 }

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

// Agora, no cliente, a mágica acontece
const PropertySearch = () => {
 const [filters, setFilters] = useState<PropertyFilters>({
   price: { min: 100000, max: 500000 },
   bedrooms: 2
 });

 // Busca reativa com debounce inteligente
 const { 
   data: searchResults,
   loading: searchLoading,
   error: searchError
 } = app.useFetch(
   async (ctx) => ctx.rpc.searchProperties(filters),
   {
     // Quando filters muda, refaz a busca
     deps: [filters],
     // Mas espera 300ms de 'silêncio' antes de buscar
     depsDebounce: {
       filters: 300
     }
   }
 );

 // Agora, vamos buscar os detalhes das propriedades encontradas
 const {
   data: propertyDetails,
   loading: detailsLoading,
   fetch: refreshDetails
 } = app.useFetch(
   async (ctx) => {
     if (!searchResults?.length) return null;

     // Isso parece fazer múltiplas chamadas, mas...
     return ctx.rpc.batch([
       // Na verdade, tudo é multiplexado em uma única requisição!
       ...searchResults.map(result => 
         ctx.rpc.getPropertyDetails(result.id)
       )
     ]);
   },
   {
     // Atualiza sempre que searchResults mudar
     deps: [searchResults]
   }
 );

 // Interface bonita e responsiva
 return (
   <div>
     <FiltersPanel 
       value={filters}
       onChange={setFilters}
       disabled={searchLoading}
     />

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

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

パラメータ

  1. 初期状態

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

    • debounce: 状態が変化した後に URL を更新する速度を制御します。
    • kebabCase: URL にシリアル化するときに状態キーを kebab-case に変換します。
    • omitKeys: URL から除外するキーを指定します。
    • omitValues: 存在する場合、関連するキーを URL から除外する値。
    • pickKeys: シリアル化された状態を制限して、特定のキーのみを含めます。
    • プレフィックス: すべてのクエリ パラメーターにプレフィックスを追加します。
    • url: 同期用のベース URL。通常はアプリケーション コンテキストから派生します。

利点

  • SEO フレンドリー: 状態に依存するビューが共有可能な URL に確実に反映されます。
  • デバウンスによる更新: 急速に変化する入力に対する過度のクエリ更新を防ぎます。
  • クリーンな URL: kebabCase やomitKeys などのオプションにより、クエリ文字列を読み取り可能に保ちます。
  • State Hydration: コンポーネントのアセンブル時に URL の状態を自動的に初期化します。
  • すべての環境で動作します: サーバー側のレンダリングとクライアント側のブラウジングをサポートします。

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

  • リスティングのフィルター: ユーザーが適用したフィルターを URL と同期します。
  • ダイナミック ビュー: 地図のズーム、中心点、その他の設定が保持されるようにします。
  • ユーザー設定: 選択した設定を共有用の URL に保存します。

app.useStorageState: 永続的な状態

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

// No servidor
class UserAPI extends Rpc {
  async searchUsers(query: string, filters: UserFilters) {
    // Validação com Zod
    const validated = searchSchema.parse({ query, filters });
    return this.db.users.search(validated);
  }
}

// No cliente
const UserSearch = () => {
  const { rpc } = app.useContext<App.Context>();

  // TypeScript sabe exatamente o que searchUsers aceita e retorna!
  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) => {
  // Configurar CORS
  // Validar request
  // Tratar erros
  // Serializar resposta
  // ...100 linhas depois...
}

// 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]);

  // ... resto do componente
}

app.useDistinct: 重複のない状態

次のように入力して一意の値の配列を維持します:

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

// No servidor
class UserAPI extends Rpc {
  async searchUsers(query: string, filters: UserFilters) {
    // Validação com Zod
    const validated = searchSchema.parse({ query, filters });
    return this.db.users.search(validated);
  }
}

// No cliente
const UserSearch = () => {
  const { rpc } = app.useContext<App.Context>();

  // TypeScript sabe exatamente o que searchUsers aceita e retorna!
  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 は、重要なツールを単一の直感的なインターフェイスにまとめることにより、開発者の作業を簡素化するように設計されました。初心者でも専門家でも、CLI を使用すると、プロジェクトを手間をかけずに効率的に構成、開発、テスト、デプロイできます。

主な機能

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

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

本番環境の使用例

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

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

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

De Next.js a React Edge com Cloudflare Workers: Uma História de Libertação
De Next.js a React Edge com Cloudflare Workers: Uma História de Libertação

そして間もなく、Easy Auth の巨大な自動マーケティング プロジェクトも立ち上げ、このテクノロジーの可能性をさらに実証する予定です。

結論

それで、親愛なる読者の皆さん、React Edge の世界を巡るこの冒険も終わりに来ました! Basic や Bearer などの最も単純な認証や、開発者の日常生活をより幸せにするその他の小さな秘密など、探索すべき信じられないほどのことがまだたくさんあることを私は知っています。でも、落ち着いてください!将来的には、これらの各機能を真っ先に掘り下げる、より詳細な記事を提供する予定です。

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

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

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

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

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