Contournement angulaire de l'écueil et gain de temps

Avec Angular, vous pouvez tout faire. Ou presque tout. Mais parfois, cette insidieuse «presque» conduit au fait que le développeur perd du temps en créant des solutions de contournement, ou en essayant de comprendre pourquoi quelque chose se passe, ou pourquoi quelque chose ne fonctionne pas comme prévu.



L'auteur de l'article que nous traduisons aujourd'hui dit qu'il souhaite partager des conseils qui aideront les développeurs Angular à gagner du temps. Il va parler des pièges d'Angular, qu'il (et pas seulement lui) a eu la chance de rencontrer.

N ° 1. La directive personnalisée que vous avez appliquée ne fonctionne pas


Donc, vous avez trouvé une belle directive angulaire tierce et avez décidé de l'utiliser avec des éléments standard dans le modèle angulaire. Super! Essayons de faire ceci:

<span awesomeTooltip="'Tooltip text'"> </span> 

Vous démarrez l'application ... Et rien ne se passe. Vous, comme tout programmeur expérimenté, consultez la console Chrome Developer Tools. Et vous n'y voyez rien. La directive ne fonctionne pas et Angular est silencieux.

Ensuite, une tête brillante de votre équipe décide de placer la directive entre crochets.

 <span [awesomeTooltip]="'Tooltip text'"> </span> 

Après cela, après avoir perdu un peu de temps, nous voyons ce qui suit dans la console.


Voici la chose: nous avons juste oublié d'importer le module avec la directive

Maintenant, la cause du problème est assez évidente: nous avons juste oublié d'importer le module directive dans le module d'application Angular.
Cela conduit à une règle importante: ne jamais utiliser de directives sans crochets.

Vous pouvez expérimenter avec des directives ici .

N ° 2. ViewChild renvoie undefined


Supposons que vous ayez créé un lien vers un élément d'entrée de texte décrit dans un modèle angulaire.

 <input type="text" name="fname" #inputTag> 

Vous allez, en utilisant la fonction RxJS fromEvent , créer un flux dans lequel tombera ce qui est entré dans le champ. Pour ce faire, vous avez besoin d'un lien vers le champ de saisie, qui peut être obtenu à l'aide du décorateur Angular ViewChild :

 class SomeComponent implements AfterViewInit {    @ViewChild('inputTag') inputTag: ElementRef;    ngAfterViewInit(){        const input$ = fromEvent(this.inputTag.nativeElement, 'keyUp')    } ...   } 

Ici, en utilisant la fonction RxJS fromEvent , nous créons un flux dans lequel les données entrées dans le champ tomberont.
Testez ce code.


Erreur

Qu'est-il arrivé?

En fait, la règle suivante s'applique ici: si ViewChild retourne undefined , recherchez *ngIf dans le modèle.

 <div *ngIf="someCondition">    <input type="text" name="fname" #inputTag> </div> 

Ici, il est le coupable du problème.

De plus, vérifiez le modèle pour d'autres directives structurelles ou ng-template au ng-template dessus de l'élément problème.
Considérez les solutions possibles à ce problème.

▍Variant de résoudre le problème №1


Vous pouvez simplement masquer l'élément de modèle si vous n'en avez pas besoin. Dans ce cas, l'élément continuera toujours d'exister et ViewChild pourra lui renvoyer un lien dans le hook ngAfterViewInit .

 <div [hidden]="!someCondition">    <input type="text" name="fname" #inputTag> </div> 

▍Variant de résoudre le problème №2


Une autre façon de résoudre ce problème consiste à utiliser des setters.

 class SomeComponent {   @ViewChild('inputTag') set inputTag(input: ElementRef|null) {    if(!input) return;       this.doSomething(input);  }  doSomething(input) {    const input$ = keysfromEvent(input.nativeElement, 'keyup');    ...  } } 

Ici, dès que Angular attribue une valeur spécifique à la propriété inputTag , nous créons un flux à partir des données entrées dans le champ de saisie.

Voici quelques ressources utiles liées à ce problème:

  • Ici, vous pouvez lire que les résultats de ViewChild dans Angular 8 peuvent être statiques et dynamiques.
  • Si vous éprouvez des difficultés à travailler avec les RxJ, consultez ce cours vidéo.

Numéro 3. Exécution de code lors de la mise à jour de la liste générée avec * ngFor (après que les éléments soient apparus dans le DOM)


Supposons que vous ayez une directive personnalisée intéressante pour organiser les listes déroulantes. Vous allez l'appliquer à une liste créée à l'aide de la *ngFor Angular *ngFor .

 <div *ngFor="let item of itemsList; let i = index;"     [customScroll]     >  <p *ngFor="let item of items" class="list-item">{{item}}</p>   </div> 

Habituellement, dans de tels cas, lors de la mise à jour de la liste, vous devez appeler quelque chose comme scrollDirective.update pour configurer le comportement de défilement, en tenant compte des modifications qui se sont produites dans la liste.

Il peut sembler que cela peut être fait en utilisant le hook ngOnChanges :

 class SomeComponent implements OnChanges {  @Input() itemsList = [];   @ViewChild(CustomScrollDirective) scroll: CustomScrollDirective;  ngOnChanges(changes) {    if (changes.itemsList) {      this.scroll.update();    }  } ... } 

Certes, nous sommes ici face à un problème. Le hook est appelé avant que le navigateur affiche la liste mise à jour. Par conséquent, le recalcul des paramètres de directive pour faire défiler la liste est effectué de manière incorrecte.

Comment passer un appel juste après que *ngFor fini de fonctionner?

Vous pouvez le faire en suivant ces 3 étapes simples:

▍Etape numéro 1


Placez les liens vers les éléments où *ngFor ( #listItems ) est #listItems .

 <div [customScroll]>    <p *ngFor="let item of items" #listItems>{{item}}</p> </div> 

▍Étape numéro 2


Obtenez une liste de ces éléments à l'aide du décorateur Angular ViewChildren . Il renvoie une entité de type QueryList .

▍Étape numéro 3


La classe QueryList possède une propriété de modifications en lecture seule qui QueryList événements chaque fois que la liste change.

 class SomeComponent implements AfterViewInit {  @Input() itemsList = [];   @ViewChild(CustomScrollDirective) scroll: CustomScrollDirective;  @ViewChildren('listItems') listItems: QueryList<any>;  private sub: Subscription;   ngAfterViewInit() {    this.sub = this.listItems.changes.subscribe(() => this.scroll.update())  } ... } 

Maintenant, le problème est résolu. Ici, vous pouvez expérimenter avec l'exemple correspondant.

Numéro 4. Problèmes avec ActivatedRoute.queryParam qui se produisent lorsque des requêtes peuvent être exécutées sans paramètres


Le code suivant nous aidera à comprendre l'essence de ce problème.

 // app-routing.module.ts const routes: Routes = [    {path: '', redirectTo: '/home', pathMatch: 'full'},    {path: 'home', component: HomeComponent},  ];   @NgModule({    imports: [RouterModule.forRoot(routes)], //  #1    exports: [RouterModule]  })  export class AppRoutingModule { }    //app.module.ts  @NgModule({    ...    bootstrap: [AppComponent] //  #2  })  export class AppModule { }   // app.component.html  <router-outlet></router-outlet> //  #3    // app.component.ts  export class AppComponent implements OnInit {    title = 'QueryTest';     constructor(private route: ActivatedRoute) { }     ngOnInit() {      this.route.queryParams          .subscribe(params => {            console.log('saveToken', params); //  #4          });    }  } 

Certains fragments de ce code sont des commentaires comme le #x . Considérez-les:

  1. Dans le module principal de l'application, nous avons défini les routes et y RouterModule ajouté le RouterModule . Les itinéraires sont configurés de sorte que si un itinéraire n'est pas fourni dans l'URL, nous redirige l'utilisateur vers la page /home .
  2. En tant que composant à télécharger, nous AppComponent dans le module principal AppComponent .
  3. AppComponent utilise <router-outlet> pour sortir les composants de route appropriés.
  4. Maintenant, la chose la plus importante. Nous devons obtenir queryParams pour l'itinéraire à partir de l'URL

Supposons que nous ayons l'URL suivante:

 https://localhost:4400/home?accessToken=someTokenSequence 

Dans ce cas, queryParams ressemblera à ceci:

 {accessToken: 'someTokenSequence'} 

Regardons le travail de tout cela dans le navigateur.


Test d'une application qui implémente un système de routage

Ici, vous pouvez avoir une question sur l'essence du problème. Nous avons les paramètres, tout fonctionne comme prévu ...

Jetez un œil à la capture d'écran ci-dessus du navigateur et à ce qui s'affiche dans la console. Ici, vous pouvez voir que l'objet queryParams est émis deux fois. Le premier objet est vide; il est émis lors du processus d'initialisation du routeur angulaire. Ce n'est qu'après cela que nous obtenons un objet qui contient les paramètres de la demande (dans notre cas - {accessToken: 'someTokenSequence'} ).

Le problème est que s'il n'y a pas de paramètres de requête dans l'URL, le routeur ne renverra rien. C'est-à-dire qu'après avoir émis le premier objet vide, le deuxième objet, également vide, ce qui pourrait indiquer l'absence de paramètres, ne sera pas émis.


Le deuxième objet lors de l'exécution d'une demande sans paramètres n'est pas émis

Par conséquent, il s'avère que si le code attend un deuxième objet à partir duquel il peut recevoir des données de demande, il ne sera pas lancé s'il n'y avait aucun paramètre de demande dans l'URL.

Comment résoudre ce problème? Ici, les RxJ peuvent nous aider. Nous allons créer deux objets observables basés sur ActivatedRoute.queryParams . Comme d'habitude - envisagez une solution étape par étape au problème.

▍Etape numéro 1


Le premier objet observable, paramsInUrl$ , paramsInUrl$ données si queryParams pas vide:

 export class AppComponent implements OnInit {    constructor(private route: ActivatedRoute,                private locationService: Location) {    }    ngOnInit() {             //            //              const paramsInUrl$ = this.route.queryParams.pipe(            filter(params => Object.keys(params).length > 0)        );     ...    } } 

▍Étape numéro 2


Le deuxième objet observable, noParamsInUrl$ , noParamsInUrl$ valeur vide uniquement si aucun paramètre de requête n'a été trouvé dans l'URL:

 export class AppComponent implements OnInit {    title = 'QueryTest';    constructor(private route: ActivatedRoute,                private locationService: Location) {    }    ngOnInit() {                 ...        //   ,     ,   URL        //             const noParamsInUrl$ = this.route.queryParams.pipe(            filter(() => !this.locationService.path().includes('?')),            map(() => ({}))        );            ...    } } 

▍Étape numéro 3


Combinez maintenant les objets observés à l'aide de la fonction de fusion RxJS:

 export class AppComponent implements OnInit {    title = 'QueryTest';    constructor(private route: ActivatedRoute,                private locationService: Location) {    }    ngOnInit() {             //            //              const paramsInUrl$ = this.route.queryParams.pipe(            filter(params => Object.keys(params).length > 0)        );        //   ,     ,   URL        //             const noParamsInUrl$ = this.route.queryParams.pipe(            filter(() => !this.locationService.path().includes('?')),            map(() => ({}))        );        const params$ = merge(paramsInUrl$, noParamsInUrl$);        params$.subscribe(params => {            console.log('saveToken', params);        });    } } 

Désormais, l'objet param$ observé param$ valeur param$ seule fois, que quelque chose soit queryParams dans queryParams (un objet avec des paramètres de requête est queryParams ) ou non (un objet vide est queryParams ).

Vous pouvez expérimenter avec ce code ici .

N ° 5. Pages lentes


Supposons que vous ayez un composant qui affiche des données formatées:

 // home.component.html <div class="wrapper" (mousemove)="mouseCoordinates = {x: $event.x, y: $event.y}">  <div *ngFor="let item of items">     <span>{{formatItem(item)}}</span>  </div> </div> {{mouseCoordinates | json}} // home.component.ts export class HomeComponent {    items = [1, 2, 3, 4, 5, 6];    mouseCoordinates = {};    formatItem(item) {              //           const t = Array.apply(null, Array(5)).map(() => 1);             console.log('formatItem');        return item + '%';    } } 

Ce composant résout deux problèmes:

  1. Il affiche un tableau d'éléments (on suppose que cette opération est effectuée une fois). De plus, il formate ce qui est affiché en appelant la méthode formatItem .
  2. Il affiche les coordonnées de la souris (cette valeur sera évidemment mise à jour très souvent).

Vous ne vous attendez pas à ce que ce composant rencontre des problèmes de performances. Par conséquent, exécutez un test de performance uniquement pour respecter toutes les formalités. Cependant, certaines bizarreries apparaissent lors de ce test.


Beaucoup d'appels formatItem et beaucoup de charge CPU

Quelle est la question? Mais le fait est que lorsque Angular redessine le modèle, il appelle toutes les fonctions du modèle (dans notre cas, la fonction formatItem ). Par conséquent, si des calculs lourds sont effectués dans les fonctions de modèle, cela met à rude épreuve le processeur et affecte la façon dont les utilisateurs perçoivent la page correspondante.

Comment y remédier? Il suffit d'effectuer à l'avance les calculs effectués dans formatItem , et d'afficher les données déjà prêtes sur la page.

 // home.component.html <div class="wrapper" (mousemove)="mouseCoordinates = {x: $event.x, y: $event.y}">  <div *ngFor="let item of displayedItems">    <span>{{item}}</span>  </div> </div> {{mouseCoordinates | json}} // home.component.ts @Component({    selector: 'app-home',    templateUrl: './home.component.html',    styleUrls: ['./home.component.sass'] }) export class HomeComponent implements OnInit {    items = [1, 2, 3, 4, 5, 6];    displayedItems = [];    mouseCoordinates = {};    ngOnInit() {        this.displayedItems = this.items.map((item) => this.formatItem(item));    }    formatItem(item) {        console.log('formatItem');        const t = Array.apply(null, Array(5)).map(() => 1);        return item + '%';    } } 

Maintenant, le test de performance semble beaucoup plus décent.


Seulement 6 appels de formatItem et faible charge de processeur

Maintenant, l'application fonctionne beaucoup mieux. Mais la solution utilisée ici présente des fonctionnalités qui ne sont pas toujours agréables:

  • Puisque nous affichons les coordonnées de la souris dans le modèle, l'événement mousemove déclenche toujours une vérification des modifications. Mais, comme nous avons besoin des coordonnées de la souris, nous ne pouvons pas nous en débarrasser.
  • Si le mousemove événements mousemove n'a besoin que d'effectuer certains calculs (qui n'affectent pas ce qui est affiché sur la page), alors pour accélérer l'application, vous pouvez procéder comme suit:

    1. Vous pouvez utiliser NgZone.runOutsideOfAngular dans la fonction de gestionnaire d'événements. Cela empêche la vérification des modifications de démarrer lorsque l'événement mousemove (cela n'affectera que ce gestionnaire).
    2. Vous pouvez empêcher le correctif zone.js pour certains événements en utilisant la ligne de code suivante dans polyfills.ts . Cela affectera l'ensemble de l'application angulaire.

 * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; //      

Si vous êtes intéressé par les questions d'amélioration des performances des applications angulaires - ici , ici , ici et ici - des documents utiles à ce sujet.

Résumé


Maintenant que vous avez lu cet article, 5 nouveaux outils devraient apparaître dans votre arsenal de développeur angulaire avec lesquels vous pouvez résoudre certains problèmes courants. Nous espérons que les conseils que vous trouverez ici vous permettront de gagner du temps.

Chers lecteurs! Savez-vous quelque chose qui vous aide à gagner du temps lors du développement d'applications angulaires?

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


All Articles