L'application Snapface, dans son état actuel, est générée sous forme d'un seul fichier JavaScript pour sa logique principale.
La conséquence de cette architecture est que tout utilisateur qui s'en sert doit charger l'application entière, même s'il ne se sert que d'une partie. Ça entraîne des temps de chargement inutilement longs, et l'expérience utilisateur en souffre.
Angular a une solution à ce problème, appelée lazy loading. Il s'agit d'une technique de routing – qui dépend de l'architecture de modules que vous avez implémentée dans le chapitre précédent – qui fera en sorte qu'Angular, au moment du déploiement, crée des fichiers JS séparés pour chaque feature module. Ces fichiers sont ensuite chargés au besoin – quand l'utilisateur accède à la route qui y correspond.
Mettons en place dès maintenant le lazy loading dans l'application Snapface !
Modifiez le routing
Le module pour lequel vous allez implémenter le lazy loading est FaceSnapsModule.
Pour qu'un module puisse être lazy loaded, il doit s'occuper de son propre routing, mais pour l'instant, tout le routing de l'application se trouve dans AppRoutingModule. Du coup, vous allez déménager le routing de FaceSnapsModule dans un nouveau fichier, face-snaps-routing.module.ts
, à côté de face-snaps.module.ts
:
import { NgModule } from '@angular/core';
import { Routes } from '@angular/router';
const routes: Routes = [];
@NgModule({
imports: [],
exports: []
})
export class FaceSnapsRoutingModule {}
Avant d'y copier les routes, réfléchissons à la structure du routing.
À part la route create
, toutes les routes liées aux FaceSnaps commencent par facesnaps/
. Du coup, si on transforme la route create
pour aussi commencer par facesnaps/
, on pourrait dire que AppRoutingModule délègue toutes les routes commençant par facesnaps/
à FaceSnapsModule !
Ça veut dire que les routes que vous allez enregistrer dans FaceSnapsRoutingModule ne doivent pas contenir le préfixe :
const routes: Routes = [
{ path: ':id', component: SingleFaceSnapComponent },
{ path: '', component: FaceSnapListComponent },
{ path: 'create', component: NewFaceSnapComponent },
];
La technique pour enregistrer ces routes ressemble à celle de AppRoutingModule, avec une nuance importante :
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { SingleFaceSnapComponent } from './components/single-face-snap/single-face-snap.component';
import { FaceSnapListComponent } from './components/face-snap-list/face-snap-list.component';
import { NewFaceSnapComponent } from './components/new-face-snap/new-face-snap.component';
const routes: Routes = [
{ path: ':id', component: SingleFaceSnapComponent },
{ path: '', component: FaceSnapListComponent },
{ path: 'create', component: NewFaceSnapComponent },
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class FaceSnapsRoutingModule {}
Au lieu d'utiliser RouterModule.forRoot() (qui ne doit être appelée qu'une seule fois par votre routeur racine), vous utilisez RouterModule.forChild() pour enregistrer ces routes au routeur déjà créé.
Pour que ces routes soient enregistrées, il faut importer FaceSnapsRoutingModule dans FaceSnapsModule :
imports: [
CommonModule,
ReactiveFormsModule,
FaceSnapsRoutingModule
],
Vous pouvez retirer l'import de RouterModule, car votre module de routing vous le fournit !
Il faut dire à AppRoutingModule de déléguer les routes facesnaps/
à FaceSnapsModule. Vous allez utiliser une syntaxe spécifique qui permet à Angular de mettre en place le lazy loading :
const routes: Routes = [
{ path: 'facesnaps', loadChildren: () => import('./face-snaps/face-snaps.module').then(m => m.FaceSnapsModule) },
{ path: '', component: LandingPageComponent }
];
Cette syntaxe fait en sorte qu'Angular génère un fichier JS séparé pour FaceSnapsModule, et l'application ne la charge que si l'utilisateur visite une route facesnaps/
.
Pour terminer l'implémentation, il faut retirer l'import de FaceSnapsModule dans AppModule :
imports: [
BrowserModule,
AppRoutingModule,
CoreModule,
LandingPageModule
],
Ça y est !
Vous me croyez sur parole, non ?
Bon OK, si vous voulez quand même vérifier, commencez par charger l'application à http://localhost:4200. Allez sur l'onglet Network (Réseau) des DevTools de votre navigateur, et filtrez si possible pour ne voir que les fichiers JS chargés.
Le premier chargement donne :
Précédemment, toute la logique se trouvait dans main.…
. Cependant, avec le lazy loading implémenté, si vous cliquez maintenant sur le lien FaceSnaps ou le bouton Continuer vers Snapface...
Vous voyez le chargement du fichier JS qui correspond à FaceSnapsModule !
Déboguez le routing
Vous vous dites peut-être que réparer ce bug sera simple. Si vous avez une idée, je vous invite à l'essayer.
Il faut, en effet, modifier la méthode onAddNewFaceSnap()
de HeaderComponent :
onAddNewFaceSnap() {
this.router.navigateByUrl('/facesnaps/create');
}
C'est bien ce qu'il faut, mais si vous cliquez sur le bouton + du Header, vous verrez une erreur dans la console :
Est-ce que vous comprenez ce qui ne va pas ? 🤓
En fait, le routing essaie de traiter "create"
comme un id
de FaceSnap !
Pourquoi ? Eh bien parce que la route :id
est avant la route create
dans le tableau de Routes. Du coup, le routeur voit une route de la forme facesnaps/quelque-chose
et présume que "create"
correspond à un id
!
Ce bug illustre très bien l'importance de l'ordre des routes lors de leur enregistrement. Angular traverse le tableau de Routes dans l'ordre, et applique la première qui ressemble à la route demandée.
Du coup, si vous changez l'ordre :
const routes: Routes = [
{ path: 'create', component: NewFaceSnapComponent },
{ path: ':id', component: SingleFaceSnapComponent },
{ path: '', component: FaceSnapListComponent },
];
Tout fonctionne de nouveau correctement, et votre module FaceSnapsModule est bien lazy loaded !
En résumé
Le lazy loading génère un fichier JS séparé, pour un module qui n'est chargé que si l'utilisateur visite la route correspondante ;
Pour implémenter le lazy loading, le module en question doit s'occuper de tout son routing ;
Le routing est ensuite délégué par le routeur principal avec une syntaxe particulière :
{ path: 'module-route', loadChildren: () => import('path/to/module').then(m => m.NameOfModule) }
Dans le prochain et dernier chapitre, vous découvrirez une autre technique liée au routing qui permet de protéger certaines routes de votre application, avec les guards.