Intégrer les avantages des micro-front-ends aux applications Web traditionnelles
Nous avons récemment écrit un article au sujet d'une nouvelle architecture par fragments qui permet de créer des applications en garantissant rapidité, rentabilité et évolutivité pour les projets de plus grande envergure, tout en proposant un cycle d'itération facile. Cette méthode implique la collaboration de nombreux Workers Cloudflare pour rendre et diffuser des micro-front-ends dans une application interactive plus rapidement qu'avec les méthodes côté client. Il en résulte une amélioration de l'expérience utilisateur et des scores SEO.
Cette méthode est parfaite si vous démarrez un nouveau projet ou si vous êtes en mesure de réécrire entièrement votre application actuelle. Mais en réalité la plupart des projets sont trop gros pour une reconstruction intégrale et ne peuvent adopter des modifications architecturales que par incréments.
Dans cet article, nous proposons une technique pour ne remplacer qu'une sélection d'éléments d'une application traditionnelle utilisant un rendu côté client par des fragments utilisant un rendu côté serveur. Dans l'application ainsi obtenue, les vues les plus importantes sont plus rapidement interactives, peuvent être développées indépendamment et bénéficient de tous les avantages d'une démarche micro-front-end, sans qu'il soit nécessaire de réécrire de grandes portions de la base de code héritée. Cette méthode ne dépend d'aucune infrastructure ; dans cet article, nous présentons des fragments créés avec React, Qwik et SolidJS.
La lourdeur des applications front-end de grande envergure
Nombreuses sont les grandes applications front-end développées aujourd'hui qui peinent à proposer une expérience utilisateur satisfaisante. Le problème vient souvent d'architectures qui exigent le téléchargement, l'analyse et l'exécution de grands volumes JavaScript avant que les utilisateurs ne puissent interagir avec l'application. Malgré les efforts déployés pour différer le chargement du code JavaScript avec le lazy loading, et l'utilisation du rendu côté serveur, ces applications volumineuses tardent toujours trop à devenir interactive et à répondre aux utilisateurs.
Qui plus est, les grandes applications monolithiques peuvent être complexes à créer et à déployer. De nombreuses équipes peuvent être amenées à collaborer sur une unique base de code et les efforts nécessaires à la coordination des tests et du déploiement du projet ne facilitent pas le déploiement et l'itération des fonctionnalités individuelles.
Comme nous l'avons mentionné dans notre article précédent, les micro-front-ends fournis par Cloudflare Workers peuvent résoudre ces problèmes mais la conversion d'une application monolithe en une architecture micro-front-end peut s'avérer complexe et onéreuse. Cela peut prendre des mois, voire des années en temps de développement avant que les utilisateurs ou les développeurs n'en constatent les avantages.
Nous avons besoin d'une méthode qui permette à un projet d'adopter des micro-front-ends graduellement pour les éléments les plus influents de l'application, sans qu'il soit nécessaire de réécrire toute l'application d'un coup.
Les fragments à la rescousse
L'objectif d'une architecture reposant sur les fragments est de réduire considérablement la latence du chargement et des interactions pour les applications web volumineuses (selon les mesures des signaux web essentiels) en décomposant l'application en micro-front-ends qui peuvent être rapidement rendus (et mis en cache) dans Cloudflare Workers. Toute la difficulté réside dans le moyen d'intégrer un fragment micro-front-end dans une application traditionnelle qui utilise le rendu côté client en limitant au minimum les coûts pour le projet initial.
La technique que nous proposons nous permet de convertir les éléments les plus précieux d'une IU d'application traditionnelle, en l'isolant du reste de l'application.
Il s'avère que dans beaucoup d'applications, les éléments les plus précieux d'une IU sont souvent imbriqués dans une interface d'application de type « shell » qui propose un en-tête, un pied de page et des éléments de navigation. Les formulaires de connexion, le volet de description d'un produit dans une application de commerce électronique, la boîte de réception d'un client e-mail en sont quelques exemples parmi d'autres.
Prenons l'exemple du formulaire de connexion. S'il faut plusieurs secondes à l'application pour afficher le formulaire de connexion, les utilisateurs vont redouter le moment de la connexion et on risque de les perdre. Il est toutefois possible de convertir le formulaire de connexion en un fragment rendu côté serveur, qui s'affiche et devient interactif immédiatement, pendant que le reste de l'application traditionnelle prend le temps de démarrer en arrière-plan. Le fragment étant rapidement interactif, l'utilisateur peut même entrer ses identifiants avant que l'application traditionnelle n'ait démarré et que le rendu du reste de la page soit affiché.
L'animation affichant le formulaire de connexion est disponible avant l'application principale
Cette méthode permet aux équipes d'ingénieurs d'apporter des améliorations précieuses aux utilisateurs en une fraction du temps et des coûts d'ingénierie qu'exigeraient les méthodes traditionnelles, qui soit sacrifient les améliorations de l'expérience utilisateur, soit nécessitent une réécriture longue et très risquée de l'application entière. Elle permet aux équipes disposant d'applications monolithiques à page unique d'adopter une architecture micro-front-end de manière graduelle, de cibler les améliorations sur les parties les plus précieuses de l'application, et donc d'accélérer le retour sur investissement.
L'aspect complexe mais intéressant de l'extraction des éléments d'IU pour en tirer des fragments rendu côté serveur tient au fait que nous cherchons à ce que, une fois affichés dans le navigateur, l'application traditionnelle et les fragments donnent l'impression de n'être qu'une seule et même application. Les fragments doivent être habilement intégrés dans le shell de l'application traditionnelle, ce qui permet de garder l'application accessible en respectant la hiérarchie DOM, mais nous voulons également que les fragments rendus côté serveur soient affichés et deviennent interactifs aussi rapidement que possible, avant même que le shell de l'application traditionnelle rendue côté client n'existe. Comment intégrer des fragments d'IU dans un shell d'application qui n'existe pas encore ? Nous avons résolu ce problème à l'aide d'une technique que nous avons imaginée, et que nous appelons l'« insertion de fragments ».
Insertion de fragments
L'insertion de fragments combine les HTML/DOM produits par les fragments micro-front-end rendus côté serveur avec ceux produits par l'application traditionnelle rendue côté client.
Les fragments micro-front-end sont rendus directement dans le niveau supérieur de la réponse HTML et sont conçus pour devenir immédiatement interactifs. En arrière-plan, l'application traditionnelle est rendue côté client comme une jumelle de ces fragments. Lorsque la procédure est prête, les fragments sont « insérés » dans l'application traditionnelle (le DOM de chaque fragment est placé correctement dans le DOM de l'application traditionnelle) sans la moindre incidence visuelle, ni la moindre perte d'état côté client concernant l'élément actif, les données de formulaires ou la sélection de données. Une fois « inséré », un fragment peut commencer à communiquer avec l'application traditionnelle dont il fait désormais partie intégrante.
Voici un fragment de « connexion » et l'élément « root » de l'application traditionnelle vide en haut du DOM, avant l'intégration.
<body>
<div id="root"></div>
<piercing-fragment-host fragment-id="login">
<login q:container...>...</login>
</piercing-fragment-host>
</body>
Et ici vous pouvez constater que le fragment a été inséré dans l'élément div de la page de connexion dans l'application traditionnelle rendue.
<body>
<div id="root">
<header>...</header>
<main>
<div class="login-page">
<piercing-fragment-outlet fragment-id="login">
<piercing-fragment-host fragment-id="login">
<login q:container...>...</login>
</piercing-fragment-host>
</piercing-fragment-outlet>
</div>
</main>
<footer>...</footer>
</div>
</body>
Pour empêcher le fragment de bouger et de modifier la présentation visuelle pendant la transition, nous appliquons des styles CSS qui placent le fragment à l'identique avant et après l'insertion.
À tout moment une application peut afficher un certain nombre de fragments insérés, ou aucun. Cette technique ne se limite pas uniquement au chargement initial d'une application traditionnelle. Les fragments peuvent également être ajoutés ou retirés d'une application à tout moment. Cela permet de rendre les fragments en réponse à des interactions d'utilisateur et à un routage côté client.
Avec l'insertion de fragments, nous pouvez commencer à adopter des micro-front-ends de manière graduelle, un fragment à la fois. Vous décidez de la granularité des fragments et des éléments de l'application que vous souhaitez fragmenter Il n'est pas nécessaire que les fragments utilisent tous la même infrastructure Web, ce qui peut-être utile lors du changement de piles, ou lors d'une intégration après achat de multiples applications.
Démo de la « Producitivy Suite »
À des fins de démonstration d'une insertion de fragment et d'adoption graduelle nous avons mis au point une application de démo « productivity suite » qui permet aux utilisateurs de gérer des listes de tâches, de s'informer de l'actualité concernant les pirates, etc. Nous avons mis en œuvre le shell de cette application en tant qu'application React rendue côté client ; un choix technique courant dans les applications d'entreprise. Il s'agit de notre « application traditionnelle ». Trois itinéraires ont été mis à jour dans l'application pour l'utilisation de fragments micro-front-end :
/login
- un simple formulaire de connexion fictif avec la validation côté client, affiché lorsque les utilisateurs ne sont pas authentifiés (mis en œuvre dans Qwik)./todos
- gère une ou plusieurs listes de tâches, mis en œuvre en tant que deux fragments oeuvrant en collaboration :/news
- un clone de la démo HackerNews ws (mis en œuvre dans SolidJS).
Cette démo illustre le fait que différentes technologies indépendantes peuvent être utilisées à la fois pour l'application traditionnelle et pour chacun des fragments.
Vue des fragments insérés dans l'application traditionnelle
L'application est déployée sur https://productivity-suite.web-experiments.workers.dev/.
Pour essayer, vous devez d'abord vous connecter (utilisez simplement le nom d'utilisateur de votre choix, pas besoin de mot de passe). Les données de l'utilisateur sont sauvegardées dans un cookie, vous pouvez donc vous déconnecter et reconnecter avec le même nom d'utilisateur. Une fois que vous êtes connecté, circulez dans les différentes pages à l'aide de la barre de navigation en haut de l'application. Consultez en particulier les pages « Todo Lists » et « News » pour voir l'insertion en action.
À tout moment, essayez de recharger la page pour observer le rendu instantané des fragments tandis que l'application traditionnelle se charge lentement en arrière-plan. Essayez d'interagir avec les fragments avant même que l'application traditionnelle n'apparaisse.
Tout en haut de la page, des contrôles vous permettent de constater l'incidence de l'insertion de fragment en action.
Utilisez le curseur « Legacy app bootstrap delay » pour régler le délai simulé avant le démarrage de l'application traditionnelle.
Activez/désactivez la case « Piercing Enabled » pour comparer ce que serait l'expérience utilisateur en l'absence de fragments dans l'application.
Activez/désactivez « Show Seams » pour observer où se trouve chaque fragment dans la page actuelle.
Fonctionnement
L'application est composée d'un certain nombre d'éléments constitutifs.
Aperçu des Workers collaborateurs et de l'hôte d'application traditionnelle
L'hôte d'application traditionnelle de notre démo sert les fichiers qui définissent l'application React côté client (HTML, JavaScript et les feuilles de style). Les applications créées avec d'autres piles technologiques fonctionnent tout aussi bien. Les Fragment Workers hébergent les fragments micro-front-end, tel que nous l'avons décrit dans notre précédent article sur l'architecture fragmentée. Et le Gateway Worker gère les demandes en provenance du navigateur et sélectionne, récupère et combine les flux de réponses provenant de l'application traditionnelle et des fragments micro-front-end.
Une fois que ces éléments sont tous déployés, ils collaborent pour traiter chaque requête en provenance du navigateur. Observons ce qui se passe lorsque vous accédez à l'itinéraire `/login`.
Flux de requêtes lors de la visualisation de la page de connexion.
L'utilisateur accède à l'application et le navigateur effectue une requête auprès du Gateway Worker pour obtenir le code HTML initial. Le Gateway Worker comprend que le navigateur demande la page de connexion. Il effectue ensuite deux sous-requêtes parallèles. Une pour récupérer l'index.html de l'application traditionnelle et l'autre pour demander le fragment de connexion rendu côté serveur. Il combine ensuite ces deux réponses dans un flux de réponse unique qui contient le code HTML remis au serveur.
La navigateur affiche la réponse HTML contenant l'élément racine (root) vide pour l'application traditionnelle et le fragment de connexion rendu côté serveur, qui est immédiatement interactif pour l'utilisateur.
La navigateur demande ensuite le code JavaScript de l'application traditionnelle. La requête est mise en proxy par le Gateway Worker pour l'hôte d'application traditionnelle. De la même façon, n'importe quelle autre ressource à destination de l'application traditionnelle ou de fragments est acheminée par Gateway Worker vers l'hôte d'application traditionnelle ou le Fragment Worker approprié.
Une fois que le code JavaScript de l'application traditionnelle a été téléchargé et exécuté, mettant ainsi le shell de l'application en production, l'insertion de fragments entre en jeu, plaçant le fragment à l'emplacement attendu dans l'application traditionnelle, sans interférer avec l'état de l'interface utilisateur.
Certes nous sommes concentrés sur le fragment de connexion pour expliquer l'insertion de fragments, toutefois, la même logique s'applique aux autres fragments mis en œuvre dans /todos
and /news
.
Bibliothèque d'insertion
Même s'ils sont mis en œuvre à l'aide de différentes infrastructures web, l'ensemble des fragments est intégré dans l'application traditionnelle de la même façon avec les mêmes commandes d'assistance d'une « bibliothèque d'insertion ». Cette bibliothèque est une collection d'utilitaires côté serveur et côté client que nous avons développés pour les fins de cette démo et qui servent à l'intégration de l'application traditionnelle avec des fragments micro-front-end. Les principales fonctionnalités de la bibliothèque sont la classe PiercingGateway
, les éléments personnalisés fragment hosts et fragment outlet, et la classe MessageBus
.
PiercingGateway
La classe PiercingGateway
peut être utilisée pour instancier un Gateway Worker qui traite toutes les requêtes pour le code HTML de notre application, JavaScript et d'autres ressources. PiercingGateway achemine les requêtes via les Fragment Workers appropriés ou vers l'hôte de l'application traditionnelle. Elle combine également les flux de réponse HTML issus de ces fragments avec la réponse provenant de l'application traditionnelle en un flux HTML unique renvoyé vers le navigateur.
La mise en œuvre d'un Gateway Worker est simple si vous vous servez de la bibliothèque d'insertion. Créez une nouvelle instance gateway
de PiercingGateway
, en lui passant l'URL de l'hôte d'application traditionnelle et une fonction pour déterminer si l'insertion est activée pour la requête donnée. Exportez la gateway
comme exportation par défaut à partir du script Worker afin que l'environnement d'exécution de Workers puisse connecter son handler fetch()
.
const gateway = new PiercingGateway<Env>({
// Configure the origin URL for the legacy application.
getLegacyAppBaseUrl: (env) => env.APP_BASE_URL,
shouldPiercingBeEnabled: (request) => ...,
});
...
export default gateway;
Les fragments peuvent être enregistrés sur appel de la méthode registerFragment()
pour que gateway
achemine automatiquement les requêtes concernant le code HTML et les ressources d'un fragment vers le Fragment Worker correspondant. Voici un exemple de présentation d'enregistrement de fragment :
gateway.registerFragment({
fragmentId: "login",
prePiercingStyles: "...",
shouldBeIncluded: async (request) => !(await isUserAuthenticated(request)),
});
Hôte et sortie de fragment
Les requêtes de routage et la combinaison de réponses HTML dans le Gateway Worker ne représentent que la moitié de ce qui rend l'insertion possible. L'autre doit se produire dans la navigateur où les fragments doivent être insérés dans l'application traditionnelle selon la technique décrite précédemment.
L'insertion de fragment dans le navigateur est facilitée par une paire d'éléments personnalisés, l'hôte de fragment (<piercing-fragment-host>
) et la sortie de fragment (<piercing-fragment-outlet>
).
Le Gateway Worker enveloppe le code HTML de chaque fragment dans l'hôte de fragment. Dans le navigateur, l'hôte de fragment gère la durée de vie du fragment et il est utilisé lors du placement du DOM du fragment dans l'application traditionnelle.
<piercing-fragment-host fragment-id="login">
<login q:container...>...</login>
</piercing-fragment-host>
Dans l'application traditionnelle, le développeur indique où doit apparaître un fragment lorsqu'il est inséré par l'ajout d'une sortie de fragment. L'itinéraire de connexion de notre application de démo se présente comme suit :
export function Login() {
…
return (
<div className="login-page" ref={ref}>
<piercing-fragment-outlet fragment-id="login" />
</div>
);
}
Lorsqu'une sortie de fragment est ajoutée au DOM, elle cherche le document actuel pour l'hôte de fragment qui lui est associé. Si la recherche est fructueuse, l'hôte de fragment et son contenu sont placés dans la sortie. Si l'hôte de fragment est introuvable, la sortie envoie une requête au worker de passerelle pour récupérer le code HTML du fragment, qui est ensuite transmis directement dans la sortie du fragment, avec la bibliothèque writable-dom (une bibliothèque modeste mais puissante développée par l'équipe de MarkoJS).
Ce mécanisme de secours rend possible la navigation côté serveur vers des itinéraires qui contiennent de nouveaux fragments. Ainsi les fragments peuvent être rendus dans le navigateur à la fois par la navigation initiale (hard) et par la navigation côté serveur (soft).
Bus de publication de messages
À moins que les fragments de notre application ne soient que des présentations ou soient totalement autonomes, ils doivent également communiquer avec l'application traditionnelle et d'autres fragments. Le [MessageBus](https://github.com/cloudflare/workers-web-experiments/blob/df50b60cfff7bc299cf70ecfe8f7826ec9313b84/productivity-suite/piercing-library/src/message-bus/message-bus.ts#L18)
est un bus de communication simple, asynchrone, isomorphe et indépendant de l'infrastructure, auquel l'application traditionnelle et chacun des fragments peuvent accéder.
Dans notre application de démo, le fragment de connexion doit informer l'application traditionnelle lorsque l'utilisateur est authentifié. Ce message dispatch est mis en oeuvre dans le composant Qwik LoginForm
comme suit :
const dispatchLoginEvent = $(() => {
getBus(ref.value).dispatch("login", {
username: state.username,
password: state.password,
});
state.loading = true;
});
L'application traditionnelle peut alors écouter ces messages comme suit :
useEffect(() => {
return getBus().listen<LoginMessage>("login", async (user) => {
setUser(user);
await addUserDataIfMissing(user.username);
await saveCurrentUser(user.username);
getBus().dispatch("authentication", user);
navigate("/", { replace: true, });
});
}, []);
Nous avons opté pour la mise en œuvre du bus de messages car nous avions besoin d'une solution indépendante de toute infrastructure et fonctionnant aussi bien sur le serveur que sur le client.
Lancez-vous !
Avec les fragments, l'insertion de fragment et Cloudflare Workers, vous pouvez améliorer les performances ainsi que le cycle de développement des applications traditionnelles utilisant le rendu côté client. Ces modifications peuvent être adoptées de manière graduelle, et vous pouvez même procéder ainsi tout en mettant en œuvre des fragments avec l'infrastructure web de votre choix.
Vous trouverez l'application « Productivity Suite » illustrant ces possibilités à l'adresse https://productivity-suite.web-experiments.workers.dev/.
L'ensemble du code que nous avons affiché est un code open source et a été publié sur Github : https://github.com/cloudflare/workers-web-experiments/tree/main/productivity-suite.
N'hésitez pas à cloner le référentiel. Il est facile à exécuter en local et même lorsqu'il s'agit de déployer vos propres versions (gratuitement) sur Cloudflare. Nous avons essayé de rendre le code aussi réutilisable que possible. L'essentiel de la logique centrale se trouve dans la bibliothèque d'insertion que vous pouvez essayer dans vos propres projets. Nous serions ravis de recevoir vos commentaires et suggestions ou de savoir avec quelles applications vous souhaiteriez l'utiliser. Rejoignez notre discussion GitHub ou contactez-nous sur notre canal discord.
Nous pensons qu'en associant Cloudflare Workers aux idées les plus modernes en matière d'infrastructures nous pourrons donner l'élan nécessaire aux prochaines grandes avancées qui enrichiront l'expérience des utilisateurs et des développeurs dans les applications Web. Attendez-vous à voir plus de démos, d'articles de blog et de collaborations à mesure que nous allons continuer à repousser les limites de ce que le Web peut offrir. Et si vous souhaitez également faire directement partie de l'aventure, nous informons avec enthousiasme que nous recrutons !