Angular 9 et Ivy: chargement de composants paresseux

Chargement de composants paresseux dans Angular? Peut-être que nous parlons de modules de chargement paresseux utilisant le routeur angulaire? Non, nous parlons des composants. La version actuelle d'Angular ne prend en charge que le chargement de module paresseux. Mais Ivy donne au développeur de nouvelles opportunités pour travailler avec des composants.



La charge paresseuse que nous avons utilisée jusqu'à présent: les routes


Le chargement paresseux est un excellent mécanisme. Dans Angular, vous pouvez utiliser ce mécanisme sans avoir à faire presque aucun effort en déclarant des itinéraires qui prennent en charge le chargement paresseux. Voici un exemple de la documentation Angular qui le démontre:

const routes: Routes = [     { path: 'customer-list',       loadChildren: () => import('./customers/customers.module').then(m => m.CustomersModule) }     ]; 

Grâce au code ci-dessus, un fragment séparé pour le customers.module sera créé, qui sera chargé lorsque l'utilisateur naviguera le long de l'itinéraire de la customer-list des customer-list .

C'est un très bon moyen de réduire la taille du bundle principal du projet et d'accélérer le chargement initial de l'application.

Mais malgré cela, ne serait-il pas agréable de pouvoir contrôler plus précisément le chargement paresseux? Par exemple, que faire si nous pouvions organiser le chargement paresseux de composants individuels?

Jusqu'à présent, cela n'a pas été possible. Mais tout a changé avec l'avènement d'Ivy.

Ivy et le concept de "localité"


Les modules sont le concept de base et le bloc de construction de base de chaque application angulaire. Les modules déclarent des composants, des directives, des tuyaux, des services.

Une application angulaire moderne ne peut exister sans modules. Une des raisons à cela est le fait que ViewEngine ajoute toutes les métadonnées nécessaires aux modules.

Ivy, en revanche, adopte une approche différente. Dans Ivy, un composant peut exister sans module. Cela est possible grâce au concept de localité. Son essence est que toutes les métadonnées sont locales au composant.

Expliquons cela en analysant le bundle es2015 généré à l'aide d'Ivy.


Ensemble Es2015 généré à l'aide de Ivy

Dans la section Component code du Component code , vous pouvez voir que le système Ivy a enregistré le code du composant. Il n'y a rien de spécial ici. Mais Ivy ajoute ensuite des métadonnées au code.

La première partie des métadonnées est représentée sur la figure comme une Component factory . L'usine sait instancier un composant. Dans la section Component metadata du Component metadata , Ivy place des attributs supplémentaires, tels que le type et les selectors , c'est-à-dire tout ce dont le composant a besoin pendant l'exécution du programme.

Il convient de mentionner qu'Ivy ajoute ici la fonction de template . Il est affiché dans la Compiled version of your template section de Compiled version of your template . Arrêtons-nous plus en détail sur ce fait intéressant.

La fonction template est une version compilée de notre code HTML. Elle suit les instructions Ivy pour créer un DOM. Ceci est différent du fonctionnement de ViewEngine.

Le système ViewEngine prend le code et le contourne. Angular exécutera alors le code si nous l'utilisons.

Et avec l'approche utilisée par Ivy, le composant qui invoque les commandes angulaires est en charge de tout. Cette modification permet aux composants d'exister indépendamment, ce qui conduit au fait que l'algorithme de tremblement d'arbre peut être appliqué au code de base angulaire.

Un vrai exemple de chargement de composants paresseux


Maintenant que nous savons que le chargement de composants paresseux est possible, considérons-le avec un exemple réel. À savoir, nous allons créer une application Quiz qui pose des questions à l'utilisateur avec des options de réponse.

L'application affiche une image de la ville et des options à partir desquelles vous devez choisir le nom de cette ville. Dès que l'utilisateur sélectionne une option en cliquant sur le bouton correspondant, ce bouton change immédiatement, indiquant si la réponse était bonne ou mauvaise. Si l'arrière-plan du bouton devient vert, la réponse était correcte. Si l'arrière-plan devient rouge, cela signifie que la réponse était incorrecte.

Une fois la réponse à la question en cours reçue, le programme affiche la question suivante. Voici à quoi ça ressemble.


Démo du quiz

Les questions que le programme pose à l'utilisateur sont représentées par le composant QuizCardComponent .

Concept de chargement de composants paresseux


Illustrons d'abord l'idée générale du chargement paresseux du composant QuizCardComponent .


Le processus du composant QuizCardComponent

Après que l'utilisateur a commencé le quiz en cliquant sur le bouton Start quiz , nous commençons le chargement paresseux du composant. Une fois le composant à notre disposition, nous le plaçons dans un conteneur.

Nous réagissons aux événements questionAnsvered d'un composant «paresseux» de la même manière que nous réagirions aux événements d'un composant régulier. À savoir, après l'occurrence de l'événement, nous affichons à l'écran la prochaine carte avec la question.

Analyse de code


Afin de comprendre ce qui se passe lors du chargement paresseux d'un composant, nous commençons par une version simplifiée de QuizCardComponent , qui affiche les propriétés de la question.

Ensuite, nous développerons ce composant en utilisant des composants de matériau angulaire. Et à la fin, nous ajusterons la réaction aux événements générés par le composant.

QuizCardComponent le chargement paresseux d'une version simplifiée du composant QuizCardComponent , qui a le modèle suivant:

 <h1>Here's the question</h1> <ul>    <li><b>Image: </b> {{ question.image }}</li>    <li><b>Possible selections: </b> {{ question.possibleSelections.toString() }}</li>    <li><b>Correct answer: </b> {{ question.correctAnswer }}</li> </ul> 

La première étape consiste à créer un élément conteneur. Pour ce faire, nous pouvons soit utiliser un élément réel, comme <div> , ou - recourir à ng-container , ce qui nous permet de nous passer d'un niveau supplémentaire de code HTML. Voici à quoi ressemble la déclaration de l'élément conteneur dans laquelle nous plaçons le composant «paresseux»:

 <mat-toolbar color="primary">  <span>City quiz</span> </mat-toolbar> <button *ngIf="!quizStarted"        mat-raised-button color="primary"        class="start-quiz-button"        (click)="startQuiz()">Start quiz</button> <ng-container #quizContainer class="quiz-card-container"></ng-container> 

Dans le composant, vous devez accéder au conteneur. Pour ce faire, nous utilisons l'annotation @ViewChild et nous faisons savoir que nous voulons lire ViewContainerRef .

Notez que dans Angular 9, la propriété static dans l'annotation @ViewChild est définie sur false par défaut:

 @ViewChild('quizContainer', {read: ViewContainerRef}) quizContainer: ViewContainerRef; 

Nous avons maintenant un conteneur dans lequel nous voulons ajouter le composant "paresseux". Ensuite, nous avons besoin de ComponentFactoryResolver et Injector . Les deux peuvent être acquis en recourant à la méthodologie de l'injection de dépendance.

L'entité ComponentFactoryResolver est un simple registre qui établit la relation entre les composants et les classes ComponentFactory générées automatiquement qui peuvent être utilisées pour instancier des composants:

 constructor(private quizservice: QuizService,            private cfr: ComponentFactoryResolver,            private injector: Injector) { } 

Nous avons maintenant tout ce qui est nécessaire pour atteindre l'objectif. Travaillons sur le contenu de la méthode startQuiz et organisons le chargement paresseux du composant:

 const {QuizCardComponent} = await import('./quiz-card/quiz-card.component'); const quizCardFactory = this.cfr.resolveComponentFactory(QuizCardComponent); const {instance} = this.quizContainer.createComponent(quizCardFactory, null, this.injector); instance.question = this.quizservice.getNextQuestion(); 

Nous pouvons utiliser la commande import ECMAScript pour organiser le chargement paresseux de QuizCardComponent . Une expression d'importation renvoie une promesse. Vous pouvez travailler avec lui soit en utilisant la construction async/await .then soit en utilisant le gestionnaire .then . Après avoir résolu la promesse, nous utilisons la déstructuration pour créer une instance du composant.

De nos jours, vous devez utiliser ComponentFactory pour fournir une compatibilité descendante. À l'avenir, la ligne correspondante n'est plus nécessaire, car nous pourrons travailler directement avec le composant.

L'usine ComponentFactory nous donne componentRef . Nous passons componentRef et Injector à la méthode createComponent du conteneur.

La méthode createComponent renvoie un ComponentRef où l'instance de composant est contenue. Nous utilisons l' instance pour transmettre les @Input du composant @Input .

À l'avenir, tout cela peut probablement être fait en utilisant la méthode Angular renderComponent . Cette méthode est encore expérimentale. Cependant, il est très probable que cela deviendra une méthode angulaire régulière. Voici des documents utiles sur ce sujet.

C'est tout ce qui est nécessaire pour organiser le chargement paresseux d'un composant.


Chargement de composants paresseux

Après avoir appuyé Start quiz bouton Start quiz , le chargement paresseux du composant commence. Si vous ouvrez l'onglet Network des outils de développement, vous pouvez voir le processus de chargement paresseux du fragment de code représenté par le fichier quiz-card-quiz-card-component.js . Après le chargement et le traitement du composant est affiché, l'utilisateur voit une carte de question.

Extension de composant


Nous QuizCardComponent actuellement le composant QuizCardComponent . C’est très bien. Mais notre application n'est pas encore particulièrement fonctionnelle.

Corrigeons cela en y ajoutant des fonctionnalités et des composants supplémentaires de matériau angulaire:

 <mat-card class="quiz-card">  <mat-card-header>    <div mat-card-avatar class="quiz-header-image"></div>    <mat-card-title>Which city is this?</mat-card-title>    <mat-card-subtitle>Click on the correct answer below</mat-card-subtitle>  </mat-card-header>  <img class="image" mat-card-image [src]="'assets/' + question.image" alt="Photo of a Shiba Inu">  <mat-card-actions class="answer-section">    <button [disabled]="answeredCorrectly !== undefined" *ngFor="let selection of question.possibleSelections"            mat-stroked-button color="primary"            [ngClass]="{              'correct': answeredCorrectly && selection === question.correctAnswer,              'wrong': answeredCorrectly === false && selection === question.correctAnswer             }"            (click)="answer(selection)">      {{selection}}    </button>  </mat-card-actions> </mat-card> 

Nous avons inclus de beaux composants matériels dans le composant. Et les modules correspondants?

Ils peuvent bien sûr être ajoutés à l' AppModule . Mais cela signifie que ces modules se chargeront en mode "gourmand". Et ce n'est pas une bonne idée. De plus, l'assemblage du projet échouera, avec le message suivant:

 ERROR in src/app/quiz-card/quiz-card.component.html:9:1 - error TS-998001: 'mat-card' is not a known element: 1. If 'mat-card' is an Angular component, then verify that it is part of this module. 2. If 'mat-card' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@NgModule.schemas' of this component to suppress this message. 

Que faire Comme vous l'avez probablement déjà compris, ce problème est complètement résoluble. Cela peut être fait à l'aide de modules.

Mais cette fois, nous les utiliserons un peu différemment qu'auparavant. Nous allons ajouter un petit module dans le même fichier que QuizCardComponent (dans le fichier quizcard.component.ts ):

 @NgModule({  declarations: [QuizCardComponent],  imports: [CommonModule, MatCardModule, MatButtonModule] }) class QuizCardModule { } 

Notez qu'il n'y a pas de déclaration d' export .

Cette spécification de module appartient exclusivement à notre composant, chargé en mode paresseux. Par conséquent, le seul composant déclaré dans le module sera QuizCardComponent . La section d'importation importe uniquement les modules dont notre composant a besoin.

Nous n'exportons pas le nouveau module afin que les modules chargés en mode gourmand ne puissent pas l'importer.

Redémarrez l'application et regardez comment elle se comporte lorsque vous cliquez sur le Start quiz .


Analyse d'application modifiée

Super! Le composant QuizCardComponent chargé en mode ViewContainer et ajouté au ViewContainer . Toutes les dépendances nécessaires en sont chargées.

Nous analyserons le bundle d'application à l'aide de l' webpack-bundle-analyze fourni par le module npm correspondant. Il vous permet de visualiser le contenu des fichiers produits par Webpack et d'examiner le schéma résultant.


Analyse du bundle d'applications

La taille du bundle principal de l'application est d'environ 260 Ko. Si nous téléchargions le composant QuizCardComponent avec lui, la taille des données téléchargées serait d'environ 270 Ko. Il s'avère que nous avons pu réduire la taille du paquet principal de 10 Ko, en ne chargeant qu'un seul composant en mode paresseux. Bon résultat!

Le code de QuizCardComponent après l'assemblage obtient un fichier séparé. Si vous analysez le contenu de ce fichier, il s'avère qu'il n'y a pas seulement le code QuizCardComponent , mais aussi les modules Material utilisés dans ce composant.

Même si QuizCardComponent utilise MatButtonModule et MatCardModule , seul MatCardModule pénètre dans le MatCardModule code fini. La raison en est que nous utilisons également le MatButtonModule dans l' AppModule , affichant le bouton Start quiz . En conséquence, le code correspondant tombe dans un autre fragment du bundle.

Nous avons maintenant organisé le chargement paresseux de QuizCardComponent . Ce composant affiche une carte, conçue dans le style du matériel, contenant une image, une question et des boutons avec des options de réponse. Que se passe-t-il maintenant si vous cliquez sur l'un de ces boutons?

Le bouton, selon qu'il contient la bonne ou la mauvaise réponse, devient vert ou rouge lorsqu'il est enfoncé. Que se passe-t-il d'autre? Rien. Et nous avons besoin qu'après avoir répondu à une question, une carte d'une autre question soit affichée. Réparez-le.

Gestion des événements


L'application n'affiche pas de nouvelle carte de question lorsque vous cliquez sur le bouton de réponse car nous n'avons pas encore mis en place de mécanismes pour répondre aux événements des composants chargés en mode paresseux. Nous savons déjà que le composant QuizCardComponent génère des événements à l'aide de EventEmitter . Si vous regardez la définition de la classe EventEmitter , vous pouvez découvrir qu'il s'agit d'un descendant de Subject :

 export declare class EventEmitter<T extends any> extends Subject<T> 

Cela signifie que EventEmitter a une méthode d' subscribe , qui vous permet de configurer la réponse du système aux événements qui se produisent:

 instance.questionAnswered.pipe(    takeUntil(instance.destroy$) ).subscribe(() => this.showNewQuestion()); 

Ici, nous nous showNextQuestion au flux questionAnswered et appelons la méthode showNextQuestion , qui exécute la logique lazyLoadQuizCard :

 async showNewQuestion() {  this.lazyLoadQuizCard(); } 

La construction takeUntil(instance.destroy$) est nécessaire pour effacer l'abonnement qui est exécuté après la destruction du composant. Si le hook ngOnDestroy du cycle de vie du composant ngOnDestroy est appelé, destroy$ est appelé avec next et complete .

Le composant étant déjà chargé, le système n'effectue pas de requête HTTP supplémentaire. Nous utilisons le contenu du fragment de code que nous avons déjà, créons un nouveau composant et le mettons dans le conteneur.

Crochets de cycle de vie des composants


Presque tous les crochets du cycle de vie des composants sont automatiquement appelés lorsque vous travaillez avec le composant QuizCardComponent à l'aide de la technique de chargement paresseux. Mais un crochet ne suffit pas. Pouvez-vous comprendre lequel?


Crochets de cycle de vie des composants

C'est le ngOnChanges plus important de ngOnChanges . Puisque nous mettons nous-mêmes à jour les propriétés d'entrée de l'instance de composant, nous sommes également responsables de l'appel du ngOnChanges cycle de vie ngOnChanges .

Pour appeler le hook ngOnChanges de l'instance de ngOnChanges , vous devez construire vous-même un objet SimpleChange :

 (instance as any).ngOnChanges({    question: new SimpleChange(null, instance.question, true) }); 

Nous appelons manuellement ngOnChanges instance de composant et lui passons un objet SimpleChange . Cet objet indique que cette modification est la première, que la valeur précédente est null et que la valeur actuelle est une question.

Super! Nous avons chargé le composant avec des modules tiers, répondu aux événements qu'il a générés et configuré l'appel des hooks nécessaires du cycle de vie du composant.

Voici le code source du projet fini sur lequel nous travaillions ici.

Résumé


Le chargement de composants paresseux offre aux développeurs Angular de grandes opportunités pour optimiser les performances des applications. À sa disposition se trouvent les outils pour affiner très finement la composition des matériaux chargés en mode paresseux. Auparavant, lorsqu'il était possible de charger uniquement des itinéraires en mode paresseux, nous n'avions pas une telle précision.

Malheureusement, lorsque vous utilisez des modules tiers dans le composant, nous devons également prendre soin des modules. Cependant, il convient de rappeler qu'à l'avenir, cela pourrait changer.

Le moteur Ivy a introduit le concept de localité, grâce auquel les composants peuvent exister indépendamment. Ce changement est le fondement de l'avenir d'Angular.

Chers lecteurs! Envisagez-vous d'utiliser la technique de chargement de composants paresseux dans vos projets Angular?

Source: https://habr.com/ru/post/fr484618/


All Articles