Maison > interface Web > js tutoriel > le corps du texte

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

DDD
Libérer: 2024-11-20 13:58:12
original
855 Les gens l'ont consulté
  • La goutte d'eau
  • L'alternative avec Cloudflare ?
  • React Edge : Le Framework React issu de tous (ou presque) les soucis d'un développeur
    • 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 prospective
  • app.useContext : la passerelle vers Edge
  • app.useUrlState : Statut synchronisé avec l'URL
  • app.useStorageState : État persistant
  • app.useDebounce : contrôle de fréquence
  • app.useDistinct : Statut sans doublons
  • La CLI React Edge : la puissance à portée de main
  • Conclusion

La dernière goutte

Tout a commencé par une facture de Vercel. Non, cela a en fait 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 telles que la protection DDoS, des journaux plus détaillés ou même un pare-feu décent, des files d'attente de création, etc. Le sentiment d’être piégé dans un enfermement de plus en plus coûteux avec un fournisseur.

"Et le pire de tout : nos précieux en-têtes SEO ont tout simplement cessé d'être rendus 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 du client d'utilisation et du serveur d'utilisation - des directives qui, en théorie, devraient simplifier le développement, mais qui, en pratique, ajoutent une autre couche de complexité à gérer. C'était comme si nous revenions à l'époque de PHP, marquant les fichiers avec des directives pour leur indiquer où ils doivent 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 pratiquement nouveau au sein de Next.js. Du coup, nous avions deux manières complètement différentes de faire la même chose. L'« ancien » et le « nouveau » – avec des comportements subtilement différents et des pièges cachés.

L’alternative avec Cloudflare ?

C'est à ce moment-là que j'ai réalisé : pourquoi ne pas profiter de l'incroyable infrastructure de Cloudflare avec Workers fonctionnant à la périphérie, R2 pour le stockage, KV pour les données distribuées... Plus, bien sûr, l'incroyable protection DDoS, le CDN global, le pare-feu, les règles. pour les pages, les itinéraires et tout ce que Cloudflare propose.

Et le meilleur : un modèle au juste prix, où vous payez ce que vous utilisez, sans surprise.

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

React Edge : Le Framework React issu de tous (ou presque) les soucis d'un développeur

Quand j'ai commencé à développer React Edge, j'avais un objectif clair : créer un framework qui avait du sens. Plus besoin de se battre avec des directives confuses, plus besoin de payer des fortunes pour des fonctionnalités de base et, surtout, plus besoin de gérer la complexité artificielle créée par la séparation client/serveur. Je voulais de la vitesse, quelque chose qui offre des performances sans sacrifier la simplicité. Tirant parti de ma connaissance de l'API React et de mes années 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, m'a offert 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 est ce qui a donné vie à React Edge : un framework qui résout de vrais problèmes avec des solutions modernes et efficaces.

React Edge apporte une approche révolutionnaire au développement de React. Imaginez pouvoir écrire une classe sur le serveur et l'appeler directement depuis le client, avec une saisie complète 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 pouvoir partager l'état entre le serveur et le client de manière transparente et sécurisée. En plus de simplifier l'authentification et d'apporter une approche d'internationalisation efficace, CLI et bien plus encore.

Votre communication RPC est si naturelle qu'elle semble magique : vous écrivez des méthodes dans une classe et vous 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 adressée au serveur. Le cache éphémère évite les requêtes répétées inutiles, et tout cela fonctionne aussi bien sur le serveur que sur le client.

L'un des points les plus puissants 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 permet des mises à jour à la demande. Et 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 ça ne s'arrête pas là. Le framework offre un système de routage puissant (inspiré du fantastique Hono), une gestion des actifs intégrée à Cloudflare R2 et une manière élégante de gérer les erreurs via la classe HttpError. Le middleware peut facilement envoyer des données au client via un magasin partagé, et tout est automatiquement masqué pour des raisons de sécurité.

Le plus impressionnant ? Presque tout le code du framework est hybride. Il n'existe pas de version « client » et de « serveur » : le même code fonctionne dans les deux environnements, s'adaptant automatiquement au contexte. Le client ne reçoit que 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 Edge Cloudflare Workers, offrant des performances exceptionnelles à un coût équitable. Pas de surprise sur la facture, pas de fonctionnalités de base cachées derrière des forfaits entreprise forcés, juste un framework solide qui permet de se concentrer sur ce qui compte vraiment : créer des applications incroyables. De plus, React Edge exploite l'ensemble de l'écosystème Cloudflare, y compris les files d'attente, les objets durables, le stockage KV, etc., pour fournir une base robuste et évolutive pour vos applications.

Vite a été utilisé comme base, à la fois pour l'environnement de développement et pour les tests et la construction. Vite, avec sa vitesse impressionnante et son architecture moderne, permet un flux de travail agile et efficace. Cela accélère non seulement le développement, mais optimise également le processus de construction, garantissant que le code est compilé rapidement et avec précision. 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 avoir à mémoriser des dizaines de directives comme use client ou use server ? Et encore mieux : 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 pas besoin de :

  • Créer des routes API distinctes
  • Gérer manuellement l'état de chargement/erreur
  • Mettre en œuvre l'anti-rebond à la main
  • Vous vous inquiétez de la sérialisation/désérialisation
  • Poignée CORS
  • Gérer la saisie entre client/serveur
  • Gérer les règles d'authentification manuellement
  • Gérer la manière dont l'internationalisation se fait

Et le meilleur : tout cela fonctionne à la fois sur le serveur et sur le client, sans avoir à marquer quoi que ce soit avec use client ou use server. Le framework sait quoi faire en fonction du contexte. On y va ?

La magie du RPC typé

Imaginez pouvoir faire ceci :

// 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' })
  );
};
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion

Comparez ceci à 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
}
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion

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

La récupération de données repensée

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

  • Précharger les données sur le serveur pendant SSR
  • Hydrate automatiquement le client sans scintillement
  • Maintient une 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
  • Autorise les mises à jour et les sondages programmatiques

Voyons cela en action :

// 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' })
  );
};
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion

La magie du multiplexage

L'exemple ci-dessus cache une fonctionnalité puissante : le multiplexage intelligent. Lorsque vous utilisez ctx.rpc.batch, React Edge ne se contente pas de regrouper les appels : il déduplique automatiquement les appels identiques :

// 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
}
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion

SSR Hydratation Parfaite

L'une des parties les plus impressionnantes est la façon dont useFetch gère le 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>
 );
};
Copier après la connexion
Copier après la connexion
Copier après la connexion

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 a été 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) => {
    // 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!
};
Copier après la connexion
Copier après la connexion

Hiérarchie des API RPC

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

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>
  );
};
Copier après la connexion
Copier après la connexion

Avantages de la hiérarchie

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

  • Organisation Logique : Regroupez les fonctionnalités liées de manière intuitive
  • Espace de noms naturel : évitez les conflits de noms avec des chemins clairs (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. Et grâce à la possibilité d'organiser les API en hiérarchies, vous pouvez créer des structures complexes tout en gardant votre code organisé et sécurisé.

Un système i18n qui a du sens

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

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;
};
Copier après la connexion

Utilisation dans le code :

// 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');
};
Copier après la connexion

Zéro configuration

React Edge détecte et charge automatiquement vos traductions et peut facilement enregistrer les préférences de l'utilisateur dans les cookies. Mais vous vous y attendiez déjà, n'est-ce pas ?

// 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' })
  );
};
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion

Authentification JWT qui « fonctionne tout simplement »

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

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

// 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
}
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion

Utilisation du client : zéro configuration

// 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>
 );
};
Copier après la connexion
Copier après la connexion
Copier après la connexion

Pourquoi est-ce révolutionnaire ?

  1. Zéro passe-partout

    • Pas de gestion manuelle des cookies
    • Pas besoin d'intercepteurs
    • Pas de manuel de jeton de mise à niveau
  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 avec Zod intégré
    • Erreurs d'authentification tapées
  4. Intégration transparente

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!
};
Copier après la connexion
Copier après la connexion

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. Voyons comment cela fonctionne :

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>
  );
};
Copier après la connexion
Copier après la connexion

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 du middleware : le middleware peut remplir le magasin avec des données publiques et privées, garantissant un flux continu d'informations entre la logique du serveur et le rendu côté client.

Avantages

  1. Sécurité : la séparation des données publiques et privées garantit que les informations confidentielles 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 de statut dynamiques 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 super pouvoirs pour 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' })
  );
};
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion

Principales fonctionnalités

  • Itinéraires groupés : regroupement logique des itinéraires associés 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.
  • Cache intégré : 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 du cache natif garantit des temps de réponse optimaux sans configuration manuelle.

Cache distribué avec cache Edge

React Edge dispose d'un puissant système de mise en cache qui fonctionne à la fois pour les données JSON et les pages entières :

// 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
}
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion

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.
  • Time to Live (TTL) : définissez les délais d'expiration pour les entrées de cache afin de garantir des données fraîches tout en maintenant des performances élevées.

Avantages

  1. Performances améliorées : réduit la charge sur les API en fournissant des réponses en cache pour les données fréquemment consultées.
  2. Évolutivité : gère efficacement les grands ensembles de données et le trafic élevé avec 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 prospective

Le composant Link est une solution intelligente et performante de préchargement de ressources côté client, assurant une navigation plus fluide et plus rapide aux utilisateurs. Sa fonctionnalité de prélecture est activée lors du survol du curseur sur le lien, profitant du moment d'inactivité de l'utilisateur pour demander à l'avance les données de destination.

Comment ça marche ?

  1. Prélecture conditionnelle : L'attribut de prélecture (actif par défaut) contrôle si le préchargement sera effectué.

  2. Smart Cache : Un Set est utilisé pour stocker les liens déjà préchargés, évitant ainsi les appels redondants.

  3. Mouse Enter : Lorsque l'utilisateur passe le curseur sur le lien, la fonction handleMouseEnter vérifie si un préchargement est nécessaire et, si c'est le cas, lance une demande de récupération vers la destination.

  4. Error Safe : tout échec dans la requête est supprimé, garantissant que le comportement du composant n'est pas affecté par des erreurs réseau momentanées.

// 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' })
  );
};
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion

Lorsque l'utilisateur passe la souris sur le lien « À propos de nous », le composant commence à précharger les données de la page /about, offrant une transition presque instantanée. Idée géniale, non ? Mais je l'ai vu dans la documentation de React.dev.

app.useContext : la passerelle vers Edge

app.useContext est le hook fondamental de React Edge, donnant accès à l'ensemble du contexte du travailleur :

// 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
}
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion

Principales fonctionnalités de app.useContext

  • Gestion de l'itinéraire : 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 total sur la visibilité (publique/privée).
  • Accès universel à l'URL : accédez facilement à l'URL complète de la requête en cours pour un rendu dynamique et des 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 s'appuient sur un état partagé, une récupération de données sécurisée et un rendu contextuel sans code répétitif. 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 d'URL, vous donnant 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.

// 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>
 );
};
Copier après la connexion
Copier après la connexion
Copier après la connexion

Paramètres

  1. État initial

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

    • anti-rebond : contrôle la rapidité avec laquelle l'URL est mise à jour après un changement d'état.
    • kebabCase : convertit les clés d'état en kebab-case lors de la sérialisation en URL.
    • omitKeys : Spécifie les clés à exclure de l'URL.
    • 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 des clés spécifiques.
    • préfixe : ajoute un préfixe à tous les paramètres de requête.
    • url : L'URL de base pour la synchronisation, généralement dérivée du contexte de l'application.

Avantages

  • SEO Friendly : garantit que les vues dépendant de l'état sont reflétées dans les URL partageables.
  • Mises à jour avec anti-rebond : empêche les mises à jour excessives des requêtes pour les entrées qui changent rapidement.
  • Nettoyer les URL : des options telles que kebabCase et omitKeys maintiennent les chaînes de requête lisibles.
  • State Hydration : initialise automatiquement l'état de l'URL lors de l'assemblage du composant.
  • Fonctionne dans tous les environnements : prend en charge le rendu côté serveur et la navigation côté client.

Applications pratiques

  • Filtres pour les annonces : synchronise les filtres appliqués par l'utilisateur avec l'URL.
  • Vues dynamiques : garantit la persistance du zoom de la carte, des points centraux ou d'autres paramètres.
  • Préférences utilisateur : enregistre les paramètres sélectionnés sur une URL pour le partage.

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 la saisie.

// 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' })
  );
};
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion

Options de persistance

  • anti-rebond : contrôle la fréquence d'enregistrement
  • stockage : choisissez entre localStorage et sessionStorage
  • omitKeys/pickKeys : contrôle précis des données persistantes

Performance

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

Cas d'utilisation courants

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

app.useDebounce : contrôle de fréquence

Anti-rebond des valeurs réactives en toute simplicité :

// 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
}
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion

app.useDistinct : État sans doublons

Maintenir des tableaux de valeurs uniques en tapant :

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

// 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' })
  );
};
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion

Principales fonctionnalités

  1. Détection de valeur distincte :
    • Surveille 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 des vérifications d'égalité approfondies 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 identique à useState : Facile à intégrer dans 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 entraîne 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 React Edge ont été conçus pour fonctionner en harmonie, offrant une expérience de développement fluide et typée. Les combiner 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 React Edge a été conçue pour simplifier la vie des développeurs en regroupant les outils essentiels dans une interface unique et intuitive. Que vous soyez débutant ou expert, la CLI garantit que vous pouvez configurer, développer, tester et déployer des projets efficacement et sans tracas.

Fonctionnalités clés

Commandes modulaires et flexibles :

  • build : construit à la fois l'application et le travailleur, avec des options permettant de spécifier les environnements et les modes de développement ou de production.
  • dev : lance des serveurs de développement locaux ou distants, vous permettant de travailler séparément sur l'application ou le travailleur.
  • déploiement : effectue des déploiements rapides et efficaces en utilisant la puissance combinée de Cloudflare Workers et de Cloudflare R2, garantissant performances et évolutivité sur l'infrastructure périphérique.
  • journaux : surveillez les journaux des travailleurs directement dans le terminal.
  • lint : automatise l'exécution de Prettier et ESLint, avec prise en charge des corrections automatiques.
  • test : exécute des tests avec une couverture facultative à l'aide de Vitest.
  • type-check : valide la saisie TypeScript dans le projet.

Cas d'utilisation en production

Je suis fier de vous annoncer que la première application de production utilisant React Edge fonctionne désormais ! Il s'agit d'une société immobilière brésilienne, Lopes Imóveis, qui profite déjà de toutes les performances et de la flexibilité du framework.

Sur le site de l'agence immobilière, les propriétés sont chargées en cache pour optimiser la recherche et offrir une expérience plus fluide aux utilisateurs. Comme il s’agit d’un site Web extrêmement dynamique, le cache de route utilise un TTL de seulement 10 secondes, combiné à la stratégie périmée pendant la revalidation. Cela garantit que le site fournit des données à 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 efficacement et occasionnellement en arrière-plan, et enregistrées directement dans le cache de Cloudflare, à l'aide du système de cache intégré à RPC. Cette approche réduit le temps de réponse aux requêtes ultérieures et rend les recommandations d'interrogation presque instantanées. De plus, toutes les images sont stockées sur Cloudflare R2, offrant un stockage évolutif et distribué sans recourir à des fournisseurs externes.

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

Et bientôt nous aurons également le lancement d'un gigantesque projet de marketing automatisé pour Easy Auth, démontrant encore davantage le potentiel de cette technologie.

Conclusion

Et voilà, chers lecteurs, nous arrivons au terme de cette aventure à travers l'univers React Edge ! Je sais qu'il y a encore une mer de choses incroyables à explorer, comme les authentifications les plus simples comme Basic et Bearer, et d'autres petits secrets qui rendent la vie quotidienne d'un développeur beaucoup plus heureuse. Mais calme-toi ! L’idée est de proposer à l’avenir des articles plus détaillés pour approfondir chacune de ces fonctionnalités.

Et, 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 m'anime. Alors retenez votre anxiété, car le meilleur est à venir ! ?

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

Mon email est feliperohdee@gmail.com, et je suis toujours ouvert aux commentaires, ce n'est que le début de ce voyage, des suggestions et des critiques constructives. Si vous avez aimé ce que vous avez lu, partagez-le avec vos amis et collègues et gardez un œil sur les nouveautés à venir. Merci de m'avoir suivi jusqu'ici, et à la prochaine ! ???

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!

source:dev.to
Déclaration de ce site Web
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
Tutoriels populaires
Plus>
Derniers téléchargements
Plus>
effets Web
Code source du site Web
Matériel du site Web
Modèle frontal