Fermer

juin 27, 2024

Comment implémenter des fonctionnalités en temps réel avec React et TRPC.io

Comment implémenter des fonctionnalités en temps réel avec React et TRPC.io


Les mises à jour en temps réel peuvent être un excellent moyen de tenir tous les clients abonnés informés des dernières modifications. Dans ce didacticiel, nous explorons comment implémenter des fonctionnalités en temps réel à l’aide de TRPC.io et WebSockets pour avertir tous les utilisateurs abonnés.

TRPC.io est une bibliothèque indépendante du framework qui peut être utilisée pour créer des API de type sécurisé. Il comprend des bibliothèques client et serveur chargées de créer les points de terminaison de l’API du serveur et de les utiliser côté client.

Un gros avantage de TRPC.io est que les types sont partagés entre le serveur et le client. Le compilateur TypeScript met immédiatement en évidence toute incohérence dans les données envoyées entre eux.

Dans ce didacticiel, nous explorerons comment implémenter des fonctionnalités en temps réel à l’aide de TRPC.io.

Configuration du projet

Pour ce projet, nous devons créer une application qui sera composée d’un frontend et d’un backend. Pour le côté client, nous utiliserons React avec Vite. Quant au backend, TRPC peut être utilisé avec des frameworks comme Express ou Fastify via des adaptateurs, mais il offre également un moyen d’exécuter un serveur autonome. Nous utiliserons l’approche autonome.

Pour coder, vous pouvez suivre les instructions de configuration du projet ci-dessous ou simplement exécuter les commandes suivantes pour cloner le référentiel de démarrage et passer au start bifurquer.

$ git clone https://github.com/ThomasFindlay/real-time-functionality-with-trpcio
$ cd https://github.com/ThomasFindlay/real-time-functionality-with-trpcio
$ git checkout start

Assurez-vous d’installer les dépendances dans les deux client et server répertoires, puis exécutez le npm run dev commande dans chaque répertoire.

Si vous ne clonez pas le dépôt de démarrage, créez un nouveau dossier pour le projet. Dirigez-vous vers votre terminal et exécutez les commandes ci-dessous.

$ mkdir real-time-functionality-with-trpcio
$ cd real-time-functionality-with-trpcio

Le fonctionnalité-en-temps-réel-avec-trpcio le dossier comprendra deux répertoires :client et serveur.

Configuration du client

Nous utiliserons Vite pour échafauder un nouveau projet React avec TypeScript. Exécutez simplement les commandes ci-dessous dans le fonctionnalité-en-temps-réel-avec-trpcio dossier.

$ npm create vite@latest client -- --template react-ts
$ cd client
$ npm install @trpc/client

En plus de créer un nouveau projet, nous installons également le @trpc/client bibliothèque. Nous l’utiliserons dans notre application client pour effectuer des requêtes API vers le backend. Configureons-le ensuite.

Configuration du serveur

Nous devons prendre quelques mesures. Commençons par initialiser le projet backend avec le npm init commande.

$ mkdir server
$ cd server
$ npm init 

npm init vous posera quelques questions. Vous pouvez tous les ignorer en appuyant sur la touche Entrée.

Plusieurs dépendances sont nécessaires pour le serveur :

  • @trpc/serveur – Fonctionnalité TRPC côté serveur pour créer des routeurs
  • ws – Implémentation client et serveur WebSocket
  • zod – Validation du schéma
  • cors – Configurer les en-têtes Access-Control-Allow-Origin
  • manuscrit – Initialiser le tsconfig.json déposer
  • gant – Bibliothèque d’émetteurs d’événements
  • nodemon & ts-noeud – Exécuter le serveur en mode développement

Exécutez ensuite les commandes suivantes :

$ npm install @trpc/server cors zod ws mitt
$ npm install nodemon ts-node typescript @types/cors @types/ws -D
$ npx tsc --init
$ npx gitignore node

En plus d’installer des dépendances, nous créons également un tsconfig.json fichier en utilisant le tsc --init commande et créer un .gitignore déposer.

Ensuite, nous devons mettre à jour le package.json fichier pour ajouter un dev commande qui exécutera le backend. Remplacez la section « scripts » par le code ci-dessous.

serveur/paquet.json

"scripts": {
  "dev": "nodemon src/index.ts"
},

Maintenant que les projets sont configurés, les deux peuvent être exécutés à l’aide du npm run dev commande. Notez que vous devez l’exécuter dans chaque projet :client et serveur. Si vous exécutez le dev commande pour le serveur, vous verrez très probablement une erreur, car nous n’avons pas le src/index.ts fichier encore. Nous l’ajouterons dans la section suivante.

Serveur back-end avec TRPC.io

Commençons par créer un fichier appelé trpc.ts où nous allons configurer une instance TRPC et exporter un routeur et une procédure publique. UN procédure signifie essentiellement un point de terminaison d’API, qui peut être une requête, une mutation ou un abonnement.

serveur/src/trpc.ts

import { initTRPC } from "@trpc/server";
const t = initTRPC.create();
export const router = t.router;
export const publicProcedure = t.procedure;

Dans ce tutoriel, nous apprendrons comment créer des abonnements en direct à l’aide de TRPC.io. Nous allons implémenter une fonctionnalité qui permettra aux clients de s’abonner à un point de terminaison, de recevoir un nouveau devis toutes les 5 secondes et de le restituer dans l’interface utilisateur.

Cependant, avant de faire cela, implémentons d’abord un point de terminaison de requête qui renverra un devis provenant d’une API tierce.

serveur/src/quote/quote.router.ts

import { publicProcedure, router } from "../trpc";
import { z } from "zod";

const quoteSchema = z.object({
  id: z.number(),
  quote: z.string(),
  author: z.string(),
});

type Quote = z.infer<typeof quoteSchema>;

export const quoteRouter = router({
  getRandomQuote: publicProcedure.query(async (): Promise<Quote> => {
    const response = await fetch("https://dummyjson.com/quotes/random", {
      headers: {
        "Content-Type": "application/json",
      },
    });
    const result = await response.json();

    return quoteSchema.parse(result);
  }),
});

Dans le citation.router.ts fichier, nous avons le quoteRouter avec une seule procédure de requête—getRandomQuote. Il appelle le dummyjson API et récupère un devis aléatoire. Une fois le résultat reçu, il est validé à l’aide du quoteSchema. Les données renvoyées à l’aide de fetch sont de type unknownc’est donc une bonne pratique de valider les données et d’affiner leur type.

Maintenant que nous avons le quoteRouter configuré, créons le fichier d’entrée pour le backend qui contiendra la configuration d’un serveur TRPC autonome.

serveur/src/index.ts

import { createHTTPServer } from "@trpc/server/adapters/standalone";
import { quoteRouter } from "./quote/quote.router";
import { router } from "./trpc";
import cors from "cors";

const PORT = 3000;
const appRouter = router({
  quotes: quoteRouter,
});

export type AppRouter = typeof appRouter;

const { server, listen } = createHTTPServer({
  middleware: cors({
    origin: "http://localhost:5173",
  }),
  router: appRouter,
});

server.on("listening", () => {
  console.log(`Server listening on port ${PORT}`);
});

listen(PORT);

Nous utilisons le cors package pour autoriser les demandes provenant d’une origine différente. Notez que nous spécifions explicitement que la seule origine autorisée est http://localhost:5173. En mode développement, vous pouvez autoriser n’importe quelle origine à accéder à votre serveur, mais pour la production, il est important de vous assurer que seuls des clients spécifiques peuvent le faire.

Maintenant, nous pouvons démarrer le serveur en exécutant npm run dev commande dans le serveur annuaire.

Consommation de points de terminaison TRPC dans React

Le backend utilise le @trpc/server package pour configurer le serveur et les points de terminaison de l’API. Du côté du client, le @trpc/client Le package doit être utilisé à la place. Nous devons configurer un client proxy TRPC qui pointera vers le http://localhost:3000 URL, car c’est l’origine de notre serveur. Créons le client TRPC dans un fichier appelé api.ts.

client/src/api/api.ts

import { createTRPCProxyClient, httpBatchLink } from "@trpc/client";
import { type AppRouter } from "../../../server/src/index";

export const client = createTRPCProxyClient<AppRouter>({
  links: [
    httpBatchLink({
      url: "http://localhost:3000",
    }),
  ],
});

L’offre principale de TRPC est constituée d’API de type sécurisé de bout en bout, obtenues en important le AppRouter tapez à partir du répertoire du serveur et transmettez-le à createTRPCProxyClient. Grâce à cela, nous obtenons des indications de type avec auto-complétion pour les points de terminaison du serveur côté client et des erreurs du compilateur TS si les données côté client ne correspondent pas aux points de terminaison de l’API.

De plus, s’il y a des modifications dans la forme des données côté serveur, les informations de saisie à ce sujet seront propagées au code client, réduisant ainsi les risques de bogues accidentels.

Maintenant que le client TRPC est prêt, utilisons-le pour consommer le getRandomQuote requête. Nous le ferons en ajoutant une nouvelle fonction appelée fetchRandomQuote dans le quote.api.ts déposer.

client/src/api/quote.api.ts

import { client } from "./api";

export const fetchRandomQuote = () => {
  return client.quotes.getRandomQuote.query();
};

Enfin et surtout, mettons à jour le App composant. Il récupérera et affichera un devis aléatoire.

client/src/App.tsx

import { useState, useEffect } from "react";
import "./App.css";
import { fetchRandomQuote } from "./api/quote.api";

type Quote = {
  id: number;
  quote: string;
  author: string;
};

function App() {
  const [randomQuote, setRandomQuote] = useState<Quote | null>(null);

  useEffect(() => {
    (async () => {
      const quote = await fetchRandomQuote();
      setRandomQuote(quote);
    })();
  }, []);

  return (
    <div>
      <p>Quote:</p>
      {randomQuote ? (
        <div>
          <p>{randomQuote.quote}</p>
          <p>{randomQuote.author}</p>
        </div>
      ) : null}
    </div>
  );
}

export default App;

L’image ci-dessous montre à quoi devrait ressembler l’interface utilisateur. L’application doit afficher une citation et son auteur.

Citation aléatoire

WebSockets avec TRPC.io

Jusqu’à présent, nous avons implémenté une fonctionnalité permettant de récupérer un devis aléatoire à l’aide de TRPC.io. Maintenant, ajoutons quelques fonctionnalités en temps réel au mix. Premièrement, nous devons modifier le quote.router.ts fichier dans notre backend.

serveur/src/quote/quote.router.ts

import mitt from "mitt";
import { publicProcedure, router } from "../trpc";
import { z } from "zod";
import { observable } from "@trpc/server/observable";

const quoteSchema = z.object({
  id: z.number(),
  quote: z.string(),
  author: z.string(),
});

type Quote = z.infer<typeof quoteSchema>;

const fetchRandomQuote = async () => {
  const response = await fetch("https://dummyjson.com/quotes/random", {
    headers: {
      "Content-Type": "application/json",
    },
  });
  const result = await response.json();
  return quoteSchema.parse(result);
};

const quoteEventEmitter = mitt<{
  "on-random-quote": Quote;
}>();

export const quoteRouter = router({
  getRandomQuote: publicProcedure.query(async (): Promise<Quote> => {
    return fetchRandomQuote();
  }),
  onNewRandomQuote: publicProcedure.subscription(() => {
    return observable<Quote>(emit => {
      const onNewRandomQuote = (data: Quote) => {
        emit.next(data);
      };

      (async () => {
        const quote = await fetchRandomQuote();
        quoteEventEmitter.emit("on-random-quote", quote);
      })();

      quoteEventEmitter.on("on-random-quote", onNewRandomQuote);
    });
  }),
});

setInterval(async () => {
  const quote = await fetchRandomQuote();
  quoteEventEmitter.emit("on-random-quote", quote);
}, 5000);

Le premier changement est que nous avons extrait la logique de récupération des devis dans une fonction distincte fetchRandomQuoteafin que nous puissions le réutiliser.

const fetchRandomQuote = async () => {
  const response = await fetch("https://dummyjson.com/quotes/random", {
    headers: {
      "Content-Type": "application/json",
    },
  });
  const result = await response.json();
  return quoteSchema.parse(result);
};

De plus, le mitt la bibliothèque est utilisée pour créer un émetteur d’événements avec un événement :on-random-quote.

const quoteEventEmitter = mitt<{
  "on-random-quote": Quote;
}>();

L’émetteur d’événements est utilisé pour déclencher la diffusion à tous les clients abonnés lorsqu’un nouveau devis est récupéré.

Le onNewRandomQuote La procédure est un abonnement qui renvoie un observable qui est exécuté chaque fois qu’un nouveau client s’abonne aux mises à jour.

onNewRandomQuote: publicProcedure.subscription(() => {
  return observable<Quote>(emit => {
    const onNewRandomQuote = (data: Quote) => {
      emit.next(data);
    };

    (async () => {
      const quote = await fetchRandomQuote();
      quoteEventEmitter.emit("on-random-quote", quote);
    })();

    quoteEventEmitter.on("on-random-quote", onNewRandomQuote);
  });
}),

Enfin, à la fin, nous avons l’intervalle qui récupère un nouveau devis toutes les 5 secondes et utilise le quoteEventEmitter pour envoyer le devis récupéré à tous les abonnés.

setInterval(async () => {
  const quote = await fetchRandomQuote();
  quoteEventEmitter.emit("on-random-quote", quote);
}, 5000);

L’abonnement est prêt, mais nous n’avons pas encore de serveur WebSocket configuré. Configurons-le dans le index.ts déposer.

serveur/src/index.ts

import { applyWSSHandler } from "@trpc/server/adapters/ws";
import { createHTTPServer } from "@trpc/server/adapters/standalone";
import { quoteRouter } from "./quote/quote.router";
import { router } from "./trpc";
import cors from "cors";
import ws from "ws";

const PORT = 3000;

const wss = new ws.Server({
  port: 3001,
});

const appRouter = router({
  quotes: quoteRouter,
});

const handler = applyWSSHandler({ wss, router: appRouter });

export type AppRouter = typeof appRouter;

const { server, listen } = createHTTPServer({
  middleware: cors({
    origin: "http://localhost:5173",
  }),
  router: appRouter,
});

server.on("listening", () => {
  console.log(`Server listening on port ${PORT}`);
});

listen(PORT);

process.on("SIGTERM", () => {
  console.log("SIGTERM");
  handler.broadcastReconnectNotification();
  wss.close();
});

Le serveur WebSocket s’exécutera sur le port 3001. À la ligne 18, le applyWSSHandler est utilisé pour combiner le serveur WebSocket avec les routes TRPC et transmettre les messages aux procédures d’abonnement appropriées, telles que onNewRandomQuote que nous avons ajouté il y a un instant.

C’est tout ce que nous avions à faire pour le backend. Maintenant, occupons-nous du frontend. Premièrement, nous devons modifier le api.ts déposer. Pour le moment, il contient un client proxy TRPC avec le httpBatchLink, qui est responsable de l’exécution des requêtes API. Maintenant, nous devons configurer un client WebSocket et le connecter au client proxy TRPC. Un client WebSocket peut être créé à l’aide du createWSClient méthode. Il peut ensuite être transmis au wsLink méthode, qui est un équivalent de la httpBatchLinkmais pour les WebSockets.

client/src/api/api.ts

import {
  createTRPCProxyClient,
  createWSClient,
  httpBatchLink,
  splitLink,
  wsLink,
} from "@trpc/client";
import { type AppRouter } from "../../../server/src/index";

const wsClient = createWSClient({
  url: `ws://localhost:3001`,
});

export const client = createTRPCProxyClient<AppRouter>({
  links: [
    splitLink({
      condition(op) {
        return op.type === "subscription";
      },
      true: wsLink({
        client: wsClient,
      }),
      false: httpBatchLink({
        url: "http://localhost:3000",
      }),
    }),
  ],
});

Notez comment nous utilisons le splitLink pour indiquer à TRPC quel lien il doit utiliser en fonction du type d’opération. wsLink sera utilisé pour les procédures de souscription et httpBatchLink pour tout le reste. Ensuite, ajoutons une nouvelle méthode au quote.api.ts déposer.

client/src/api/quote.api.ts

import { client } from "./api";

export const fetchRandomQuote = () => {
  return client.quotes.getRandomQuote.query();
};

export type SubscribeToRandomQuotesParams = Parameters<
  (typeof client)["quotes"]["onNewRandomQuote"]["subscribe"]
>;

export const subscribeToRandomQuotes = (
  args: SubscribeToRandomQuotesParams[0],
  config: SubscribeToRandomQuotesParams[1]
) => {
  return client.quotes.onNewRandomQuote.subscribe(args, config);
};

Le subscribeToRandomQuotes La méthode renvoie un abonnement au onNewRandomQuote procédure.

Enfin, nous pouvons mettre à jour le App composant et ajoutez une logique pour vous abonner à des cotations aléatoires afin d’afficher une nouvelle cotation toutes les 5 secondes.

client/src/App.tsx

import { useState, useEffect } from "react";
import "./App.css";
import { fetchRandomQuote, subscribeToRandomQuotes } from "./api/quote.api";

type Quote = {
  id: number;
  quote: string;
  author: string;
};

function App() {
  const [randomQuote, setRandomQuote] = useState<Quote | null>(null);
  const [latestRandomQuote, setLatestRandomQuote] = useState<Quote | null>(
    null
  );

  useEffect(() => {
    (async () => {
      const quote = await fetchRandomQuote();
      setRandomQuote(quote);
    })();

    const subscription = subscribeToRandomQuotes(undefined, {
      onData(data: Quote) {
        setLatestRandomQuote(data);
      },
    });

    return () => {
      subscription.unsubscribe();
    };
  }, []);

  return (
    <div>
      <p>Quote:</p>
      {randomQuote ? (
        <div>
          <p>{randomQuote.quote}</p>
          <p>{randomQuote.author}</p>
        </div>
      ) : null}

      <hr />

      <p>Quote from subscription:</p>
      {latestRandomQuote ? (
        <div>
          <p>{latestRandomQuote.quote}</p>
          <p>{latestRandomQuote.author}</p>
        </div>
      ) : null}
    </div>
  );
}

export default App;

Dans le useEffect nous nous abonnons à de nouvelles cotations aléatoires en appelant le subscribeToRandomQuotes méthode. Le setLatestRandomQuote met à jour le dernier devis lorsqu’un nouveau est envoyé. Le GIF ci-dessous montre à quoi devrait ressembler l’interface utilisateur.

Abonnement à des devis aléatoires en temps réel

Conclusion

Dans ce didacticiel, nous avons exploré comment utiliser TRPC.io pour implémenter une API de type sécurisé et une fonctionnalité en temps réel. Nous avons créé une application qui reçoit et affiche un nouveau devis toutes les quelques secondes à l’aide de TRPC.io avec WebSockets. Si vous êtes prêt à relever un défi, vous pouvez étendre l’application que nous avons créée pour permettre aux utilisateurs de créer eux-mêmes des devis et de les diffuser à d’autres.

Bon codage !




Source link