Comment architecturer un projet Angular ?

Publié le

Savoir architecturer un projet Angular est essentiel.

(Cet article est disponible en Anglais sur Medium.)

Il y a un an, je publiais Comprendre les modules Angular (NgModule). Cet article se focalisait sur un point technique : la portée, pour comprendre quand importer un NgModule. Vous devriez le lire d’abord, mais il n’expliquait pas comment organiser ses propres modules.

Jusqu’à maintenant, pour architecturer un projet Angular, je suivais principalement ce qui était suggéré dans la doc d’Angular. Mais face à de gros projets, plusieurs problèmes sont apparus, et quelque chose n’allait pas.

J’ai donc replongé dans la documentation d’Angular : il y a maintenant 12 longues pages pour expliquer comment fonctionnent les NgModule, dont une FAQ. Mais après avoir tout lu, j’étais encore plus confus qu’avant. Des questions basiques comme « quel est le bon endroit pour déclarer un service ? » n’y trouvent pas une réponse claire, et parfois même des suggestions contradictoires.

J’ai donc pris le temps de repenser l’ensemble du sujet, pour implémenter une bonne architecture dans les applications Angular, avec ces objectifs :

  • cohérence : simplicité (pour les petites applis) et évolutivité (pour les grosses apps) ;
  • réutilisabilité dans plusieurs projets ;
  • optimisation (cohérent avec ou sans chargement à la demande) ;
  • testabilité.

Angular modules

Qu’est-ce qu’un NgModule ?

Le but d’un NgModule est simplement de regrouper les composants et/ou services qui vont ensemble. Rien de plus, rien de moins.

Vous pouvez donc les comparer à un package Java ou à un namespace en PHP ou C#.

La seule question est : comment choisir ce qui va ensemble ?

Types de modules Angular

Vous pouvez créer 3 principaux types de NgModules :

  • des modules de pages ;
  • des modules de services globaux
  • des modules de composants d’UI réutilisables.

A minima, vous créerez des modules de pages (sinon votre app est vide). Les 2 autres types de modules sont optionnels, mais vous en aurez vite besoin si vous voulez réutiliser et optimiser votre code.

Modules de pages

Les modules de pages sont les modules avec routing. Ils servent à séparer et organiser les différentes parties de votre application. Ils sont chargés une seule fois, soit dans l’AppModule, soit à la demande (lazy-loading).

Par exemple, vous pourriez avoir un AccountModule pour vos pages d’inscription, de connexion et de déconnexion ; puis un HeroesModule pour les pages de la liste des héros et du détail d’un héros ; etc.

Ces modules contiennent 3 choses :

  • /shared : services et interfaces,
  • /pages : composants routés,
  • /components : purs composants de présentation.

Shared services for pages

Pour afficher une page, vous avez d’abord besoin de données. Et voilà le service :

@Injectable({ providedIn: 'root' })
export class SomeService {
constructor(protected http: HttpClient) {}
getData() {
return this.http.get<SomeData>('/path/to/api');
}
}
view raw some.service.ts hosted with ❤ by GitHub

Très vite, plusieurs pages auront besoin du même service. D’où le répertoire shared.

Mais assurez-vous que les services pour vos pages soient spécifiques à ce module. Car si vous optez pour le chargement à la demande, ils seront disponibles uniquement dans ce module, et pas ailleurs dans l’application.

Reprenons par exemple l’exemple du AccountModule. Le service qu’il contient doit just gérer la communication avec l’API (qui répond « oui » ou « non » suivant les informations de connexion de l’utilisateur). Mais le statut de connexion de l’utilisateur ne doit pas être stocké ici, sinon il risque de ne pas être disponible ailleurs dans l’application. Il sera géré dans un module de services globaux (voir ci-dessous).

Pages : composants routés

Un composant page injecte seulement le service, et l’utilise pour récupérer les données.

Vous pourriez afficher les données directement dans le template du composant, mais il ne faut pas : les données doivent être transférées à autre composant via un attribut.

@Component({
template: `<app-presentation *ngIf="data" [data]="data"></app-presentation>`
})
export class PageComponent {
data: SomeData;
constructor(protected someService: SomeService) {}
ngOnInit() {
this.someService.getData().subscribe((data) => {
this.data = data;
});
}
}

Chaque composant page est associé à une route.

Composants de présentation

Un composant de présentation récupère simplement les données transférées avec le décorateur Input, et les affiche dans la template.

@Component({
selector: 'app-presentation',
template: `<h1>{{data.title}}</h1>`
})
export class PresentationComponent {
@Input() data: SomeData;
}

Est-ce du MVx ?

D’un point de vue théorique, non. Mais si vous venez du monde du back-end et que cela vous aide d’un point de vue pratique, vous pouvez comparer :

  • les services seraient les Modèles ;
  • les composants de présentation seraient les Vues ;
  • les composants pages seraient les Contrôleurs / Presenters / ViewModels (choisissez ce dont vous avez l’habitude).

Même s’il ne s’agit pas exactement du même concept, le but est le même : la séparation des rôles. Pourquoi est-ce important ?

  • la réutilisabilité : les composants de présentation sont réutilisables dans plusieurs pages ;
  • l’optimisation : la détection de changement des composants de présentation peut être optimisée ;
  • la testabilité : les tests unitaires sont possibles sur les composants de présentation (si vous n’avez pas séparé les rôles, oubliez les tests, ça sera juste un enfer).

En résumé

Voici un example de module de pages :

@NgModule({
imports: [CommonModule, MatCardModule, PagesRoutingModule],
declarations: [PageComponent, PresentationComponent]
})
export class PagesModule {}
view raw pages.module.ts hosted with ❤ by GitHub

où les services sont spécifiques à ce module.

Modules de services globaux

Les modules de services globaux sont des modules avec des services dont vous avez besoin partout dans l’application. Comme les services ont généralement une portée globale, ces modules sont chargés une seule fois dans l’AppModule, et les services sont ensuite accessibles partout (y compris dans les modules chargés à la demande).

Vous utilisez probablement au moins l’un d’entre eux : le module HttpClient. Et vous aurez vite besoin des vôtres. Un cas très classique est l’AuthModule, pour stocker le statut de connexion de l’utilisateur (cette donnée devant être accessible partout dans l’app) et sauvegarder le token.

Note : depuis Angular 6, il n’est plus nécessaire d’avoir un NgModule pour les services, qui désormais s’auto-fournissent. Cela ne change rien à l’architecture présentée ici.

Point d’entrée

Vous pouvez facilement réutiliser les modules de services globaux dans différents projets, à condition de faire attention de n’avoir aucune dépendance particulière (pas de code spécifique à votre app ou à une librairie d’UI particulière), et si vous séparez bien chaque fonctionnalité dans différents modules (ne mettez pas tous les services dans un seul module global).

Comme un tel module sera utilisé depuis l’extérieur, il est conseillé de créer un point d’entrée, dans lequel vous exportez le NgModule, les services et peut-être aussi des interfaces et de tokens d’injection.

export { SomeService } from './some.service';
export { SomeModule } from './some.module';
view raw index.ts hosted with ❤ by GitHub

Dois-je faire un CoreModule ?

Ce n’est pas nécessaire. La documentation suggère de faire un CoreModule pour les services globaux. Vous pouvez tout à fait les rassembler dans un répertoire /core/, mais comme mentionné ci-dessus, assurez-vous de séparer chaque fonctionnalité. Il ne faut pas mettre tous les services globaux dans un seul CoreModule, sinon vous ne pourrez pas réutiliser chaque fonctionnalité dans un autre projet.

En résumé

Voici un exemple de module de services globaux :

@NgModule({
providers: [SomeService]
})
export class SomeModule {}
view raw some.module.ts hosted with ❤ by GitHub

Encore une fois, ce module n’est plus nécessaire depuis Angular 6.

Modules de composants réutilisables

Les modules de composants réutilisables sont les modules de composants d’interface que vous souhaitez réutiliser dans plusieurs projets. Comme les composants sont en portée locale, ces modules doivent être chargés dans chaque module de pages qui en a besoin.

Vous en utilisez probablement, comme Material, NgBootstrap ou PrimeNg. Vous pouvez aussi faire les vôtres.

Comment récupérer les données ?

Les composants d’UI sont de purs composants de présentation. Ils fonctionnent donc exactement comme ceux des modules de pages (voir ci-dessus) : les données doivent provenir du décorateur Input (parfois aussi de <ng-content> dans des cas avancés) :

@Component({
selector: 'ui-carousel'
})
export class CarouselComponent {
@Input() delay = 5000;
}
view raw ui.component.ts hosted with ❤ by GitHub

Vous ne devez pas utiliser un service ici, car les services sont souvent spécifiques à une application en particulier. Pourquoi ? Ne serait-ce qu’à cause de l’URL de l’API. Fournir les données sera le rôle des composants pages. Les composants d’UI ne font que les récupérer.

Composants publics et privés

Les composants étant en portée locale, n’oubliez pas de les exporter dans le NgModule. Vous devez seulement exporter les composants publics : les sous-composants internes peuvent rester privés.

Directives et pipes

Un module d’UI peut aussi contenir des directives ou des filtres. Il en va de même que pour les composants : ils doivent être exportés s’ils sont publics.

Services privés

Des services dans des modules de composants peuvent être pertinents pour manipuler les données, s’ils ne contiennent rien de spécifique. Mais soyez sûr de les fournir à l’intérieur du composant, pour qu’ils soient en portée locale/visibilité privée, et surtout pas dans le NgModule :

@Component({
selector: 'some-ui',
providers: [LocalService]
})
export class SomeUiComponent {}

Services publics

Comment faire si votre module d’UI doit aussi fournir des services publics, liés à votre composant ? Cela doit être évité autant que possible, mais c’est parfois nécessaire.

Vous devrez alors fournir ces services publics dans le NgModule. Mais comme ce module sera chargé plusieurs fois à cause de la portée des composants, cela va poser un problème pour les services.

Il faut alors ajouter un code spécifique pour chaque service public, pour empêcher qu’il soit chargé plusieurs fois. Il serait trop long de l’expliquer ici, mais c’est une bonne pratique (utilisé par exemple dans Material). Remplacez simplement SomeService par le nom de votre classe.

export function SOME_SERVICE_FACTORY(parentService: SomeService) {
return parentService || new SomeService();
}
@NgModule({
providers: [{
provide: SomeService,
deps: [[new Optional(), new SkipSelf(), SomeService]],
useFactory: SOME_SERVICE_FACTORY
}]
})
export class UiModule {}
view raw ui.module.ts hosted with ❤ by GitHub

Point d’entrée

Les modules de composants d’UI sont réutilisables dans différents projets. Comme un tel module sera utilisé depuis l’extérieur, il est conseillé de créer un point d’entrée, dans lequel vous exportez le NgModule, les composants publics/exportés (et peut-être aussi des directives, des pipes, des services publics, des interfaces et des tokens d’injection).

export { SomeUiComponent } from './some-ui/some-ui.component';
export { UiModule } from './ui.module';
view raw index.ts hosted with ❤ by GitHub

Dois-je faire un SharedModule ?

Non. La documentation suggère de faire un SharedModule, pour factoriser tous les modules de composants dans un seul module. Mais je vais plaider contre la documentation sur ce point.

Le problème est que chaque module dans lequel vous allez importer le SharedModule va devenir spécifique à votre application, et ne sera pas donc réutilisable dans un autre projet.

Il est normal d’avoir à importer les dépendances chaque fois dont on en a besoin. Avec les outils actuels comme les imports automatiques dans VS Code, cela n’est plus un souci.

Vous pouvez en revanche tout à fait rassembler les modules de composants réutilisables dans un répertoire /ui/ (ne l’appelez pas /shared/, cela porterait à confusion avec les services qui sont également partagés).

En résumé

Voici un exemple de module de composants d’interface réutilisables :

@NgModule({
imports: [CommonModule],
declarations: [PublicComponent, PrivateComponent],
exports: [PublicComponent]
})
export class UiModule {}
view raw ui.module.ts hosted with ❤ by GitHub

Conclusion

En suivant ces étapes :

  • vous aurez une architecture cohérente, dans les petites ou grosses apps, avec ou sans lazy-loading ;
  • vos modules de services globaux et ceux de composants d’UI sont prêts à être transformés en librairies, réutilisables facilement dans d’autres projets ;
  • vous pourrez faire des tests unitaires sans vous arracher les cheveux.

Voici un exemple d’architecture réelle :

app/
|- app.module.ts
|- app-routing.module.ts
|- core/
|- auth/
|- auth.module.ts (optional since Angular 6)
|- auth.service.ts
|- index.ts
|- othermoduleofglobalservice/
|- ui/
|- carousel/
|- carousel.module.ts
|- index.ts
|- carousel/
|- carousel.component.ts
|- carousel.component.css
|- othermoduleofreusablecomponents/
|- heroes/
|- heroes.module.ts
|- heroes-routing.module.ts
|- shared/
|- heroes.service.ts
|- hero.ts
|- pages/
|- heroes/
|- heroes.component.ts
|- heroes.component.css
|- hero/
|- hero.component.ts
|- hero.component.css
|- components/
|- heroes-list/
|- heroes-list.component.ts
|- heroes-list.component.css
|- hero-details/
|- hero-details.component.ts
|- hero-details.component.css
|- othermoduleofpages/

Devenez un Pro!

Cet article vous a aidé ? Et vous voulez soutenir mes contributions open source (avec notamment un outil utilisé par 600 000 développeurs) ? Faites-vous une faveur à vous-même : Schematics Pro est un outil d’automatisation de code pour Angular, React, Vue et tous les autres frameworks JavaScript, qui vous fait gagner en productivité et vous aide à vous assurer du suivi des bonnes pratiques.

En savoir plus sur Schematics Pro

Découvrez la formation Angular