Angular Pitfall Bypass und Zeitersparnis

Mit Angular können Sie alles tun. Oder fast alles. Aber manchmal führt dieses heimtückische „fast“ dazu, dass der Entwickler Zeit verschwendet, indem er Problemumgehungen erstellt oder versucht zu verstehen, warum etwas passiert oder warum etwas nicht wie erwartet funktioniert.



Der Autor des Artikels, den wir heute übersetzen, sagt, er möchte Tipps teilen, die Angular-Entwicklern helfen, Zeit zu sparen. Er wird über die Fallstricke von Angular sprechen, die er (und nicht nur er) kennenlernen durfte.

Nr. 1. Die von Ihnen angewendete benutzerdefinierte Direktive funktioniert nicht


Sie haben also eine nette Angular-Direktive von Drittanbietern gefunden und beschlossen, sie mit Standardelementen in der Angular-Vorlage zu verwenden. Großartig! Versuchen wir Folgendes:

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

Sie starten die Anwendung ... und nichts passiert. Sie schauen wie jeder normale, erfahrene Programmierer in die Chrome Developer Tools-Konsole. Und Sie sehen dort nichts. Die Direktive funktioniert nicht und Angular schweigt.

Dann beschließt ein kluger Kopf Ihres Teams, die Richtlinie in eckige Klammern zu setzen.

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

Nachdem wir etwas Zeit verloren haben, sehen wir Folgendes in der Konsole.


Hier ist die Sache: Wir haben nur vergessen, das Modul mit der Direktive zu importieren

Jetzt liegt die Ursache des Problems auf der Hand: Wir haben nur vergessen, das Direktivenmodul in das Angular-Anwendungsmodul zu importieren.
Dies führt zu einer wichtigen Regel: Verwenden Sie niemals Direktiven ohne eckige Klammern.

Hier können Sie mit Direktiven experimentieren.

Nr. 2. ViewChild gibt undefiniert zurück


Angenommen, Sie haben einen Link zu einem Texteingabeelement erstellt, das in einer Winkelvorlage beschrieben ist.

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

Mit der Funktion RxJS fromEvent erstellen Sie einen Stream, in den das, was in das Feld eingegeben wird, fällt. Dazu benötigen Sie einen Link zum Eingabefeld, der mit dem Angular ViewChild Dekorator abgerufen werden kann:

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

Hier erstellen wir mit der Funktion RxJS fromEvent einen Stream, in den die in das Feld eingegebenen Daten fallen.
Testen Sie diesen Code.


Fehler

Was ist passiert?

Tatsächlich gilt hier die folgende Regel: Wenn ViewChild undefined zurückgibt, suchen *ngIf in der Vorlage nach *ngIf .

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

Hier ist er der Schuldige des Problems.

Überprüfen Sie außerdem die Vorlage auf andere strukturelle Anweisungen oder die ng-template über dem Problemelement.
Überlegen Sie sich mögliche Lösungen für dieses Problem.

▍Variante zur Lösung des Problems №1


Sie können das Vorlagenelement einfach ausblenden, wenn Sie es nicht benötigen. In diesem Fall bleibt das Element immer bestehen und ViewChild kann im ngAfterViewInit Hook einen Link dazu ngAfterViewInit .

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

▍Variante zur Lösung des Problems №2


Eine andere Möglichkeit, dieses Problem zu lösen, ist die Verwendung von Setzern.

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

Sobald Angular der Eigenschaft inputTag einen bestimmten Wert inputTag , erstellen wir hier einen Stream aus den im Eingabefeld eingegebenen Daten.

Im Folgenden finden Sie einige nützliche Ressourcen zu diesem Problem:

  • Hier können Sie lesen, dass die Ergebnisse von ViewChild in Angular 8 statisch und dynamisch sein können.
  • Wenn Sie Schwierigkeiten haben, mit RxJs zu arbeiten, schauen Sie sich diesen Videokurs an.

Nummer 3. Codeausführung beim Aktualisieren der mit * ngFor generierten Liste (nachdem die Elemente im DOM angezeigt wurden)


Angenommen, Sie haben eine interessante benutzerdefinierte Direktive zum Organisieren von scrollbaren Listen. Sie werden es auf eine Liste anwenden, die mit der *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> 

In solchen Fällen müssen Sie beim Aktualisieren der Liste scrollDirective.update etwas wie scrollDirective.update , um das Bildlaufverhalten unter Berücksichtigung der in der Liste vorgenommenen Änderungen zu konfigurieren.

Es scheint, dass dies mit dem ngOnChanges Hook möglich ist:

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

Hier stehen wir zwar vor einem Problem. Der Hook wird aufgerufen, bevor der Browser die aktualisierte Liste anzeigt. Infolgedessen wird die Neuberechnung der Direktivenparameter zum Scrollen der Liste falsch durchgeführt.

Wie kann ich direkt nach Abschluss der Arbeit einen Anruf *ngFor ?

Sie können dies tun, indem Sie diese 3 einfachen Schritte ausführen:

▍Schritt Nummer 1


*ngFor Sie die Links zu den Elementen ein, auf die *ngFor ( #listItems ) #listItems .

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

▍Schritt Nummer 2


ViewChildren mit dem Angular ViewChildren Dekorator eine Liste dieser Elemente ab. Es gibt eine Entität vom Typ QueryList .

▍Schritt Nummer 3


Die QueryList Klasse verfügt über eine schreibgeschützte Änderungseigenschaft , die bei jeder Änderung der Liste Ereignisse QueryList .

 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())  } ... } 

Jetzt ist das Problem behoben. Hier können Sie mit dem entsprechenden Beispiel experimentieren.

Nummer 4. Probleme mit ActivatedRoute.queryParam, die auftreten, wenn Abfragen ohne Parameter ausgeführt werden können


Der folgende Code hilft uns, die Essenz dieses Problems zu verstehen.

 // 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          });    }  } 

Einige Fragmente dieses Codes sind Kommentare wie #x . Betrachten Sie sie:

  1. Im Hauptmodul der Anwendung haben wir die Routen definiert und dort das RouterModule hinzugefügt. Routen sind so konfiguriert, dass wir den Benutzer zur /home Seite umleiten, wenn in der URL keine Route angegeben ist.
  2. Als Komponente zum Herunterladen geben wir im Hauptmodul AppComponent .
  3. AppComponent verwendet <router-outlet> um die entsprechenden AppComponent auszugeben.
  4. Nun das Wichtigste. Wir müssen queryParams für die Route von der URL erhalten

Angenommen, wir haben die folgende URL:

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

In diesem Fall queryParams aus:

 {accessToken: 'someTokenSequence'} 

Schauen wir uns die Arbeit im Browser an.


Testen einer Anwendung, die ein Routing-System implementiert

Hier haben Sie möglicherweise eine Frage zum Wesen des Problems. Wir haben die Parameter, alles funktioniert wie erwartet ...

Schauen Sie sich den obigen Screenshot des Browsers an und was in der Konsole angezeigt wird. Hier sehen Sie, dass das queryParams Objekt zweimal ausgegeben wird. Das erste Objekt ist leer und wird während des Initialisierungsprozesses des Angular-Routers ausgegeben. Erst danach erhalten wir ein Objekt, das die Anforderungsparameter enthält (in unserem Fall - {accessToken: 'someTokenSequence'} ).

Das Problem ist, dass der Router nichts zurückgibt, wenn die URL keine Anforderungsparameter enthält. Das heißt, nach der Ausgabe des ersten leeren Objekts wird das zweite ebenfalls leere Objekt, das auf das Fehlen von Parametern hinweisen könnte, nicht ausgegeben.


Wenn eine Anforderung ohne Parameter ausgeführt wird, wird das zweite Objekt nicht ausgegeben

Infolgedessen stellt sich heraus, dass der Code, wenn er ein zweites Objekt erwartet, von dem er Anforderungsdaten empfangen kann, nicht gestartet wird, wenn die URL keine Anforderungsparameter enthält.

Wie kann man dieses Problem lösen? Hier können uns RxJs helfen. Wir werden zwei beobachtbare Objekte basierend auf ActivatedRoute.queryParams erstellen. Überlegen Sie sich wie gewohnt eine schrittweise Lösung des Problems.

▍Schritt Nummer 1


Das erste beobachtbare Objekt, paramsInUrl$ , gibt Daten zurück, wenn queryParams nicht leer ist:

 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)        );     ...    } } 

▍Schritt Nummer 2


Das zweite beobachtbare Objekt, noParamsInUrl$ , gibt nur dann noParamsInUrl$ leeren Wert zurück, wenn in der URL keine Anforderungsparameter gefunden wurden:

 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(() => ({}))        );            ...    } } 

▍Schritt Nummer 3


Kombinieren Sie nun die beobachteten Objekte mit der RxJS- Zusammenführungsfunktion :

 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);        });    } } 

Jetzt gibt das beobachtete param$ -Objekt nur einmal param$ Wert zurück - unabhängig davon, ob etwas in queryParams (ein Objekt mit Abfrageparametern wird queryParams ) oder nicht (ein leeres Objekt wird queryParams ).

Sie können hier mit diesem Code experimentieren.

Nr. 5. Langsame Seiten


Angenommen, Sie haben eine Komponente, die einige formatierte Daten anzeigt:

 // 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 + '%';    } } 

Diese Komponente löst zwei Probleme:

  1. Es zeigt ein Array von Elementen an (es wird angenommen, dass diese Operation einmal ausgeführt wird). Darüber hinaus wird formatiert, was durch Aufrufen der formatItem Methode angezeigt wird.
  2. Es zeigt die Koordinaten der Maus an (dieser Wert wird offensichtlich sehr oft aktualisiert).

Sie erwarten nicht, dass diese Komponente Leistungsprobleme aufweist. Führen Sie daher nur einen Leistungstest durch, um alle Formalitäten zu erfüllen. Während dieses Tests treten jedoch einige Kuriositäten auf.


Viele formatItem-Aufrufe und ziemlich viel CPU-Auslastung

Was ist los? Tatsache ist jedoch, dass Angular beim erneuten Zeichnen der Vorlage alle Funktionen aus der Vorlage aufruft (in unserem Fall die formatItem Funktion). Wenn in den Vorlagenfunktionen umfangreiche Berechnungen durchgeführt werden, belastet dies den Prozessor und wirkt sich darauf aus, wie Benutzer die entsprechende Seite wahrnehmen.

Wie kann ich das beheben? Es reicht aus, die in formatItem durchgeführten formatItem im Voraus formatItem und die bereits bereitstehenden Daten auf der Seite anzuzeigen.

 // 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 + '%';    } } 

Jetzt sieht der Leistungstest viel anständiger aus.


Nur 6 formatItem-Aufrufe und geringe Prozessorlast

Jetzt funktioniert die Anwendung viel besser. Die hier verwendete Lösung weist jedoch einige Funktionen auf, die nicht immer angenehm sind:

  • Da wir die Koordinaten der Maus in der Vorlage mousemove , löst das mousemove immer noch eine Änderungsprüfung aus. Da wir jedoch die Koordinaten der Maus benötigen, können wir dies nicht loswerden.
  • Wenn der mousemove Ereignishandler nur einige Berechnungen durchführen muss (die sich nicht auf die Anzeige auf der Seite auswirken), können Sie Folgendes tun, um die Anwendung zu beschleunigen:

    1. Sie können NgZone.runOutsideOfAngular in der Ereignishandlerfunktion verwenden. Dies verhindert, dass die Überprüfung von Änderungen mousemove wenn das mousemove (dies mousemove sich nur auf diesen Handler aus).
    2. Sie können den Patch zone.js für einige Ereignisse verhindern, indem Sie die folgende Codezeile in polyfills.ts verwenden . Dies wirkt sich auf die gesamte Angular-Anwendung aus.

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

Wenn Sie sich für die Verbesserung der Leistung von Angular-Anwendungen interessieren - hier , hier , hier und hier - nützliche Materialien dazu.

Zusammenfassung


Nachdem Sie diesen Artikel gelesen haben, sollten in Ihrem Angular-Entwickler-Arsenal 5 neue Tools angezeigt werden, mit denen Sie einige häufig auftretende Probleme lösen können. Wir hoffen, dass die Tipps, die Sie hier gefunden haben, Ihnen helfen, Zeit zu sparen.

Liebe Leser! Wissen Sie etwas, das Ihnen hilft, Zeit bei der Entwicklung von Angular-Anwendungen zu sparen?

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


All Articles