Archives Wild Code School
Angular

19 - Architecture & Multi modules

Quête 15 sur 16 · 3 images archivées

title-book
Introduction

Dans cette quête tu vas découvrir qu'il est possible de découper ton application Angular en plusieurs sous-parties : les modules.

Objectifs 🤓

✅ Comprendre comment architecturer un projet Angular pour le rendre plus simple et évolutif. ✅ Mettre en place ta première architecture multi-modulaire.

Avant de démarrer, assure-toi d'avoir terminé les quêtes suivantes👇

Principe fondateur de l'architecture Angular

Les "SOLID Design Principles"

Cela fait quelques quêtes maintenant que tu crées et manipules des composants. Ils constituent l'essence même de la philosophie du framework Angular :

Info

Les composants permettent de découper ton application en un ensemble de petits éléments simples et réutilisables.

Cette philosophie s'inspire des principes fondateurs de conception en programmation orientée objet, qui ont pour but de rendre tes conceptions plus compréhensibles, flexibles et surtout plus maintenables. Ils sont au nombre de 5 et sont communément appelés S.O.L.I.D. Design Principles. Le but ici n'est pas de te les détailler, mais plus de te sensibiliser à leur philosophie et leur approche clean code.

👉 Les 2 ressources ci-dessous requièrent de bonnes connaissances en POO, donc ne t'inquiète pas si tu ne comprends pas encore tout. En revanche, assure-toi de les mettre en favoris ! 🤓

↗
SOLID Angular: A Guide to Writing Better Code
medium.com
↗
SOLID design principles in Angular (vidéo)
youtube.com

Couche de logique, composants pages, composants intelligents et composants d'affichages

Notre architecture Angular va donc s'inspirer des SOLID Design Principles. Pour cela, nous allons introduire 4 notions :

  • Couche de logique
  • Composants pages
  • Composants intelligents
  • Composants d'affichages
Info

Attends, il faut faire une distinction entre 3 catégories de composants maintenant ? 🤯

Et bien oui ! Ou du moins, si tu veux t'assurer de créer une architecture Angular la plus flexible et maintenable possible.

Voici un schéma qui te présente l'architecture fondamentale d'un module Angular flexible :

Architecture de base Angular : Business layer, smart & dumb components

Pas de panique, nous allons détailler tous les éléments ensemble 🤓

💡 L'idée est la suivante :

  • Une couche de logique (ou business layer) s'occupe de traiter toute la logique métier : récupération des données d'une API, algorithmes de formatage de données ou de tri, fusion de plusieurs flux de données... en bref, tout ce qui est en lien avec la partie métier de l'application et qui demande un traitement "complexe".

Par exemple, ça pourrait être une classe qui s'occupe de récupérer les données d'une API sur les animaux, et de ranger les animaux par ordre alphabétique. On utilise généralement un service pour créer une couche de logique.

  • Cette couche de logique est connectée à un (ou plusieurs) composants intelligents (ou smart component). On dit d'un composant qu'il est intelligent (smart) lorsqu'il a connaissance de la logique métier (business logic). Autrement dit, un smart component est un composant relié à un service par injection de dépendances (dependency injection ou DI).
↗
L'injection de dépendance avec Angular

Petit rappel sur le principe d'injection de dépendance. C'est un sujet important mais aussi complexe. N'hésite pas à revenir dessus plusieurs fois durant ces prochaines semaines !

angular.io
  • Chaque composant intelligent appartient à un composant page (view component) qui est chargé via le routing. Pour rappel, pour naviguer vers une page de ton application, tu définies une route et un composant lié à cette route. Lorsque tu navigues sur la route, tu charges le composant lié. Ce "composant lié" est un composant page. Il est très simple et ne contient pas de logique (pas de typescript). Son seul rôle est d'appeler les composants intelligents dans sa partie HTML.

  • Chaque composant intelligent récupère les données depuis la couche logique. C'est son seul et unique rôle (ou objectif). Ce n'est pas à lui d'afficher les données : il délègue cette tâche aux composants d'affichages ou dumb components en leur envoyant en @Input() les données que ces derniers sont chargés d'afficher.

👉 Si on reprend notre exemple précédent :

  • Notre couche de logique (un service) détient les données de l'API d'animaux triés par ordre alphabétique.
  • Notre composant page est chargé en routing et appelle notre composant intelligent.
  • Notre composant intelligent est connecté à notre couche de logique par injection de dépendance et récupère les données triées. Il appelle un composant d'affichage et lui envoie les données par @Input()
  • Notre composant d'affichage se charge d'afficher par binding les données reçues en @Input() (ça serait par exemple une card) et, si besoin, émet des événéments au composant intelligent par @Output()
Info

Si tu reprends le schéma de tout à l'heure, il devrait te paraître un peu plus clair maintenant 😌

Tous ces termes techniques peuvent te sembler un petit peu effrayant et c'est normal ! Mais tu finiras par vite t'y habituer. Retiens surtout que respecter ce modèle de conception change vraiment la vie lorsque ton application prend de l'ampleur !

Architecture des dossiers au sein d'un module

Tu sais maintenant qu'il existe une hiérarchie entre les fichiers d'un même module : la couche de logique envoie ses données au composant intelligent qui délègue l'affichage à son (ses) composant(s) d'affichage(s). C'est une bonne première étape, mais dès lors que ton projet prend de l'envergure, tu crées beaucoup de composants. Il faut donc que tu puisses les organiser correctement pour t'y retrouver sans problème 🐙.

👉 Pour cela, tu vas devoir segmenter ton module en plusieurs sous-dossiers :

  • Un dossier shared dans lequel tu ajoutes ta business layer (tes services)
  • Un dossier components qui comporte 2 sous-dossiers :
    • Un sous-dossier feature qui comporte tous tes smart components
    • Un sous-dossier ui qui comporte tes dumb components
  • Un dossier pages qui regroupe tous tes view components
  • Un dossier models qui regroupe toutes les classes/interfaces dont ton module a besoin

Voici ce que ça donne en image :

Architecture de base Angular : capture d'écran

Cette organisation peut sembler très militaire, mais elle est fondamentale pour charpenter des projets Angular scalables et évolutifs dans lesquels il est agréable de naviguer en tant que développeur.

👉 Tu viens donc de voir comment s'architecture UN module Angular. Mais ce n'est pas encore tout à fait fini :

Info

Il te reste une dernière étape : diviser ton application en plusieurs modules 🤓

Créer un projet multi-modulaire

Oui mais pourquoi ?

La création de modules dans ton application n'est pas très difficile. Le plus délicat, c'est de bien réfléchir à leurs rôles respectifs. C'est quelque chose qui vient avec l'expérience. Néanmoins, tu peux retenir que l'idée est de créer un module par fonctionnalité principale.

Dans la plupart des applications, tu as généralement des fonctionnalités principales comme : authentification, paiement, espace membre, espace administrateur, etc... (pense à Odyssey par exemple).

👉 Avec Angular, on créerait donc un module par fonctionnalité principale :

  • Un module pour la partie authentification (inscription/connexion)

  • Un module pour la partie paiement

  • Un module pour l'espace membre

  • Un module pour l'espace adminisateur

  • etc...

Et tu peux redécouper chaque module en sous-modules ! (c'est là tout l'intérêt !). Si on reprend notre exemple, le module authentification (auth.module.ts) gère à la fois l'inscription et la connexion. Tu créerais alors :

  • Un sous-module pour l'inscription (registration.module.ts)
  • Un sous-module pour la connexion (connexion.module.ts)

Pour revenir à un exemple plus simple, admettons que tu viennes de créer un nouveau projet et que tu décides de créer 2 modules : account et auth. Voici ce que donnerait ton architecture Angular :

Architecture angular en image

✅ Cette nouvelle organisation permet de facilement comprendre la responsabilité de chaque module et te repérer dans ton code. C'est aussi pour une question de performances : tu peux demander à Angular de ne charger ton module que lorsque tu navigues dessus. On parle alors de lazy loading (chargement fainéant).

↗
NgModule : lazy loading
angular.io

Et comment je fais ça ?

Pour créer un projet multi-modulaire, nous allons repartir d'un projet vierge. Tu peux par exemple l'appeler multimod.

Création et architecture du premier module

Jusqu'à maintenant, lorsque tu crées un composant avec le CLI Angular (ng generate component "nom-du-composant"), Angular ajoute automatiquement ce nouveau composant dans le app.module.ts.

Attention

Dans un contexte multi-modulaire, le composant sera ajouté dans le module le plus proche (module du dossier le plus proche).

On fera donc attention à créer les sous modules avant de créer leurs composants !

👉 Si on reprend notre exemple :

  • Place toi à la racine de ton application angular (src/app) et créé un nouveau dossier que tu appelleras modules. Tu viendras y ranger tous tes modules.

  • Dans ton nouveau projet, génère un nouveau module (par exemple un module qui va permettre de gérer un compte utilisateur. On l'appellera account). Pour cela, tu peux utiliser la commande de ton cli : ng g module modules/account --routing. Cela aura pour conséquence de :

  • Créer le module account.module.ts dans le dossier modules
  • Créer un fichier de routing account.routing.module.ts pour gérer le routing relatif à ce module

✅ Cela t'indique que le module a bien été créé. Mais avant de créer le moindre composant, tu vas devoir architecturer ton module comme ce que tu as vu plus haut. Il faut donc créer les dossiers shared, components, feature, ui, pages, models à l'intérieur de ce nouveau module.

Une fois que c'est fait, tu souhaites maintenant créer un composant page dans ton module account :

  • Rends-toi avec ton terminal dans le dossier pages de ton module account
  • Créé un nouveau composant ng g component account-home. Cela aura pour conséquence de créer le componsant AccountHomeComponent.
Info

Première étape terminée ! 🥳 Il ne te reste plus qu'à savoir comment charger en lazy loading un composant appartenant à un module.

Les performances & le LazyLoading

Au lancement de ton application, tous les éléments sont chargés automatiquement. Pourtant, la première fois qu'un utilisateur vient sur ton application, il est inutile de charger le module account puisque la première chose qu'il verra sera probablement la partie "vitrine" du site, ou bien une autre page telle que la page de connexion ou la page d'inscription.

Pour gérer ce scénario, Angular met à notre disposition le "Lazy Loading" 😎

En développement, la notion de Lazy/Eager revient souvent, c'est l'opposition entre "Charger le minimum (fainéant) / Tout charger automatiquement (avide)". Lazy sera toujours plus performant, mais parfois moins pratique à implémenter.

Quand on veut mettre en place un système lazy loadé, cela signifie donc qu'on charge les choses "à la dernière minute" ou plutôt juste avant d'en avoir besoin. Pour que ce mécanisme fonctionne, il faut que :

  • Nous ayons un routing.module par module. Par exemple, il faut avoir le fichier account.routing.module.ts pour le module account.

  • Que les routes n'utilisent plus le Eager loading (que tu as mis en place dans la quête sur le routing) mais plutôt le Lazy loading.

Quêtes liées

Et pour implémenter le lazy loading, voici à quoi doit ressembler ta route dans ton app.routing.ts :

typescript
// Dans le fichier app-routing.module.ts
{ path: 'account', loadChildren: () =>  import('./modules/account/account.module').then(m  => m.AccountModule) }

Si l'on décortique :

  • path est une partie de ton URL. Lorsque ton router reçoit stricto sensu cette URL, il déclenche la méthode loadChildren.
  • loadChildren est une méthode qui importe ton module de manière asynchrone. Lorsque l'import est terminé, il charge ensuite le module. Ne te formalise pas sur la syntaxe. Sache il s'agit simplement d'une Promise en Javascript.
Info

Tu y es presque ! Dernière étape : configurer le routing de ton nouveau module.

En reprenant l'exemple ci-dessus, lorsque tu navigues vers /account, tu charges le module account. Mais pour le moment, ça ne fait pas grand chose : ce nouveau module doit lui aussi configurer son propre routing afin de charger ses propres composants. C'est pour cette raison que lorsque tu as créé ton module tout à l'heure, tu as ajouté --routing à la suite de ng g module modules/account.

👉 L'option --routing permet de créer un fichier de routing propre à son module.

Tu as donc, si tout va bien, un fichier account.routing.module.ts. Et bien c'est là que tu vas pouvoir déclarer les routes pour charger tes composants comme d'habitude (en Eager Loading, puisqu'ici il ne s'agit pas de charger un autre module, mais simplement de charger des componsants de ce module). Tu vas donc pouvoir charger le composant account-home qui appartient à account.module.ts que tu as créés tout à l'heure :

typescript
// Dans le fichier account-routing.module.ts
{ path: 'home', component: AccountHomeComponent }

Enfin, pour accéder à ton composant AccountHomeComponent, tu vas devoir entrer l'URL /account/home. Celui-ci est une composition :

  • Du path présent dans le routing module parent (dans notre cas, le path "account" est présent dans le app-routing.module)

  • Du path présent dans le routing module lazy loadé (dans notre cas, le path "home" est présent pour charger AccountHomeComponent dans account.routing.module.ts

✅ Le chemin final est donc "/account" + "/home" (c'est à dire: /account/home) ! Et tu es maintenant prêt·e pour le challenge 🦹‍♂️

Challenge

Gérer des utilisateurs

Tu vas devoir créer un projet multi-modulaire ! Pour cela, commence par créer un nouveau projet Angular dans lequel tu crées un fichier users.json que tu placeras dans assets/json. Ce fichier simulera la réponse d'une API. Voici les données à copier dedans :

hidden
Voir les données |||jsx||||||0|||Cacher
[
  {
    "id": 1,
    "username": "johnDoe",
    "firstname": "John",
    "lastname": "Doe",
    "email": "johndoe@example.com"
  },
  {
    "id": 2,
    "username": "janeSmith",
    "firstname": "Jane",
    "lastname": "Smith",
    "email": "janesmith@example.com"
  },
  {
    "id": 3,
    "username": "bobJohnson",
    "firstname": "Bob",
    "lastname": "Johnson",
    "email": "bobjohnson@example.com"
  },
  {
    "id": 4,
    "username": "saraWilson",
    "firstname": "Sara",
    "lastname": "Wilson",
    "email": "sarawilson@example.com"
  },
  {
    "id": 5,
    "username": "davidLee",
    "firstname": "David",
    "lastname": "Lee",
    "email": "davidlee@example.com"
  }
]

Ton application devra contenir :

  • Un module app, le module initial généré par Angular qui comporte :
    • Une classe "User" qui a pour propriétés : "username", "firstname", "lastname", "email".
    • une couche de logique (service) qui récupère en requête HTTP le fichier users.json.

Ces deux éléments sont dans le module racine app car ils sont transversaux à tous tes modules. Les règles de dossiers pour organiser un module s'appliquent également au module app.

  • Un module admin qui comporte :

    • un composant page chargé par l'URL /admin/home.
    • un composant intelligent qui récupère les données de la couche de logique.
    • un composant d'affichage qui affiche chaque user de users.
  • Un module account qui comporte :

    • un composant page chargé par l'URL /account/home.
    • un composant intelligent qui récupère les données de la couche de logique.
    • un composant d'affichage qui affiche un user aléatoire.
Info

BONUS 1 : le composant d'affichage du module account affiche non pas un user aléatoire mais un user dont l'ID correspond à un paramètre d'URL.

Info

BONUS 2 : Créer un module shared-components dans lequel tu déclares les composants réutiltisés à travers tes modules.

Critères de validation

  • Il y au total 3 modules : app, admin, account.
  • La classe et le service sont dans le module app
  • Les modules adminet account sont chargés en lazyloading.
  • La liste des utilisateurs est affichée lorsque tu navigues sur /admin/home.
  • Un utilisateur est affiché lorsque tu navigues sur /account/home.
  • Il y a 2 routes lazy loadées dans le app-routing.module.