La structure des dossiers et la gestion des modules d'une application peuvent devenir très complexes à mesure que l'application se développe. Cette croissance peut rapidement devenir difficile à suivre. Un bon moyen de structurer l'application consiste à l'écrire en se basant sur les fonctionnalités, chaque fonctionnalité résidant à sa place.
Un modèle commun et simple consiste à diviser l'application dans différents packages, représentant chacun une fonctionnalité, un composant ou encore une API. Ces packages peuvent être partagés entre différentes applications.
Cette structure multi-packages est déjà utilisée par plusieurs grands noms de l'informatique et est connue sous le nom de monorepo.
Pour gérer ce genre d'architecture, différents outils existent :
Yarn, avec son API Workspaces, ainsi que Lerna, sont très répandus. Ils permettent de gérer très facilement un projet de type monorepo.
Peut-être avez-vous déjà utilisé un outil qui possède lui-même ce type d’architecture. Babel et React en sont le parfait exemple : ils utilisent un référentiel unique qui facilite grandement la maintenance et la synchronisation des différents packages.
Malgré les atouts incontestables de l'utilisation du monorepo, il existe quelques limites que nous souhaitions tout de même vous exposer avant d'entrer dans le vif du sujet.
Passons maintenant à notre exemple. React offre la possibilité de développer une application web et native grâce au concept interne des renderers. Un renderer est une logique de rendu du model React dans une target :
Le partage de vues entre le web et le natif ne serait pas possible sans le travail de Nicolas Gallagher, développeur du module react-native-web. Celui-ci permet d'utiliser les API de react-native en web. Le module est activement maintenu et massivement utilisé. A titre d’exemple, la PWA de Twitter est intégralement développée avec ce module.
Voici un exemple d’architecture monorepo dans un projet avec une stack 100% JavaScript :
Détail des différents packages :
Il est possible d’aller encore plus loin, par exemple, en ajoutant un package nommé extension. Celui-ci serait une extension navigateur Chrome/Brave/Mozilla/Edge, ou encore un package CLI dédié au projet en passant par un package IoT !
Ce type d’architecture demande néanmoins un certain temps de configuration et d’adaptation : un seul dossier node_modules sera créé à la racine du projet. Cela donne ce type de configuration en utilisant l’API Workspaces de Yarn :
Un fichier package.json doit être défini à la racine du projet. Celui-ci devra contenir la clé private à true ainsi que les différents workspaces du projet. Exemple en tenant compte de l’illustration ci-dessus :
{
“name”: “my-app”,
“private”: true,
“workspaces”: [
“packages/admin”,
“packages/api”,
…
]
}
Chaque package contient également son propre fichier package.json, qui doit obligatoirement contenir les clés name et version afin qu’il puisse être défini comme module du workspace. Exemple avec le core :
{
“name”: “@my-app/core”,
“version”: “0.0.1”
}
Avec cette architecture, l’installation des dépendances se fera dans un seul dossier node_modules à la racine du projet. Il est néanmoins possible que chaque package puisse avoir sa dépendance dans son propre dossier.
En savoir plus sur l’API Workspace de yarn
Cette architecture ajoute une base solide dans un projet. Elle apporte un gain de productivité en :
En dehors des packages, le tooling du projet est également centralisé : environnement de développement, dépendances, configuration des tests, les différents linter, CI/CD.
Ce modèle fait ses preuves : il permet le développement d’application par plusieurs équipes interdépendantes. Celles-ci peuvent travailler sur plusieurs packages, tout en gardant un scope dédié.