Le micro-frontend, une architecture d’avenir ?

Etienne Passot
Steeple-product
Published in
8 min readFeb 13, 2024

Le micro-frontend est devenu un des sujets les plus évoqués à la machine à café des devs.

Il y a de nombreuses possibilités de mettre en place une architecture micro-frontend. On parle parfois d’iframe. Chez Steeple, on a choisi une autre méthode.

Mais avant de passer à la technique, voici un petit retour d’expérience après un peu plus de six mois d’utilisation.

Les avantages de l’architecture micro-frontend

Un des problèmes des monolithes est l’utilisation d’une seule technologie pour un projet. Cela permet aux équipes de concentrer leur apprentissage et leur montée en compétence sur une technologie et un environnement et facilite la navigation entre les différents projets.

Mais quand il faut introduire des changements structurants, comme faire une montée de version majeure de son framework, voire changer de framework ou expérimenter de nouvelles choses, ça devient très compliqué. Ou bien lorsque l’on veut déployer une nouvelle version d’une seule partie du projet, on peut être bloqué.

Dans le premier cas, un monolithe contenant plusieurs centaines voir milliers de fichiers est complexe à mettre à jour vers des versions plus récentes d’un framework qui évolue (surtout sur les technologies front qui changent rapidement).

Avec du micro-frontend, chaque brique de notre application évolue de manière autonome. On peut construire une application avec une partie en Vue 2 et une partie en Vue 3. On peut même mélanger les frameworks et faire cohabiter React et Vue par exemple. Nos briques étant plus petites, elles sont donc plus faciles à faire évoluer.

L’exemple ci-dessous est une représentation du champ des possibilités. Nous recommandons tout de même d’éviter de travailler avec trop de framework différent pour garder une unicité des environnements de développement.

Dans le second cas, lorsqu’on déploie un monolithe, c’est binaire : tout ou rien. Ce qui veut aussi dire que l’on peut TOUT casser si un déploiement ne se passe pas comme prévu. Dans un contexte micro-frontend, on limite les risques. Pour le cas de Steeple, on pourrait, par exemple, déployer et faire tomber notre application Jobs (la partie vue3 en haut à droite) sans pour autant faire tomber toute la plateforme. Un risque limité non négligeable.

Sur l’illustration ci-dessous, les deux zones encadrées en rouge représentent l’application Job. Même si celle-ci est hors service, le reste de l’application continue de fonctionner.

Un autre avantage de cette architecture est le travail en équipe. Avec différents petits projets, on limite les risques de conflits sur le travail de chaque équipe.

Le micro-frontend, c’est très bien, mais il est quand même important de noter des points de vigilance.

Les inconvénients du micro-frontend

La mise en place d’une architecture micro-frontend peut nécessiter des compétences plus larges du côté de l’équipe technique et notamment de la partie devops. Attention donc à ne pas vouloir se lancer trop vite avec un manque de connaissance sur ces sujets.

Le choix d’une telle-architecture est aussi un choix d’entreprise. Dans une logique de développement des équipes techniques et de l’élargissement du champ des compétences, c’est un choix qui peut être pertinent. En revanche, si le projet n’a pas vocation à accueillir plusieurs équipes de développement, ce choix peut très vite devenir une sérieuse problématique quant au maintien des différents environnements (surtout si vous utilisez plusieurs frameworks).

Il est donc important d’avoir conscience des problématiques organisationnelle et technique que l’on pourra rencontrer lors de la mise en place d’une architecture micro-frontend.

Mise en place d’une architecture micro-frontend sur un projet JS

Nous y sommes, la partie technique débute ici. La mise en place d’une architecture micro-frontend peut être réalisé avec plusieurs solutions techniques différentes. Nous aborderons celle adoptée par les équipes Steeple. Elle a la particularité de n’être dépendante d’aucun framework ou d’aucune librairie concernant sa mise en place. C’est-à-dire que l’on pourrait implémenter cette solution technique avec React, Angular ou même Javascript Vanilla.

Le code qui suit et la partie ops est volontairement simplifié pour se concentrer sur le protocole de chargement d’un micro-frontend dans un conteneur. L’exemple est présenté avec des environnements Vue3 (sans Typescript).

Pour commencer, nous allons instancier un nouveau projet vue3 (que nous appellerons container) à la racine de notre dépôt. Puis, un second projet (que nous appellerons my-micro-app) à l’intérieur de container.

L’arborescence devrait ressembler à quelque chose comme :

Dans un projet d’entreprise, on utiliserait plutôt un monorepo.

On va se concentrer sur my-micro-app. C’est lui qui représente le micro-frontend que l’on va mettre en place.

Créer le micro-frontend

L’objectif : rendre accessible my-micro-app depuis un environnement extérieur. Pour cela, on va modifier le point d’entrée de l’application Vue (main.js).

import { createApp } from 'vue'
import App from './App.vue'

async function initApp(el) {
const app = createApp(App)
app.mount(el)
}

window.apps = {
microApp: initApp
}

Notre fonction initApp prend en paramètre un élément HTML qui sera transmis par l’applicationcontainer qui souhaite instancier my-micro-app.

Puis, on ajoute à la globale window un objet apps qui contient notre fonction d’instanciation de notre micro-frontend.

C’est grâce à cette fonction et sa présence dans la globale window que nous pourrons lancer my-micro-app depuis notre container.

Pour vérifier que notre projet fonctionne, on complète le composant App.vue tel que :

<template>I'm the microapp</template>

Créer le conteneur des micro-fronts

Passons du côté de container. Il a pour mission de rendre disponible notre my-micro-app sur notre page web.

La première question qu’on peut se poser, c’est : comment récupérer les sources de my-micro-app?

Dans ce contexte de simplification, on va simplement utiliser le bundle de my-micro-app en le copiant dans le bundle de container.

C’est ici que vous aurez besoin de compétences transverse pour mettre en place une architecture permettant de servir les fichiers du bundle sur une url accessible depuis le conteneur dans un environnement de production.

Pour l’exemple, on va utiliser la librairie vite-plugin-static-copy qui permet de copier un dossier dans le bundle de container.

export default defineConfig({
plugins: [
...,
viteStaticCopy({
targets: [
{
src: "my-micro-app/dist/*",
dest: "my-micro-app",
},
],
}),
],
...
});

Pour tester que notre implémentation fonctionne, on peut lancer yarn build dans le dossier my-micro-app puis à la racine du projet.

Notre dossier dist devrait ressembler à ça :

Dans le dossier my-micro-app/assets, on retrouve les sources de notre my-micro-app.

Cette solution nous permet de rendre accessible ces sources. En lançant yarn dev, on peut se rendre sur l’url http://localhost:5173/dist/my-micro-app/assets/index-[hash-du-bundle].js.

Bon, tout ça, c’est intéressant, mais ça ne nous permet pas de voir apparaître notre micro-frontend. Soyez patient, on y arrive bientôt :)

Charger le script depuis le conteneur

Dans App.vue, on va créer deux fonctions : la première pour charger le script et la seconde pour lancer my-micro-app.

Pour charger le script, on va tout simplement créer un élément script auquel on va ajouter la source que l’on a récupérée plus haut.

La fonction loadApp permet de lancer la fonction microApp récupérée depuis window.apps que l’on avait instancié dans my-micro-app.

On utilise ici setInterval qui nous permet de lancer my-micro-appune fois que le script est chargé.

L’utilisation de setInterval est nécessaire car le script est chargé de facon asynchrone.

<script setup>
import { onMounted } from "vue";

const loadScript = async () => {
const script = document.createElement("script");
script.src = 'dist/my-micro-app/assets/index-[hash-du-bundle].js';
script.type = "module";
document.head.appendChild(script);
};

function loadApp() {
const intervalId = setInterval(() => {
if (window.apps && window.apps.microApp) {
window.apps.microApp(document.getElementById("my-micro-app"));
clearInterval(intervalId);
}
}, 10);
}

onMounted(async () => {
loadScript();
loadApp();
});
</script>

<template>
<div class="my-app">
<h1>My app</h1>

<div id="my-micro-app"></div>
</div>
</template>

Vous pouvez maintenant lancer le projet avec yarn dev et magie :

On retrouve bien my-micro-app et containerqui cohabitent ensemble : Woooooow !

On va tout même apporter une petite amélioration. Dans notre exemple, on charge un script avec un hash dans le nom du fichier. C’est-à-dire qu’à chaque fois que l’ont recréé un bundle avec des changements, le hash change. Imaginez s’il faut changer ce hash à chaque fois dans la fonction loadScript()

On va se servir d’une option de build offerte par Vite : manifest.

Dans my-micro-app, on peut ajouter cette option dans notre config vite.config.js.

export default defineConfig({
build: {
manifest: true
},
...
})

Lorsqu’on définit manifest à true, le build va générer un fichier manifest.json qui liste l’ensemble des assets présent dans le bundle. Par exemple :

{
"index.html": {
"file": "assets/index-PYNsVsJ0.js",
"isEntry": true,
"src": "index.html"
}
}

Vous voyez où je veux en venir ?

En reprenant le principe du chargement du fichier script, on va, dans un premier temps, charger le manifest. Celui-ci a la particularité de ne jamais changer de nom.

const BASE_URL_MICRO_APP = "/dist/my-micro-app";

const loadManifest = async () => {
const manifest = await fetch(`${BASE_URL_MICRO_APP}/.vite/manifest.json`);
return await manifest.json();
};

On va ensuite se baser sur ce manifest pour charger le bon point d’entrée peu importe son hash.

const loadManifest = async () => {
const manifest = await fetch(`${BASE_URL_MICRO_APP}/.vite/manifest.json`);
return await manifest.json();
};

const loadScript = async (manifest) => {
const script = document.createElement("script");
script.src = `${BASE_URL_MICRO_APP}/${manifest["index.html"].file}`;
script.type = "module";
document.head.appendChild(script);
};

function loadApp() {
const intervalId = setInterval(() => {
if (window.apps && window.apps.microApp) {
window.apps.microApp(document.getElementById("my-micro-app"));
clearInterval(intervalId);
}
}, 10);
}

onMounted(async () => {
const manifest = await loadManifest();
loadScript(manifest);
loadApp();
});

On re-build my-micro-app et container, on lance yarn dev et on voit toujours bien apparaître notre micro-front dans notre container !

Vous pouvez retrouver l’ensemble du code source sur github.

Objectif rempli : on a mis en place une architecture micro-frontend !

Cette solution peut être utilisée sur n’importe quel framework puisque tout ce qu’on a évoqué précédemment peut être répliqué sur tous les environnements Javascript.

Cependant, il est important de noter que la solution étudiée ici nécessite plusieurs améliorations pour rendre l’expérience développeur (DX) agréable. Il faut aussi prendre en compte la partie ops afin de rendre l’application disponible sur un environnement de production.

Un très grand merci à toute l’équipe technique de Steeple qui a participé à l’élaboration et la réalisation de cette architecture micro-frontend.

— — —

Steeple est une solution de communication interne phygitale qui permet aux entreprises d’impliquer leurs équipes et de créer du lien entre les collaborateurs.

--

--