Accueil / Articles

Rendu côté serveur d’une application React 18 avec Express et Babel

Convertir un projet Create React App en rendu du côté serveur avec Express et Babel : renderToString, hydrateRoot, MemoryRouter, puis voir comment Remix et Gatsby réalisent le SSR.

Éditeur : REACTAPP.TOP7 min de lecture1712 mots

Sources: A Hands-on Guide for a Server-Side Rendering React 18 App

Le rendu côté serveur pour React a beaucoup évolué depuis l’arrivée de React 18. La configuration présentée ci-dessous vise React 18, Create React App 5 et React Router 6, et part d’un projet Create React App standard qui est converti étape par étape. Le rendu doit s’exécuter sur un serveur, ce qui nécessite un processus Node. Un simple Node fonctionne, mais nodemon est pratique pendant le développement car il redémarre l’application chaque fois qu’un fichier change. Une installation globale se fait comme suit :

% npm install -g nodemon

CSR versus SSR en quelques lignes

Avec le rendu côté client (CSR), le navigateur reçoit une page HTML presque vide et c’est JavaScript qui remplit le contenu. La première charge peut être lente, mais les navigations ultérieures sont rapides, et le serveur supporte peu de charge car ce sont les bibliothèques JavaScript qui effectuent le travail. La faiblesse réside dans l’optimisation pour les moteurs de recherche, car un robot ne trouve aucun contenu statique à lire.

Avec le rendu côté serveur, le navigateur reçoit plutôt une page HTML complète déjà générée par le serveur. Cela offre des avantages en termes de SEO et la première affichage de la page est rapide. L’inconvénient, c’est que les modifications ultérieures nécessitent généralement un rechargement complet de la page, ce qui peut alourdir la charge sur le serveur.

Quelques termes connexes peuvent être utiles ici :

  • Une application single-page (SPA) utilise le CSR. Au lieu de servir des HTML distincts pour chaque route, elle affiche dynamiquement chaque route directement dans le navigateur.
  • JavaScript universel s’exécute tant du côté client que du côté serveur. Il rend l’HTML dans le navigateur comme un SPA, rend le même HTML sur le serveur et le transmet au navigateur.
  • Le code React est par nature universel, donc le code écrit pour le CSR peut gérer le SSR avec la même base de code.
  • Le SSR est en réalité plus ancien que le CSR. Il est revenu sous une forme moderne basée sur JavaScript universel, et c’est ce que les gens entendent généralement par SSR aujourd’hui.

    Une application CSR de base

    Créez un projet vierge :

    % yarn create react-app react-ssr
    % cd react-ssr
    

    Démarrez-le avec npm start. Dans l’onglet Elements du navigateur, le logo en rotation et le texte apparaissent sous forme d’HTML généré par JavaScript.

    C’est là l’exemple typique de CSR : les scripts génèrent le contenu HTML. L’onglet Network montre ce que le serveur a réellement envoyé.

    Le corps du document contient un paquet JavaScript et aucun contenu réel, de sorte qu’un moteur de recherche n’a presque rien d’utile à indexer.

    Fournir la version de production

    SSR nécessite une version de production ; donc la première étape consiste à créer un petit serveur Express qui sert le résultat compilé de React. La configuration de React prête pour la production décrite précédemment pose les bases nécessaires. Le fichier du serveur existe sous deux formes : une en CommonJS, server/index.js, et l’autre en module ES, server/index.mjs.

    Exécutez npm run build pour générer le binaire, puis lancez le serveur avec nodemon server ou nodemon server/index.mjs. L’onglet Network affiche désormais un seul bundle JavaScript (ligne 12) et du markup vide (ligne 17). Aucune génération côté serveur ne se produit encore, il s’agit donc toujours de CSR.

    Les trois étapes pour le SSR dans Express

    Faire fonctionner le SSR au sein du serveur Express nécessite trois étapes.

    Étape 1 : hydrater sur le client. ReactDOM.hydrateRoot() est similaire à ReactDOM.createRoot(), avec la signature hydrateRoot(container, element[, options]). Il est destiné à un conteneur dont l’HTML a déjà été généré par ReactDOMServer. Comme le nœud contient déjà du markup rendu côté serveur, React le conserve et n’ajoute que des gestionnaires d’événements, ce qui accélère la charge initiale. Cette appel se trouve à la ligne 7 de src/index.js.

    Étape 2 : rendre sur le serveur. React fournit l’objet ReactDOMServer pour transformer les composants en markup statique, de sorte que le navigateur reçoive une page déjà remplie de données. Étant donné que le code React est du JavaScript universel, les mêmes composants fonctionnent des deux côtés.

    Étape 3 : gérer les besoins spécifiques à la page. Chaque route peut nécessiter une attention particulière, comme le montreront les sections suivantes.

    Faire comprendre à Node le JSX

    Divers paquets permettent de mettre en œuvre le SSR. @babel/register fait partie des solutions les plus simples. Installez-le en même temps que babel-plugin-transform-assets:

    % yarn add -D @babel/register
    % yarn add -D babel-plugin-transform-assets
    

    Tous deux se trouvent dans devDependencies :

    "devDependencies": {
      "@babel/register": "^7.18.9",
      "babel-plugin-transform-assets": "^1.0.2"
    }
    

    @babel/register compile les fichiers en temps réel et peut être configuré pour transformer le JSX ainsi que les ressources. Une limite existe cependant : il ne peut pas compiler les modules ES natifs de Node.js en temps réel, car aucune API stable n’existe encore pour gérer le chargement des modules ES. C’est pourquoi les expériences SSR présentées ici utilisent des fichiers CommonJS.

    React 18 a révisé ses API serveur et les a regroupées sous react-dom/server. renderToString fonctionne toujours, mais son support de Suspense est limité. Les nouvelles API, dont renderToPipeableStream, prennent pleinement en charge Suspense sur le serveur, ainsi que le SSR par flux.

    Décortiquage du serveur SSR

    Le fichier server/index.js mis à jour est expliqué ci-dessous par numéro de ligne.

    • Les lignes 1 à 15 configurent Babel à l’aide du hook require, de sorte que les fichiers sont compilés au fur et à mesure de leur chargement.
  • La deuxième ligne sélectionne deux préréglages. @babel/preset-env permet au code d’utiliser la dernière version de JavaScript sans avoir à choisir manuellement les transformations syntaxiques pour chaque environnement cible. @babel/preset-react importe automatiquement les fonctions auxquelles JSX est compilé.
  • Les lignes 3 à 14 configurent babel-plugin-transform-assets, qui définit la manière dont les fichiers médias statiques sont transformés. Sans ce plugin, une balise svg provoque l’erreur SyntaxError: Unexpected token '<'.
  • La ligne 16 nécessite React, la ligne 17 nécessite ReactDOMServer, et la ligne 18 nécessite l’export par défaut de src/App.js.
  • L’ordre est crucial ici. app.use() à la ligne 47 doit s’exécuter après app.get() aux lignes 25 à 45. Si elle était exécutée en premier, elle servirait les fichiers statiques, y compris index.html, pour la route racine, et app.get() n’aurait jamais l’occasion de s’exécuter.

    La ligne 26 enregistre l’URL de chaque requête. Pour une compilation de Create React App, la liste ressemble à ceci :

    Request URL = /
    Request URL = /static/js/main.b92cc87b.js
    Request URL = /static/css/main.073c9b0a.css
    Request URL = /static/media/logo.06e73328.svg
    Request URL = /static/css/main.073c9b0a.css.map
    Request URL = /static/js/main.b92cc87b.js.map
    Request URL = /static/media/logo.6ce24c58023cc2f8fd88fe9d219db6c6.svg
    Request URL = /favicon.ico
    Request URL = /manifest.json
    Request URL = /logo192.png
    

    Les lignes 27 à 29 s’assurent que seule la route racine est affichée par app.get(), tandis que les ressources statiques sont transmises au middleware suivant, situé à la ligne 47.

    La ligne 30 appelle ReactDOMServer.renderToString(element) pour générer du HTML sur le serveur. Il serait naturel d’écrire directement ReactDOMServer.renderToString(), mais dans ce cas, le JSX devrait se trouver dans un fichier différent de celui où @babel-register est configuré, d’où l’utilisation d’un élément séparé.

    La ligne 31 affiche le markup généré par le serveur :

    <div class="App"><header class="App-header"><img src="static/media/logo.06e73328.svg" class="App-logo" alt="logo"/><p>Edit <code>src/App.js</code> and save to reload.</p><a class="App-link" href="https://reactjs.org" target="_blank" rel="noopener noreferrer">Learn React</a></header></div>
    

    La ligne 33 charge le index.html de production, et les lignes 34 à 44 lisent son contenu. S’il n’y a pas d’erreur, le markup généré (ligne 42) est injecté dans la balise racine, et le index.html final devient la réponse à la première requête.

    Vérifier son fonctionnement

    Démarrez nodemon server et regardez à nouveau l’onglet Network. Le document téléchargé contient désormais un markup généré par le serveur. Les lignes 17 à 27 du corps de la page contiennent toute la page, ce qui fournit aux outils SEO quelque chose de concret à analyser.

    Pour un test plus rigoureux, ouvrez chrome://settings/content/javascript et désactivez JavaScript.

    La page continue de s’afficher sans JavaScript.

    C’est bien le SSR.

    Ajouter des routes

    Une application à une seule page est simple, donc la question suivante concerne les multiples routes. Commencez par installer react-router-dom:

    % yarn add react-router-dom
    

    Ce dernier s’ajoute aux dependencies :

    "dependencies": {
      "react-router-dom": "^6.11.2"
    }
    

    Modifiez ensuite src/App.js. La ligne 1 importe React, nécessaire pour SSR. La ligne 2 importe MemoryRouter à la place de BrowserRouter. Le routeur du navigateur repose sur l’API d’historique HTML5 pushState, que Node.js ne fournit pas. MemoryRouter conserve l’historique des URL en mémoire sans affecter la barre d’adresses, ce qui convient aux tests et aux environnements autres que les navigateurs. StaticRouter provenant de react-router-dom/server est une autre option, bien qu’il ne soit pas conçu pour des routes complexes. Les lignes 34 à 40 mettent MemoryRouter en usage.

    Compiliez à nouveau avec npm run build, lancez nodemon server et visitez http://localhost:8080.

    SSR fonctionne, mais avec une particularité : l’URL affichée dans la barre d’adresses ne se met pas à jour.

    Où le SSR manuel devient problématique

    Le routage fonctionne avec cette réservation, et d’autres problématiques restent à résoudre, comme la récupération des données et Redux. Le travail du côté serveur est moins simple que celui du côté client, et chaque page peut nécessiter un traitement spécifique.

    Il n’est pas nécessaire de tout résoudre manuellement, car plusieurs frameworks et bibliothèques intègrent le SSR. Remix est un framework web full-stack qui part des fondements du web pour arriver jusqu’à l’interface utilisateur, incluant le SSR et d’autres fonctionnalités sans code générique. Son code client appelle hydrateRoot à la ligne 4, et son code serveur appelle renderToPipeableStream à la ligne 4.

    React 18 a introduit la fonction renderToPipeableStream(element, options). En lui fournissant un élément React, elle génère l’HTML initial et renvoie un flux. Ce flux dispose de la méthode pipe(res) pour envoyer les données suivantes et de abort() pour annuler la requête. Le mode suspense est pleinement pris en charge, et l’HTML contenant des contenus différés génère des flux sans effet de « pop-in » tardif, les balises <script> en ligne comblant ensuite les espaces vides.

    Dans l’onglet Network, le corps de la page d’index de Remix contient déjà toute la page, ce qui correspond à un SSR.

    Gatsby, Next.js, NuxtJS, Quasar et SvelteKit offrent également du SSR ainsi que des fonctionnalités associées directement, sans code de base à écrire, bien que chacun définit le SSR d’une manière légèrement différente. La page d’index de Gatsby affiche également le même contenu complet dans l’onglet Network.

    Gatsby est un générateur de sites statiques rapide et fiable qui utilise React pour afficher du contenu statique, et il vous permet de modifier le HTML statique pendant son rendu côté serveur.

    SSR ou rendu statique

    Lorsqu’une page ne contient que des données statiques, le rendu statique est plus rapide. Si la réponse est dynamique, l’SSR est le choix préférable. Parfois, une approche hybride convient le mieux. L’SSR s’exécute sur demande, chaque fois qu’un fichier est requis, tandis que le rendu statique a lieu une seule fois lors de la compilation. Dans les deux cas, cela est favorable au SEO, car le corps HTML contient tout le contenu.

    Résumé

    La mise en place de l’SSR pour Create React App se résume à trois éléments :

    1. Afficher le markup rendu par le serveur à l’aide de ReactDOM.hydrateRoot().
    2. Rendre les composants dans du markup statique à l’aide de l’objet ReactDOMServer, dont les API servent à la fois l’SSR et le rendu statique.
  • Gérer ce que chaque page nécessite spécifiquement.
  • Dans le cadre d’un projet réel, le choix judicieux est un framework ou une bibliothèque qui fait déjà cela.

    Want to Connect?
    
    If you are interested, check out my directory of web development articles.