Maison >interface Web >js tutoriel >De Next.js à React Edge avec Cloudflare Workers : une histoire de libération

De Next.js à React Edge avec Cloudflare Workers : une histoire de libération

Susan Sarandon
Susan Sarandonoriginal
2024-11-20 14:26:17918parcourir

Index rapide

  • La goutte d'eau finale
  • L'Alternative avec Cloudflare ?
  • React Edge : le framework React né de la douleur de chaque développeur (ou presque)
    • La magie du RPC typé
    • Le pouvoir d'utilisationRécupérer : là où la magie opère
  • Au-delà de useFetch : L'Arsenal complet
    • RPC : L'art de la communication client-serveur
    • Un système i18n qui a du sens
    • Authentification JWT qui « fonctionne tout simplement »
    • La boutique partagée
    • Routage élégant
    • Cache distribué avec Edge Cache
  • Lien : La composante qui anticipe
  • app.useContext : le portail vers la périphérie
  • app.useUrlState : État synchronisé avec l'URL
  • app.useStorageState : État persistant
  • app.useDebounce : contrôle de fréquence
  • app.useDistinct : État sans doublons
  • La CLI React Edge : la puissance à portée de main
  • Conclusion

La paille finale

Tout a commencé par une facture Vercel. Non, en fait, cela a commencé bien plus tôt, avec de petites frustrations qui se sont accumulées. La nécessité de payer pour des fonctionnalités de base comme la protection DDoS, des journaux plus détaillés, ou même un pare-feu décent, la création de files d'attente, etc. Le sentiment d'être piégé dans un verrouillage fournisseur de plus en plus coûteux.

"Et le pire de tout : nos précieux en-têtes SEO ont tout simplement arrêté de s'afficher sur le serveur dans une application utilisant le routeur de pages. Un vrai casse-tête pour tout développeur ! ?"

Mais ce qui m'a vraiment fait repenser tout, c'est la direction que prenait Next.js. L’introduction des directives use client, use server qui, en théorie, devraient simplifier le développement mais, en pratique, a ajouté une autre couche de complexité à gérer. C'était comme revenir à l'époque PHP, marquer les fichiers avec des directives pour dicter où ils devaient s'exécuter.

Et ça ne s’arrête pas là. L'App Router : une idée intéressante mais mise en œuvre d'une manière qui a créé un cadre presque entièrement nouveau au sein de Next.js. Soudain, il y avait deux manières complètement différentes de faire la même chose – l’« ancienne » et la « nouvelle » – avec des comportements subtilement différents et des pièges cachés.

L'alternative avec Cloudflare ?

C'est à ce moment-là que j'ai compris : pourquoi ne pas tirer parti de l'incroyable infrastructure de Cloudflare avec Workers fonctionnant en périphérie, R2 pour le stockage, KV pour les données distribuées... Avec, bien sûr, l'incroyable protection DDoS, le CDN global, le pare-feu. , les règles de page et le routage, et tout ce que Cloudflare a à offrir.

Et le meilleur : un modèle de tarification équitable où vous payez ce que vous utilisez, sans surprise.

C'est ainsi qu'est né React Edge. Un framework qui ne vise pas à réinventer la roue mais plutôt à offrir une expérience de développement vraiment simple et moderne.

React Edge : le framework React né de la douleur de chaque développeur (ou presque)

Quand j'ai commencé à développer React Edge, j'avais un objectif clair : créer un framework qui avait du sens. Plus besoin de lutter avec des directives déroutantes, plus besoin de payer des frais exorbitants pour les fonctionnalités de base et, plus important encore, plus besoin de gérer la complexité artificielle causée par la séparation client/serveur. Je voulais de la rapidité : un framework qui offre des performances sans sacrifier la simplicité. Tirant parti de ma connaissance de l'API de React et de mes années d'expérience en tant que développeur JavaScript et Golang, je savais exactement comment gérer les flux et le multiplexage pour optimiser le rendu et la gestion des données.

Cloudflare Workers, avec sa puissante infrastructure et sa présence mondiale, a fourni l'environnement idéal pour explorer ces possibilités. Je voulais quelque chose de véritablement hybride, et cette combinaison d'outils et d'expérience a donné vie à React Edge : un framework qui résout des problèmes du monde réel avec des solutions modernes et efficaces.

React Edge introduit une approche révolutionnaire du développement de React. Imaginez écrire une classe sur le serveur et l'appeler directement depuis le client, avec une sécurité de type totale et aucune configuration. Imaginez un système de mise en cache distribué qui « fonctionne tout simplement », permettant l'invalidation par des balises ou des préfixes. Imaginez partager l’état de manière transparente et sécurisée entre le serveur et le client. Ajoutez une authentification simplifiée, un système d'internationalisation efficace, une CLI, et bien plus encore.

Sa communication RPC semble presque magique : vous écrivez des méthodes dans une classe et les appelez depuis le client comme si elles étaient locales. Le système de multiplexage intelligent garantit que même si plusieurs composants effectuent le même appel, une seule requête est envoyée au serveur. La mise en cache éphémère évite les requêtes répétées inutiles et tout fonctionne de manière transparente à la fois sur le serveur et sur le client.

L'une des fonctionnalités les plus puissantes est le hook app.useFetch, qui unifie l'expérience de récupération de données. Sur le serveur, il précharge les données pendant SSR ; sur le client, il s'hydrate automatiquement avec ces données et prend en charge les mises à jour à la demande. Avec la prise en charge de l'interrogation automatique et de la réactivité basée sur les dépendances, la création d'interfaces dynamiques n'a jamais été aussi simple.

Mais ce n’est pas tout. Le framework offre un système de routage puissant (inspiré du fantastique Hono), une gestion d'actifs intégrée avec Cloudflare R2 et une manière élégante de gérer les erreurs via la classe HttpError. Les middlewares peuvent facilement envoyer des données au client via un magasin partagé, et tout est automatiquement masqué pour des raisons de sécurité.

La partie la plus impressionnante ? Presque tout le code du framework est hybride. Il n’existe pas de version « client » et de version « serveur » : le même code fonctionne dans les deux environnements, s’adaptant automatiquement au contexte. Le client reçoit uniquement ce dont il a besoin, ce qui rend le package final extrêmement optimisé.

Et cerise sur le gâteau : tout cela fonctionne sur l'infrastructure périphérique Cloudflare Workers, offrant des performances exceptionnelles à un coût équitable. Pas de factures surprises, pas de fonctionnalités de base enfermées dans des forfaits d'entreprise coûteux : juste un cadre solide qui vous permet de vous concentrer sur ce qui compte vraiment : créer des applications étonnantes. De plus, React Edge exploite l'écosystème de Cloudflare, notamment les files d'attente, les objets durables, le stockage KV, etc., fournissant une base robuste et évolutive pour vos applications.

Vite a été utilisé comme base pour l'environnement de développement, les tests et le processus de construction. Avec sa vitesse impressionnante et son architecture moderne, Vite permet un flux de travail agile et efficace. Il accélère non seulement le développement, mais optimise également le processus de construction, garantissant une compilation rapide et précise. Sans aucun doute, Vite était le choix parfait pour React Edge.

Repenser le développement de React à l'ère de l'Edge Computing

Vous êtes-vous déjà demandé à quoi cela ressemblerait de développer des applications React sans vous soucier de la barrière client/serveur ? Sans mémoriser des dizaines de directives comme use client ou use server ? Mieux encore : et si vous pouviez appeler les fonctions du serveur comme si elles étaient locales, avec une saisie complète et aucune configuration ?

Avec React Edge, vous n'avez plus besoin de :

  • Créer des routes API distinctes
  • Gérer manuellement l'état de chargement/erreur
  • Mettez en œuvre vous-même l'anti-rebond
  • Vous vous inquiétez de la sérialisation/désérialisation
  • Traitez avec CORS
  • Gérer la saisie entre client/serveur
  • Gérer les règles d'authentification manuellement
  • Lutte avec la configuration de l'internationalisation

Et le meilleur : tout cela fonctionne de manière transparente à la fois sur le serveur et sur le client sans rien marquer comme utiliser le client ou utiliser le serveur. Le framework sait automatiquement quoi faire en fonction du contexte. On y plonge ?

La magie du RPC typé

Imaginez pouvoir faire ceci :

// 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' })
  );
};

Comparez cela avec 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
}

Le pouvoir de useFetch : là où la magie opère

Repenser la récupération de données

Oubliez tout ce que vous savez sur la récupération de données dans React. Le hook app.useFetch de React Edge introduit une approche complètement nouvelle et puissante. Imaginez un crochet qui :

  • Précharge les données sur le serveur pendant SSR.
  • Hydrate automatiquement les données sur le client sans scintillement.
  • Maintient la saisie complète entre le client et le serveur.
  • Prend en charge la réactivité avec un anti-rebond intelligent.
  • Multiplexe automatiquement les appels identiques.
  • Active les mises à jour et les sondages programmatiques.

Voyons-le en action :

// 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' })
  );
};

La magie du multiplexage

L'exemple ci-dessus cache une fonctionnalité puissante : le multiplexage intelligent. Lorsque vous utilisez ctx.rpc.batch, React Edge non seulement regroupe les appels, mais il déduplique également automatiquement les appels identiques :

// 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 Hydratation Parfaite

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

Au-delà de useFetch : L'Arsenal complet

RPC : l'art de la communication client-serveur

Sécurité et encapsulation

Le système RPC de React Edge est conçu dans un souci de sécurité et d'encapsulation. Tout dans une classe RPC n'est pas automatiquement exposé au client :

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!
};

Hiérarchies des API RPC

L'une des fonctionnalités les plus puissantes de RPC est la possibilité d'organiser les API en hiérarchies :

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>
  );
};

Avantages des hiérarchies

L'organisation des API en hiérarchies offre plusieurs avantages :

  • Organisation logique : regroupez intuitivement les fonctionnalités liées.
  • Espacement de noms naturel : évitez les conflits de noms avec des chemins clairs (par exemple, users.preferences.getTheme).
  • Encapsulation : gardez les méthodes d'assistance privées à chaque niveau.
  • Maintenabilité : chaque sous-classe peut être maintenue et testée indépendamment.
  • Saisie complète : TypeScript comprend toute la hiérarchie.

Le système RPC de React Edge rend la communication client-serveur si naturelle que vous oublierez presque que vous passez des appels à distance. Avec la possibilité d'organiser les API en hiérarchies, vous pouvez créer des structures complexes tout en gardant votre code propre et sécurisé.

Un système i18n qui a du sens

React Edge introduit un système d'internationalisation élégant et flexible qui prend en charge l'interpolation variable et le formatage complexe sans recourir à des bibliothèques lourdes.

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;
};

Utilisation dans le code :

// 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');
};

Zéro configuration

React Edge détecte et charge automatiquement vos traductions. Il permet même de sauvegarder sans effort les préférences de l’utilisateur dans les cookies. Mais bien sûr, vous vous y attendriez, n’est-ce pas ?

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

Authentification JWT qui « fonctionne tout simplement »

L'authentification a toujours été un problème dans les applications Web. La gestion des jetons JWT, des cookies sécurisés et de la revalidation nécessite souvent beaucoup de code passe-partout. React Edge change complètement cela.

Voici à quel point il est simple de mettre en œuvre un système d'authentification complet :

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

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

Utilisation du client : zéro configuration

// 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;
  }
};

Pourquoi est-ce révolutionnaire ?

  1. Zéro passe-partout

    • Pas de gestion manuelle des cookies
    • Pas besoin d'intercepteurs
    • Aucun jeton d'actualisation manuelle
  2. Sécurité par défaut

    • Les jetons sont automatiquement cryptés
    • Les cookies sont sécurisés et httpOnly
    • Revalidation automatique
  3. Saisie complète

    • La charge utile JWT est saisie
    • Validation Zod intégrée
    • Erreurs d'authentification tapées
  4. Intégration transparente

// 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' })
  );
};

Le magasin partagé

L'une des fonctionnalités les plus puissantes de React Edge est sa capacité à partager l'état en toute sécurité entre le travailleur et le client. Voici comment cela fonctionne :

Exemple d'utilisation du middleware et du magasin

// 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
}

Comment ça marche

  • Données publiques : les données marquées comme publiques sont partagées en toute sécurité avec le client, ce qui les rend facilement accessibles aux composants.
  • Données privées : les données sensibles restent dans l'environnement du travailleur et ne sont jamais exposées au client.
  • Intégration avec le middleware : le middleware peut alimenter le magasin avec des données publiques et privées, garantissant un flux d'informations transparent entre la logique côté serveur et le rendu côté client.

Avantages

  1. Sécurité : des étendues de données publiques et privées distinctes garantissent que les informations sensibles restent protégées.
  2. Commodité : l'accès transparent aux données du magasin simplifie la gestion de l'état entre le travailleur et le client.
  3. Flexibilité : le magasin s'intègre facilement au middleware, permettant des mises à jour dynamiques de l'état basées sur le traitement des demandes.

Routage élégant

Le système de routage de React Edge est inspiré de Hono, mais avec des fonctionnalités améliorées pour 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>
  );
};

Principales fonctionnalités

  • Routes groupées : regroupement logique de routes associées sous un chemin et un middleware partagés.
  • Gestionnaires flexibles : définissez des gestionnaires qui renvoient des pages ou dirigent des réponses API.
  • En-têtes par route : personnalisez les en-têtes HTTP pour les routes individuelles.
  • Mise en cache intégrée : simplifiez les stratégies de mise en cache avec ttl et balises.

Avantages

  1. Cohérence : en regroupant les itinéraires associés, vous garantissez la cohérence des applications middleware et de l'organisation du code.
  2. Évolutivité : le système prend en charge le routage imbriqué et modulaire pour les applications à grande échelle.
  3. Performance : la prise en charge native de la mise en cache garantit des temps de réponse optimaux sans configuration manuelle.

Cache distribué avec cache Edge

React Edge comprend un puissant système de mise en cache qui fonctionne de manière transparente pour les données JSON et les pages entières. Ce système de mise en cache prend en charge le balisage intelligent et l'invalidation basée sur les préfixes, ce qui le rend adapté à un large éventail de scénarios.

Exemple : mise en cache des réponses d'API avec des balises

// 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' })
  );
};

Principales fonctionnalités

  • Invalidation basée sur des balises : les entrées du cache peuvent être regroupées à l'aide de balises, permettant une invalidation facile et sélective lorsque les données changent.
  • Correspondance de préfixe : invalidez plusieurs entrées de cache à l'aide d'un préfixe commun, idéal pour des scénarios tels que les requêtes de recherche ou les données hiérarchiques.
  • Durée de vie (TTL) : définissez les délais d'expiration pour les entrées de cache afin de garantir la fraîcheur des données tout en maintenant des performances élevées.

Avantages

  1. Performances améliorées : réduisez la charge sur les API en diffusant des réponses mises en cache pour les données fréquemment consultées.
  2. Évolutivité : gérez efficacement des ensembles de données à grande échelle et un trafic élevé grâce à un système de mise en cache distribué.
  3. Flexibilité : contrôle précis de la mise en cache, permettant aux développeurs d'optimiser les performances sans sacrifier la précision des données.

Lien : La composante qui anticipe

Le composant Link est une solution intelligente et orientée performances pour précharger les ressources côté client, garantissant une expérience de navigation plus fluide et plus rapide pour les utilisateurs. Sa fonctionnalité de prélecture se déclenche lorsque l'utilisateur survole le lien, profitant des moments d'inactivité pour demander à l'avance les données de destination.

Comment ça marche

  1. Prélecture conditionnelle : L'attribut prefetch (activé par défaut) contrôle si le préchargement est exécuté.
  2. Cache intelligent : un ensemble est utilisé pour stocker des liens déjà préextraits, évitant ainsi les appels de récupération redondants.
  3. Événement Mouse Enter : lorsque l'utilisateur survole le lien, la fonction handleMouseEnter vérifie si le préchargement est nécessaire et, si c'est le cas, lance une demande de récupération pour la destination.
  4. Résilience aux erreurs : tout échec lors de la requête est supprimé, garantissant que le comportement du composant n'est pas affecté par des problèmes de réseau temporaires.

Exemple d'utilisation

// 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
}

Lorsque l'utilisateur survole le lien « À propos de nous », le composant commence à précharger les données de la page /about, assurant une transition presque instantanée. Idée géniale, n'est-ce pas ? Inspiré de la documentation de React.dev.

app.useContext : le portail vers la périphérie

Le hook app.useContext est la pierre angulaire de React Edge, offrant un accès transparent à l'ensemble du contexte du travailleur. Il fournit une interface puissante pour gérer le routage, l'état, les appels RPC, etc.

Exemple : utilisation de app.useContext dans un tableau de bord

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

Principales fonctionnalités de app.useContext

  • Gestion des itinéraires : accédez sans effort à l'itinéraire correspondant, à ses paramètres et aux chaînes de requête.
  • Intégration RPC : effectuez des appels RPC tapés et sécurisés directement depuis le client sans configuration supplémentaire.
  • Accès au magasin partagé : récupérez ou définissez des valeurs dans l'état partagé travailleur-client avec un contrôle complet sur la visibilité (publique/privée).
  • Accès URL universel : accédez facilement à l'URL complète de la demande en cours pour le rendu dynamique et les interactions.

Pourquoi c'est puissant

Le hook app.useContext comble le fossé entre le travailleur et le client. Il vous permet de créer des fonctionnalités qui reposent sur un état partagé, une récupération de données sécurisée et un rendu contextuel sans passe-partout. Cela simplifie les applications complexes, les rendant plus faciles à maintenir et plus rapides à développer.

app.useUrlState : état synchronisé avec l'URL

Le hook app.useUrlState maintient l'état de votre application synchronisé avec les paramètres de requête d'URL, offrant un contrôle précis sur ce qui est inclus dans l'URL, comment l'état est sérialisé et quand il est mis à jour.

// 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' })
  );
};

Paramètres

  1. État initial

    • Un objet définissant la structure et les valeurs par défaut de votre état.
  2. Options :

    • anti-rebond : contrôle la rapidité avec laquelle l'URL est mise à jour après un changement d'état. Utile pour éviter les mises à jour excessives.
    • kebabCase : convertit les clés d'état en kebab-case lors de la sérialisation vers l'URL (par exemple, filter.locations → filter-locations).
    • omitKeys : Spécifie les clés à exclure de l'URL. Par exemple, les données sensibles ou les objets volumineux peuvent être omis.
    • omitValues : valeurs qui, lorsqu'elles sont présentes, excluront la clé associée de l'URL.
    • pickKeys : limite l'état sérialisé pour inclure uniquement les clés spécifiées.
    • préfixe : ajoute un préfixe à tous les paramètres de requête pour l'espace de noms.
    • url : l'URL de base avec laquelle synchroniser, généralement dérivée du contexte de l'application.

Avantages

  • API useState identique : intégration facile avec les composants existants.
  • Optimisé pour le référencement : garantit que les vues dépendant de l'état sont reflétées dans les URL partageables et pouvant être ajoutées aux favoris.
  • Mises à jour anti-rebond : empêche les mises à jour excessives des requêtes pour les entrées qui changent rapidement, comme les curseurs ou les zones de texte.
  • URL propres : des options telles que kebabCase et omitKeys maintiennent les chaînes de requête lisibles et pertinentes.
  • State Hydration : initialise automatiquement l'état à partir de l'URL lors du montage du composant, ce qui rend les liens profonds transparents.
  • Fonctionne partout : prend en charge le rendu côté serveur et la navigation côté client, garantissant un état cohérent dans toute l'application.

Applications pratiques

  • Filtres pour les listes de propriétés : synchronisez les filtres appliqués par l'utilisateur, tels que les types de listes, et mappez les limites à l'URL pour les recherches partageables.
  • Vues dynamiques : assurez-vous que le zoom de la carte, les points centraux ou d'autres paramètres d'affichage persistent lors des actualisations de page ou des liens.
  • Préférences utilisateur : enregistrez les paramètres sélectionnés par l'utilisateur dans l'URL pour faciliter le partage ou la mise en signet.

app.useStorageState : état persistant

Le hook app.useStorageState vous permet de conserver l'état dans le navigateur en utilisant localStorage ou sessionStorage, avec une prise en charge complète de TypeScript.

// 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' })
  );
};

Options de persistance

  • anti-rebond : contrôle la fréquence des sauvegardes dans le stockage.
  • stockage : choisissez entre localStorage et sessionStorage.
  • omitKeys/pickKeys : contrôle précis des données qui sont conservées.

Performance

  • Mises à jour optimisées avec anti-rebond.
  • Sérialisation/désérialisation automatique.
  • Mise en cache en mémoire.

Cas d'utilisation courants

  • Historique des recherches
  • Liste des favoris
  • Préférences utilisateur
  • État du filtre
  • Panier temporaire
  • Brouillons de formulaires

app.useDebounce : contrôle de fréquence

Anti-rebond des valeurs réactives sans effort :

// 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 : état sans doublons

Conservez les tableaux avec des valeurs uniques tout en préservant la sécurité des types.

Le hook app.useDistinct est spécialisé dans la détection du moment où une valeur a réellement changé, avec la prise en charge d'une comparaison approfondie et de l'anti-rebond :

// 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' })
  );
};

Principales fonctionnalités

  1. Détection de valeur distincte :
    • Suit les valeurs actuelles et précédentes.
    • Détecte automatiquement si un changement est significatif en fonction de vos critères.
  2. Comparaison approfondie :
    • Permet de vérifier l'égalité des valeurs à un niveau approfondi pour les objets complexes.
  3. Comparaison personnalisée :
    • Prend en charge les fonctions personnalisées pour définir ce qui constitue un changement « distinct ».
  4. Rebondi :
    • Réduit les mises à jour inutiles lorsque les changements se produisent trop fréquemment.

Avantages

  • API useState identique : intégration facile avec les composants existants.
  • Performances optimisées : évite les récupérations ou les recalculs inutiles lorsque la valeur n'a pas changé de manière significative.
  • UX améliorée : empêche les mises à jour trop réactives de l'interface utilisateur, ce qui conduit à des interactions plus fluides.
  • Logique simplifiée : élimine les contrôles manuels d'égalité ou de duplication dans la gestion de l'état.

Les hooks de React Edge sont conçus pour fonctionner en harmonie, offrant une expérience de développement fluide et fortement typée. Leur combinaison permet de créer des interfaces complexes et réactives avec beaucoup moins de code.

La CLI React Edge : la puissance à portée de main

La CLI pour React Edge a été conçue pour simplifier la vie des développeurs en rassemblant les outils essentiels dans une interface unique et intuitive. Que vous soyez un développeur débutant ou expérimenté, la CLI garantit que vous pouvez configurer, développer, tester et déployer des projets efficacement et sans effort.

Principales fonctionnalités

Commandes modulaires et flexibles :

  • build : construit à la fois l'application et le travailleur, avec des options pour spécifier les environnements et les modes (développement ou production).
  • dev : démarre les serveurs de développement locaux ou distants, permettant un travail séparé sur l'application ou le travailleur.
  • déploiement : permet des déploiements rapides et efficaces en tirant parti de la puissance combinée de Cloudflare Workers et de Cloudflare R2, garantissant performances et évolutivité dans l'infrastructure de périphérie.
  • journaux : surveille les journaux des travailleurs directement dans le terminal.
  • lint : automatise l'exécution de Prettier et ESLint, avec prise en charge des correctifs automatiques.
  • test : exécute des tests avec une couverture facultative à l'aide de Vitest.
  • type-check : valide la saisie TypeScript dans tout le projet.

Cas d'utilisation réels

Je suis fier de vous annoncer que la première application de production utilisant React Edge est déjà en ligne ! Il s'agit d'une société immobilière brésilienne, Lopes Imóveis, qui récolte déjà les bénéfices de la performance et de la flexibilité du framework.

Sur leur site Web, les propriétés sont chargées dans le cache pour optimiser la recherche et offrir une expérience utilisateur plus fluide. Puisqu'il s'agit d'un site très dynamique, la mise en cache des routes utilise une durée de vie de seulement 10 secondes, combinée à la stratégie périmée pendant la revalidation. Cela garantit que le site fournit des données mises à jour avec des performances exceptionnelles, même lors des revalidations en arrière-plan.

De plus, les recommandations pour des propriétés similaires sont calculées de manière efficace et asynchrone en arrière-plan, puis enregistrées directement dans le cache de Cloudflare à l'aide du système de mise en cache RPC intégré. Cette approche réduit les temps de réponse pour les demandes ultérieures et rend les recommandations d'interrogation presque instantanées. Toutes les images sont stockées sur Cloudflare R2, offrant un stockage évolutif et distribué sans recourir à des fournisseurs externes.

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

Bientôt, nous lancerons également un projet de marketing automatisé massif pour Easy Auth, démontrant encore plus le potentiel de cette technologie.

Conclusion

Et voilà, chers lecteurs, nous avons atteint la fin de ce voyage à travers le monde de React Edge ! Je sais qu’il reste encore une multitude de fonctionnalités incroyables à explorer, comme des options d’authentification plus simples telles que Basic et Bearer, et d’autres astuces qui rendent la journée d’un développeur beaucoup plus heureuse. Mais attendez ! L'idée est de proposer à l'avenir des articles plus détaillés pour approfondir chacune de ces fonctionnalités.

Alerte spoiler : bientôt, React Edge sera open source et correctement documenté ! Équilibrer le développement, le travail, l'écriture et un peu de vie sociale n'est pas facile, mais l'excitation de voir cette merveille en action, surtout avec la vitesse absurde fournie par l'infrastructure de Cloudflare, est le carburant qui me permet de continuer. Alors restez connectés, car le meilleur reste à venir ! ?

En attendant, si vous souhaitez commencer à l'explorer et à le tester dès maintenant, le package est déjà disponible sur NPM : React Edge sur NPM..

Mon e-mail est feliperohdee@gmail.com, et je suis toujours ouvert aux commentaires – ce n'est que le début de ce voyage. Les suggestions et critiques constructives sont les bienvenues. Si vous avez apprécié ce que vous avez lu, partagez-le avec vos amis et collègues et restez à l'écoute pour plus de mises à jour. Merci d'avoir lu jusqu'ici et à la prochaine fois ! ???

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn