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

Jahia est un CMS Java rendu unique par sa capacité à faire le rendu et hydrater des composants React côté serveur et côté client, et par la façon dont nous avons implémenté l'architecture de l'île en Java. Le but de cet article est d'expliquer comment nous avons développé ces fonctionnalités avec une précision technique élevée.

Cet article est volontairement technique, et n'a pas vocation à être un tutoriel. Le but est de jeter un coup d'œil sous le capot de notre implémentation, et de comprendre comment nous l'avons fait fonctionner. Si vous cherchez une introduction plus pratique à l'architecture en îlot, nous vous recommandons de lire notre précédent article sur le sujet, "Tirer parti de l'architecture en îlots dans Jahia CMS".

En tant que CMS open-source, nous attachons une grande importance au partage et à la documentation de notre travail. Toutes les techniques utilisées dans cet article sont mises en oeuvre dans les JavaScript Modules Jahia, et utilisées en production par nous et nos clients.

Exécution de JavaScript en Java

Nous avons implémenté le moteur de modules JavaScript dans Jahia au-dessus du 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 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!');");
  }
}

L'exécution de JavaScript dans un unique contexte d’évaluation crée nombre de défis, les projets JavaScript n’étant jamais écrits comme un seul fichier, mais plutôt comme une collection de modules. L'opération consistant à transformer une collection de modules en un seul fichier est appelée bundling. Le bundler que nous utilisons dans l'écosystème des modules JavaScript est Vite, mais pour le reste de cet article, nous utiliserons Rolldown car nous n'avons pas besoin d'autant de tout ce que propose Vite.
Avec la bonne configuration de context, vous pouvez permettre des interactions entre Java et JavaScript. C'est ainsi que nous partageons des bibliothèques communes entre différents modules JavaScript et que nous avons ajouté la possibilité d'appeler des services OSGi à partir de code JavaScript.

Exécuter React en JavaScript dans Java

Nous allons essayer de construire une version de base du processus de rendu côté serveur, suivi du processus d'hydratation.

L'hydratation d'un composant React consiste à attacher des récepteurs d'événements (event listeners) à des éléments HTML existants et à effectuer un nouveau rendu du composant lorsque l'état change.
Commençons par le rendu d'un composant React sur le serveur. Nous voulons rendre une page Hello World basique, écrite en React, et sortir le document HTML sous forme de chaîne de caractères. Le code 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!');");
  }
}

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 imprimée dans la console, comme nous le faisons ici).

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

L'utilisation de Rolldown pour regrouper le code peut se faire en une seule commande : npx rolldown@canary index.jsx -o dist/index.js. (Si vous souhaitez réellement exécuter cette commande, 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 déclarations d'importation ont été remplacées par le code réel des modules que nous avons importés. Le regroupement permet de contenir tout le code dans un seul fichier, ce dont nous avons besoin pour l'exécuter dans 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 un 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 de response à la fin !

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

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

Super ! 🎉

Ce document HTML valide peut maintenant être envoyé au navigateur, ce qui signifie que nous avons rendu avec succès un composant React sur le serveur ! La prochaine étape consiste à l'hydrater côté client.

Hydratation des composants React sur le client

Étendons notre exemple précédent pour ajouter un composant dynamique qui sera hydraté côté client. Il s'agit d'un 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'; // Import the Button component

const page = (
  <html>
    <head>
      <title>My page</title>
    </head>
    <body>
      <h1>Hello World!</h1>
      {/* This button will be rendered on the server, hydrated on the client: */}
      <div data-hydrate>
        <Button />
      </div>
      {/* This script will be executed on the 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 regroupé et transpilé, ressemble à ceci :

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

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

Remarquez que nous importons le même composant Button que nous avons utilisé sur le serveur... C'est la clé de l'architecture en îlots : nous utilisons le même composant React sur le serveur et le client. C'est l'un des nombreux avantages de l'hydratation par rapport au Server-Side Rendering (SSR) avec une touche de jQuery : avoir le code du serveur et du client dans le même langage permet d'avoir une base de code plus facile à maintenir au long terme.

hydrateRoot est une fonction du module react-dom/client qui unifie un composant React rendu côté serveur avec son équivalent côté client. Elle prend deux arguments : l'élément DOM à hydrater, et le composant React à rendre. Ce qu'il fait en pratique est d'attacher des récepteurs d'événements aux éléments HTML existants, et de rendre à nouveau 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 regrouper séparément :

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

Nous ne passerons pas en revue les fichiers, ils ne sont pas très lisibles, mais ils contiennent tout ce qui est nécessaire pour fonctionner sur le serveur et le client respectivement.

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

dist/index.js :
# Sauvegarder la sortie HTML dans un fichier
node dist/index.js > dist/index.html
# Exposer le répertoire `dist` en utilisant un serveur de fichiers statique
npx sirv-cli dist

Ouvrir localhost:8080 affichera une magnifique page "Hello World !" avec un bouton. En cliquant sur le bouton, une alerte s'affiche : " Bouton cliqué !

interactive-hello-world.png

Le bouton a été correctement hydraté (✨), laissant le reste de la page intacte. Nous avons recréé le processus d'hydratation partielle que nous avions décrit dans notre précédent article sur l'architecture en ilôts.

Conclusion

Bien que plus complexe, l'implémentation que nous avons réalisée chez Jahia fonctionne à peu près de la même manière ; nous utilisons un attribut data-hydration-mode pour identifier les éléments qui doivent être hydratés, et non la page entière. Nous utilisons également un importmap pour partager les bibliothèques entre tous les composants côté client, afin d'éviter d'envoyer 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é l'architecture insulaire dans Jahia. Si vous souhaitez en savoir plus sur les modules JavaScript, nous vous recommandons de lire notre introduction aux modules JavaScript 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