Sous le capot : Hydratation des composants React en Java

Under the Hood Hydrating React Components in Java.jpg
Jahia est la seule Digitale eXperience Plateforme qui vous permet réellement de proposer des parcours personnalisés optimisés par les données clients. En savoir plus

Sous le capot : Hydratation des composants React en Java

Jahia est un CMS Java unique par sa capacité à rendre et hydrater les composants React côté serveur et client, et par la manière dont nous avons implémenté la conception en archipel en Java. Le but de cet article est d'expliquer comment nous avons construit ces fonctionnalités avec des détails techniques approfondis.

Cet article est volontairement technique et n'est pas destiné à être un tutoriel. L'objectif est de jeter un œil sous le capot de notre implémentation et de comprendre comment nous l'avons fait fonctionner. Si vous recherchez une introduction plus pratique à la conception en archipel, nous vous recommandons de lire notre article précédent Tirer parti de la conception en archipel dans Jahia CMS sur le sujet.

En tant que CMS open-source, nous attachons de l'importance au partage et à la documentation de notre travail. Toutes les techniques utilisées dans cet article sont réellement implémentées dans le dépôt JavaScript Modules de Jahia, et utilisées en production par nous et nos clients.

Exécuter JavaScript en Java

Nous avons implémenté le moteur JavaScript Modules dans Jahia par-dessus le moteur GraalVM. Il permet d'exécuter du code JavaScript dans un environnement Java et offre une bonne interopérabilité entre Java et JavaScript.

Le code minimal pour exécuter un extrait de JavaScript en Java ressemble à ceci :

import org.graalvm.polyglot.*;

public class JavaScriptRunner {
  public static void main(String[] args) {
    Context context = Context.newBuilder("js").build();
    context.eval("console.log('Hello from JavaScript!');");
  }
}

Exécuter JavaScript comme une simple évaluation crée tout un monde de défis. Les projets JavaScript ne sont jamais écrits en un seul fichier, mais plutôt comme une collection de modules. L'opération de transformation d'une collection de modules en un seul fichier s'appelle le bundling. Le bundler que nous utilisons dans l'écosystème des JavaScript Modules est Vite, mais pour le reste de cet article, nous utiliserons Rolldown car nous n'avons pas besoin d'autant de fonctionnalités que Vite en fournit.

Avec la bonne configuration de context, vous pouvez activer les interactions entre Java et JavaScript. C'est ainsi que nous partageons des bibliothèques communes entre différents modules JavaScript, et comment nous avons ajouté la capacité d'appeler des services OSGi depuis JavaScript.

Exécuter React en JavaScript en Java

Nous allons essayer de construire une version de base du processus de rendu côté serveur, suivie du processus d'hydratation. Hydrater un composant React signifie attacher des écouteurs d'événements à des éléments HTML existants, et re-rendre le composant lorsque l'état change.

Commençons par rendre un composant React sur le serveur. Nous voulons rendre une page "Hello World" de base, écrite en React, et produire le document HTML sous forme de chaîne de caractères. Le code ressemble à ceci :

// `renderToString` transforme un composant React en une chaîne de caractères HTML
import { renderToString } from 'react-dom/server.edge';

// Composant de la page Hello World!
const page = (
  <html>
    <head>
      <title>My page</title>
    </head>
    <body>
      <h1>Hello World!</h1>
    </body>
  </html>
);

// Transforme le composant React en une chaîne de caractères HTML
const response = '<!DOCTYPE html>' + renderToString(page);
console.log(response);

La fonction renderToString fait partie du module react-dom/server.edge, qui est une API de rendu côté serveur fournie par React. Elle transforme un arbre de composants React en une chaîne de caractères HTML, qui peut ensuite être envoyée au navigateur (ou affichée dans la console, comme nous le faisons ici).

Nous ne pouvons pas exécuter ce code directement en Java, car il utilise des instructions import et la syntaxe JSX. Nous devons le transpiler dans un format qui peut être exécuté par GraalVM.

Utiliser Rolldown pour bundler le code peut se faire en une seule commande : npx rolldown@canary index.jsx -o dist/index.js. (Si vous voulez vraiment exécuter cela, vous devrez créer un projet Node.js avec react et react-dom comme dépendances.)

Cela produira le code suivant :

//#region node_modules/react-dom/server.edge.js
var require_server_edge = __commonJS({
  'node_modules/react-dom/server.edge.js'(exports) {
    // ...
  },
});
//#endregion

//#region node_modules/react/jsx-runtime.js
var require_jsx_runtime = __commonJS({
  'node_modules/react/jsx-runtime.js'(exports, module) {
    // ...
  },
});
//#endregion

//#region index.jsx
var { renderToString } = __toESM(require_server_edge());
var { jsxs, jsx } = __toESM(require_jsx_runtime());
const page = jsxs('html', {
  children: [
    jsx('head', {
      children: jsx('title', { children: 'My page' }),
    }),
    jsx('body', {
      children: jsx('h1', { children: 'Hello World!' }),
    }),
  ],
});
const response = '<!DOCTYPE html>' + renderToString(page);
console.log(response);
//#endregion

C'est à peine lisible, mais décomposons-le :

  • Nos instructions d'importation ont été remplacées par le code réel des modules que nous avons importés. Le bundling permet de livrer tout le code dans un seul fichier, ce dont nous avons besoin pour l'exécuter en une seule évaluation dans GraalVM.

  • La syntaxe JSX a été remplacée par des appels de fonction. Par exemple, <h1>Hello World!</h1> a été remplacé par jsx('h1', { children: 'Hello World!' }). C'est ce qu'on appelle la transpilation : JSX n'est pas du JavaScript valide, nous devons donc le convertir dans un format valide qui peut être exécuté par le moteur JavaScript. React a choisi d'utiliser des appels de fonction pour représenter les éléments JSX.

Nous pouvons voir notre variable response à la fin !

L'exécution de ce code dans GraalVM (ou tout autre moteur JavaScript en fait, nous avons supprimé tout code spécifique à l'environnement d'exécution) produira la sortie suivante :

<!DOCTYPE html>
<html>
  <head>
    <title>My page</title>
  </head>
  <body>
    <h1>Hello World!</h1>
  </body>
</html>

Yay ! 🎉

Ce document HTML valide peut maintenant être envoyé au navigateur, ce qui signifie que nous avons réussi à rendre un composant React sur le serveur ! La prochaine étape est de l'hydrater côté client.

Hydrater les composants React côté client

Étendons notre exemple précédent pour ajouter un composant dynamique qui sera hydraté côté client. Ce sera ce simple bouton :

export default function Button() {
  return (
    <button
      onClick={() => {
        alert('Button clicked!');
      }}
    >
      Click me
    </button>
  );
}

Nous avons maintenant besoin de deux sorties : une qui s'exécute sur le serveur, et une qui s'exécute sur le client. Nous pouvons mettre à jour notre code serveur initial pour inclure notre composant bouton :

import { renderToString } from 'react-dom/server.edge';
import Button from './Button.jsx'; // Importe le composant Button

const page = (
  <html>
    <head>
      <title>My page</title>
    </head>
    <body>
      <h1>Hello World!</h1>
      {/* Ce bouton sera rendu sur le serveur, hydraté sur le client : */}
      <div data-hydrate>
        <Button />
      </div>
      {/* Ce script sera exécuté sur le client : */}
      <script src="client.js"></script>
    </body>
  </html>
);

const response = '<!DOCTYPE html>' + renderToString(page);
console.log(response);

Le code client (référencé dans la balise <script>), avant d'être bundlé et transpilé, ressemble à ceci :

import { hydrateRoot } from 'react-dom/client';
import Button from './Button.jsx';

hydrateRoot(document.querySelector('[data-hydrate]'), <Button />);

Remarquez comment nous importons le même composant Button que nous avons utilisé sur le serveur... C'est la clé de la conception en archipel : nous utilisons le même composant React à la fois sur le serveur et sur le client. C'est l'un des nombreux avantages de l'hydratation par rapport au rendu côté serveur (SSR) avec un peu de jQuery : avoir le code serveur et client dans le même langage permet une base de code plus maintenable à long terme.

hydrateRoot est une fonction du module react-dom/client qui unifie un composant React rendu côté serveur avec son homologue côté client. Elle prend deux arguments : l'élément DOM à hydrater, et le composant React à rendre. Ce qu'elle fait en pratique, c'est attacher des écouteurs d'événements aux éléments HTML existants, et re-rendre le composant lorsque l'état change.

Nous avons maintenant deux points d'entrée : index.jsx pour le serveur, et client.jsx pour le client. Nous devons les bundler séparément :

npx rolldown@canary index.jsx -o dist/index.js
npx rolldown@canary client.jsx -o dist/client.js

Nous n'allons pas passer en revue les fichiers — ils ne sont pas très lisibles — mais ils contiennent tout ce qui est nécessaire pour s'exécuter respectivement sur le serveur et sur le client.

Nous pouvons tester que l'hydratation fonctionne en servant le fichier HTML généré par dist/index.js :

# Enregistre la sortie HTML dans un fichier
node dist/index.js > dist/index.html
# Expose le répertoire `dist` à l'aide d'un serveur de fichiers statiques
npx sirv-cli dist

Ouvrir localhost:8080 affichera une magnifique page "Hello World!" avec un bouton. Cliquer sur le bouton affichera une alerte disant Button clicked! :

La page Hello World avec une alerte "Button clicked!"

Le bouton a été correctement hydraté (✨), laissant le reste de la page intact. Nous avons recréé le processus d'hydratation partielle que nous avons décrit dans notre article précédent sur la conception en archipel.

Conclusion

Bien que plus complexe, l'implémentation que nous avons faite chez Jahia fonctionne à peu près de la même manière ; nous utilisons un custom element jsm-island pour identifier les éléments qui doivent être hydratés, et ne pas hydrater la page entière. Nous tirons également parti d'une importmap pour partager des bibliothèques entre tous les composants côté client, afin d'éviter d'expédier React une fois par composant.

Nous espérons que cet article technique vous a intéressé et qu'il vous a aidé à comprendre comment nous avons implémenté la conception en archipel dans Jahia. Si vous souhaitez en savoir plus sur les JavaScript Modules, nous vous recommandons de lire notre introduction aux JavaScript Modules pour vous familiariser avec les bases.

Gautier Ben Aïm
Gautier Ben Aïm

Gautier Ben Aïm est Developer Advocate chez Jahia, où il œuvre comme interface entre les équipes techniques internes et externes. Il contribue à rendre les solutions CMS, DXP et DAM de Jahia plus accessibles, compréhensibles et adoptées par les intégrateurs, partenaires et clients, tout en favorisant une culture d’ingénierie ouverte et documentée.

Son domaine de prédilection est le développement web (open source !) avec une emphase particulière sur le partage et la transmission du savoir. Il ne cessera jamais d'apprendre et reste à la pointe des tendances du secteur en par une veille technologique assidue.

Il maîtrise le développement, l’architecture logicielle et la sécurité informatique, fort de ses précédentes expériences professionnelles dans des startups spécialisées en cybersécurité : sécurité offensive et cryptographie. Sa carrière est le reflet de sa passion pour l’informatique, dans laquelle il est tombé quand il était enfant.

https://www.linkedin.com/in/gautier-ben-aim

https://github.com/GauBen

Retour