Plongez avec moi dans cette étude de cas, où nous nous lancerons dans un voyage pour construire une application en temps réel, tirant parti de la puissance et de la finesse des actions du serveur Next.js. Que vous soyez un développeur chevronné ou que vous vous aventuriez simplement dans le domaine des applications en temps réel, une mine d'informations vous attend.
Pour le mettre en perspective, considérons quelques exemples omniprésents :
Applications de messagerie instantanée : Des plateformes comme WhatsApp et Telegram où les messages sont envoyés, reçus et vus sans délai.
Outils collaboratifs : pensez à Google Docs, où plusieurs utilisateurs peuvent modifier un document simultanément, en observant les modifications des autres en temps réel.
Live Stock Tickers : Plates-formes qui affichent les cours des actions qui se mettent à jour instantanément avec les fluctuations du marché.
Jeux multijoueurs en ligne : où les joueurs interagissent entre eux et avec l'environnement sans latence, garantissant une expérience de jeu fluide.
Alors, pourquoi la fonctionnalité en temps réel est-elle si recherchée ?
La création d'applications en temps réel n'est pas sans obstacles :
Problèmes d'évolutivité : les applications en temps réel doivent souvent gérer de nombreuses connexions simultanées, ce qui nécessite une infrastructure robuste.
Intégrité des données : S'assurer que les données en temps réel restent cohérentes sur les différentes interfaces utilisateur peut être un défi, en particulier avec plusieurs modifications ou interactions simultanées.
Latence : Une application en temps réel est seulement aussi bonne que son composant le plus lent. Garantir des retards minimaux nécessite une optimisation minutieuse et une utilisation efficace des ressources.
Les actions de l'écosystème React, bien qu'encore expérimentales, ont entraîné un changement de paradigme en permettant aux développeurs d'exécuter du code asynchrone en réponse aux interactions des utilisateurs.
Fait intéressant, bien qu'ils ne soient pas exclusifs à Next.js ou React Server Components, leur utilisation via Next.js signifie que vous êtes sur le canal expérimental React.
Pour ceux qui sont familiers avec les formulaires HTML, vous vous souviendrez peut-être de la transmission d'URL à la prop action
. Désormais, avec Actions, vous pouvez passer directement une fonction, ce qui rend les interactions plus dynamiques et intégrées.
<button action={() => { /* async function logic here */ }}>Click me!</button>
Les actions de formulaire représentent une fusion ingénieuse des actions de React avec l'API standard <form>
. Ils résonnent avec l'attribut primitif formaction
en HTML, permettant aux développeurs d'améliorer les états de chargement progressifs et d'autres fonctionnalités prêtes à l'emploi.
<!-- Traditional HTML approach --> <form action="/submit-url"> <!-- form elements --> </form> <!-- With Next.js 13.4 Form Actions --> <form action={asyncFunctionForSubmission}> <!-- form elements --> </form>
Les fonctions serveur sont essentiellement des fonctions qui fonctionnent côté serveur mais qui peuvent être appelées depuis le client. Celles-ci élèvent les capacités de rendu côté serveur de Next.js à un tout autre niveau.
Lors de la transition vers les actions de serveur , elles peuvent être comprises comme des fonctions de serveur, mais celles qui sont spécifiquement déclenchées en tant qu'action. Leur intégration avec des éléments de formulaire, en particulier via le prop action
, garantit que le formulaire reste interactif même avant le chargement du JavaScript côté client. Cela se traduit par une expérience utilisateur plus fluide, l'hydratation React n'étant pas une condition préalable à la soumission du formulaire.
// A simple Server Action in Next.js 13.4 <form action={serverActionFunction}> <!-- form elements --> </form>
Enfin, nous avons les mutations de serveur , qui sont un sous-ensemble des actions de serveur. Celles-ci sont particulièrement puissantes lorsque vous devez modifier des données sur le serveur, puis exécuter des réponses spécifiques, telles que redirect
, revalidatePath
ou revalidateTag
.
const serverMutationFunction = async () => { // Modify data logic here... // ... return { revalidatePath: '/updated-path' }; } <form action={serverMutationFunction}> <!-- form elements --> </form>
Remarques : En résumé, le framework Server Actions de Next.js 13.4, étayé par des actions, des actions de formulaire, des fonctions de serveur et des mutations de serveur, incarne une approche transformatrice des applications Web en temps réel.
Au fur et à mesure que nous avançons dans notre étude de cas, vous serez témoin de première main des prouesses que ces fonctionnalités apportent à la table. Alors, préparons-nous pour le voyage passionnant qui nous attend !
Tout d'abord, vous devez activer les actions du serveur dans votre projet Next.js. Ajoutez simplement le code suivant à votre fichier next.config.js
:
module.exports = { experimental: { serverActions: true, }, }
Dans les composants serveur : une action serveur peut être facilement définie dans un composant serveur, comme ceci :
export default function ServerComponent() { async function myAction() { 'use server' // ... } }
Avec les composants client : lors de l'utilisation d'une action serveur dans un composant client, créez l'action dans un fichier séparé, puis importez-la.
// app/actions.js 'use server' export async function myAction() { // ... }
// app/client-component.js import { myAction } from './actions' export default function ClientComponent() { return ( <form action={myAction}> <button type="submit">Add to Cart</button> </form> ) }
Invocation personnalisée : vous pouvez utiliser des méthodes personnalisées telles que startTransition
pour invoquer des actions de serveur en dehors des formulaires, des boutons ou des entrées.
// Example using startTransition 'use client' import { useTransition } from 'react' import { addItem } from '../actions' function ExampleClientComponent({ id }) { let [isPending, startTransition] = useTransition() return ( <button onClick={() => startTransition(() => addItem(id))}> Add To Cart </button> ) }
Next.js 13.4 propose également une amélioration progressive, permettant à un <form>
de fonctionner sans JavaScript. Les actions du serveur peuvent être transmises directement à un <form>
, rendant le formulaire interactif même si JavaScript est désactivé.
// app/components/example-client-component.js 'use client' import { handleSubmit } from './actions.js' export default function ExampleClientComponent({ myAction }) { return ( <form action={handleSubmit}> {/* ... */} </form> ) }
Le corps de requête maximal envoyé à une action serveur est de 1 Mo par défaut. Si nécessaire, vous pouvez configurer cette limite à l'aide de l'option serverActionsBodySizeLimit
:
module.exports = { experimental: { serverActions: true, serverActionsBodySizeLimit: '2mb', }, }
npx create-next-app@latest my-real-time-app
Remplacez my-real-time-app
par le nom souhaité pour votre projet. Cette commande configure un nouveau projet Next.js avec les configurations par défaut.
Avec l'introduction de Next.js 13.4, le routeur d'applications est une fonctionnalité importante qui permet aux développeurs d'utiliser des mises en page partagées, un routage imbriqué, la gestion des erreurs, etc. Il est conçu pour fonctionner en conjonction avec le répertoire pages
existant, mais il est hébergé dans un nouveau répertoire nommé app
.
Créez un répertoire app
à la racine de votre projet.
Par défaut, les composants du répertoire app
sont des composants serveur , offrant des performances optimales et permettant aux développeurs de les adopter facilement.
my-real-time-app/ │ ├── app/ # Main directory for App Router components │ ├── _error.js # Custom error page │ ├── _layout.js # Shared layout for the app │ │ │ ├── dashboard/ # Nested route example │ │ ├── index.js # Dashboard main view │ │ └── settings.js # Dashboard settings view │ │ │ ├── index.js # Landing/Home page │ ├── profile.js # User profile page │ ├── login.js # Login page │ └── register.js # Registration page │ ├── public/ # Static assets go here (images, fonts, etc.) │ ├── images/ │ └── favicon.ico │ ├── styles/ # Global styles or variables │ └── global.css │ ├── package.json # Dependencies and scripts ├── next.config.js # Next.js configuration └── README.md # Project documentation
Composants serveur : Idéal pour les parties non interactives de votre application. Ces composants sont rendus sur le serveur et envoyés au client au format HTML. L'avantage ici est l'amélioration des performances, la réduction du JavaScript côté client et la possibilité de récupérer des données ou d'accéder directement aux ressources principales.
Composants client : utilisés pour les éléments d'interface utilisateur interactifs. Ils sont pré-rendus sur le serveur puis "hydratés" sur le client pour ajouter de l'interactivité.
Pour différencier ces composants, Next.js a introduit la directive "use client"
. Cette directive indique qu'un composant doit être traité comme un composant client. Il doit être placé en haut d'un fichier de composant, avant toute importation.
Par exemple, si vous avez un compteur interactif, comme dans le code fourni, vous utiliserez la directive "use client"
pour indiquer qu'il s'agit d'un composant côté client.
Utilisez les composants serveur par défaut (tels qu'ils se trouvent dans le répertoire app
).
N'optez pour les composants client que lorsque vous avez des cas d'utilisation spécifiques tels que l'ajout d'interactivité, l'utilisation d'API de navigateur uniquement ou l'utilisation de hooks React qui dépendent de l'état ou des fonctionnalités du navigateur.
Remarques : en suivant cette structure et cette configuration, vous serez sur la bonne voie pour créer une application en temps réel performante avec les actions de serveur de Next.js 13.4.
La puissance de Next.js 13.4 brille lors de l'intégration de fonctionnalités backend en temps réel dans notre projet. Passons en revue les étapes avec des exemples de code pertinents pour notre my-real-time-app
.
Pour notre my-real-time-app
, les actions du serveur agissent comme notre principal pont entre le frontend et le backend , permettant des transactions de données efficaces sans avoir besoin d'API séparées.
// my-real-time-app/app/actions/index.js export * from './auth-action'; export * from './chat-action';
Dans my-real-time-app
, nous tirons parti des actions du serveur pour rationaliser le processus d'authentification.
// my-real-time-app/app/actions/auth-action.js export const login = async (credentials) => { // Logic for authenticating user with credentials // Return user details or error message }; export const logout = async (userId) => { // Logic for logging out the user // Return success or error message }; export const register = async (userInfo) => { // Logic for registering a new user // Store user in database and return success or error message };
// my-real-time-app/app/actions/chat-action.js export const sendMessage = async (messageDetails) => { // Logic to send a new message // Store message in database and inform other users via WebSocket or similar }; export const receiveMessage = async () => { // Logic to receive a message in real-time // Return the message details }; export const getRecentMessages = async (userId) => { // Logic to fetch recent messages for the user // Retrieve messages from the database };
// Initialize MongoDB connection const { MongoClient } = require('mongodb'); const client = new MongoClient(process.env.MONGODB_URI); await client.connect(); // Now, use this connection in server actions to interact with the database.
// my-real-time-app/app/actions/chat-action.js export const sendMessage = async (messageDetails) => { const messagesCollection = client.db('chatDB').collection('messages'); await messagesCollection.insertOne(messageDetails); // Inform other users via WebSocket or similar };
// Middleware for validating request data const validateRequest = (req) => { // Validation logic here return isValid; }; export const sendMessage = async (messageDetails) => { if (!validateRequest(messageDetails)) { throw new Error("Invalid request data"); } // Remaining logic... };
Dans cette section, nous allons construire une interface de chat intuitive et réactive pour my-real-time-app
. L'intégration des composants serveur de Next.js 13.4 permettra des mises à jour en temps réel pour une expérience utilisateur fluide.
// my-real-time-app/app/chat-interface.js import { useEffect, useState } from 'react'; import { getRecentMessages } from './actions/chat-action'; export default function ChatInterface() { const [messages, setMessages] = useState([]); useEffect(() => { async function loadMessages() { const recentMessages = await getRecentMessages(); setMessages(recentMessages); } loadMessages(); }, []); return ( <div className="chatBox"> {messages.map(msg => ( <p key={msg.id}>{msg.content}</p> ))} </div> ); }
// my-real-time-app/app/chat-interface.js const [socket, setSocket] = useState(null); useEffect(() => { const ws = new WebSocket("ws://your-backend-url/ws"); ws.onmessage = (event) => { const newMessage = JSON.parse(event.data); setMessages(prevMessages => [...prevMessages, newMessage]); }; setSocket(ws); return () => { ws.close(); }; }, []);
// my-real-time-app/app/chat-interface.js useEffect(() => { if (messages.length && "Notification" in window && Notification.permission === "granted") { const lastMessage = messages[messages.length - 1]; new Notification(`New message from ${lastMessage.sender}: ${lastMessage.content}`); } }, [messages]);
const HeavyComponent = React.lazy(() => import('./HeavyComponent')); function Chat() { return ( <React.Suspense fallback={<div>Loading...</div>}> <HeavyComponent /> </React.Suspense> ); }
React Server Components
de Next.js pour diviser la logique :
Avec les composants de base de notre application en temps réel en place, il est essentiel de s'assurer qu'ils fonctionnent comme prévu et qu'ils sont performants, évolutifs et robustes. Cette section met en lumière diverses approches de test adaptées aux systèmes en temps réel comme notre my-real-time-app
.
// cypress/integration/chat.spec.js describe('Chat functionality', () => { it('should send and receive messages in real-time', () => { cy.visit('/chat'); cy.get('[data-cy=messageInput]').type('Hello, World!'); cy.get('[data-cy=sendButton]').click(); cy.contains('Hello, World!').should('exist'); }); });
# artillery-config.yml config: target: '//my-real-time-app.com' phases: - duration: 300 arrivalRate: 20 scenarios: - flow: - emit: channel: 'chat' payload: message: 'Hello, World!'
$ artillery run artillery-config.yml
Node.js fournit des outils intégrés pour le profilage, et l'indicateur --inspect
peut être utilisé avec le serveur de développement Next.js pour activer l'inspecteur Node.js. En utilisant les DevTools de Chrome, on peut obtenir des informations sur les goulots d'étranglement des performances.
Pour le côté client, des outils tels que l'onglet Performance
de Chrome DevTools peuvent aider à identifier les goulots d'étranglement du rendu. Surtout avec les mises à jour en temps réel, assurez-vous qu'aucun rendu inutile ne se produit.
Exemple avec SWR :
// my-real-time-app/app/chat-interface.js import useSWR from 'swr'; function ChatInterface() { const { data: messages } = useSWR('/api/messages', fetcher); // ... rest of the component }
Remarques : Le test d'applications en temps réel nécessite une combinaison de techniques de test de logiciels standard et d'autres conçues spécifiquement pour les défis et les caractéristiques des systèmes en temps réel. En assurant un régime de test rigoureux pour my-real-time-app
, nous pouvons garantir une expérience utilisateur fluide et réactive, quelle que soit l'ampleur du trafic utilisateur ou du flux de données.
Avec l'architecture fondamentale de notre application en temps réel bien en place, notre attention se tourne maintenant vers le raffinement de ses fonctionnalités et de ses performances. Voici quelques stratégies pour améliorer l'expérience utilisateur et optimiser notre my-real-time-app
:
// my-real-time-app/app/components/Message.js function Message({ content, status }) { return ( <div> <p>{content}</p> {status === 'read' && <span>✓ Read</span>} </div> ); }
// my-real-time-app/app/components/UserStatus.js function UserStatus({ isOnline }) { return ( <div> {isOnline ? <span className="online-indicator"></span> : <span className="offline-indicator"></span>} </div> ); }
// Example: Setting up compression with a WebSocket server const WebSocket = require('ws'); const wss = new WebSocket.Server({ perMessageDeflate: { zlibDeflateOptions: { // Add compression options here } } });
// Example: Simple retry logic with fetch let retries = 3; function fetchData(url) { fetch(url) .then(response => response.json()) .catch(error => { if (retries > 0) { retries--; fetchData(url); } else { console.error('Failed to fetch data after 3 retries'); } }); }
Remarques : Le succès continu de my-real-time-app
repose non seulement sur ses fonctionnalités de base, mais également sur les améliorations subtiles et les optimisations constantes qui garantissent une expérience utilisateur sans friction. En incorporant les stratégies énumérées ci-dessus, nous sommes sur le point d'offrir à nos utilisateurs une expérience de chat supérieure, fiable et agréable.
Notre parcours avec my-real-time-app
nous a conduits depuis la configuration initiale avec Next.js 13.4, jusqu'à la création de backend avec des actions de serveur, la conception d'une expérience frontale transparente et la garantie que les capacités en temps réel ont été testées et optimisées. Nous avons approfondi les nuances des composants serveur et client, garantissant un équilibre efficace entre le rendu côté serveur et l'interactivité côté client.
Bien que my-real-time-app
ait parcouru un long chemin, le potentiel d'améliorations futures reste vaste :
Tout d'abord, un immense merci d'avoir voyagé avec moi dans ce labyrinthe complexe du monde Next.js. Si vous êtes arrivé jusqu'ici, félicitations ! Si vous avez survolé certaines parties, je ne vous en veux pas – il y a eu des moments où j'ai voulu ne pas les écrire !
Avez-vous déjà eu ces moments où vous passez des heures à déboguer un problème, pour vous rendre compte que vous avez manqué un point-virgule ? Ou lorsque vous supprimez accidentellement une partie essentielle de votre code et que vous souhaitez que la vie ait un Ctrl + Z ? Ah les joies de la programmation !
Alors la prochaine fois que votre code refuse de coopérer, respirez profondément, prenez un café (ou un thé, je ne juge pas, je suis moi-même un fan de matecocido ), et rappelez-vous que vous n'êtes pas seul dans ce cas.