Winkelkomponenten 101 – ein Überblick

Veröffentlicht: 2022-03-11

Komponenten waren von Anfang an in Angular verfügbar; Viele Menschen finden sich jedoch immer noch dabei, Komponenten falsch zu verwenden. Bei meiner Arbeit habe ich Leute gesehen, die sie überhaupt nicht benutzten, Komponenten anstelle von Attributdirektiven erstellten und mehr. Dies sind Probleme, die sowohl Junior- als auch Senior-Angular-Entwickler machen, mich eingeschlossen. Und so ist dieser Beitrag für Leute wie mich, als ich Angular lernte, da weder die offizielle Dokumentation noch die inoffizielle Dokumentation über Komponenten anhand von Anwendungsfällen erklären, wie und wann die Komponenten verwendet werden.

In diesem Artikel werde ich anhand von Beispielen die richtigen und falschen Möglichkeiten zur Verwendung von Angular-Komponenten identifizieren. Dieser Beitrag sollte Ihnen eine klare Vorstellung davon geben:

  • Die Definition von Winkelkomponenten;
  • Wann Sie separate Angular-Komponenten erstellen sollten ; und
  • Wann Sie keine separaten Angular-Komponenten erstellen sollten .

Titelbild für Angular-Komponenten

Bevor wir uns auf die korrekte Verwendung von Angular-Komponenten stürzen können, möchte ich kurz auf das Komponententhema im Allgemeinen eingehen. Im Allgemeinen hat jede einzelne Angular-Anwendung standardmäßig mindestens eine Komponente – die root component . Von da an liegt es an uns, wie wir unsere Anwendung gestalten. Normalerweise würden Sie eine Komponente für separate Seiten erstellen, und dann würde jede Seite eine Liste separater Komponenten enthalten. Als Faustregel gilt, dass ein Bauteil folgende Kriterien erfüllen muss:

  • Muss eine Klasse definiert haben, die Daten und Logik enthält; und
  • Muss mit einer HTML-Vorlage verknüpft sein, die Informationen für den Endbenutzer anzeigt.

Stellen wir uns ein Szenario vor, in dem wir eine Anwendung haben, die zwei Seiten hat: Anstehende Aufgaben und Erledigte Aufgaben . Auf der Seite Anstehende Aufgaben können wir anstehende Aufgaben anzeigen, sie als „erledigt“ markieren und schließlich neue Aufgaben hinzufügen. In ähnlicher Weise können wir auf der Seite Abgeschlossene Aufgaben abgeschlossene Aufgaben anzeigen und sie als „rückgängig“ markieren. Schließlich haben wir Navigationslinks, die es uns ermöglichen, zwischen den Seiten zu navigieren. Davon abgesehen können wir die folgende Seite in drei Abschnitte aufteilen: Stammkomponente, Seiten, wiederverwendbare Komponenten.

Diagramm der anstehenden Aufgaben und erledigten Aufgaben

Beispielanwendung Wireframe mit farblich getrennten Komponentenabschnitten

Anhand des obigen Screenshots können wir deutlich erkennen, dass die Struktur der Anwendung in etwa so aussehen würde:

 ── myTasksApplication ├── components │ ├── header-menu.component.ts │ ├── header-menu.component.html │ ├── task-list.component.ts │ └── task-list.component.html ├── pages │ ├── upcoming-tasks.component.ts │ ├── upcoming-tasks.component.html │ ├── completed-tasks.component.ts │ └── completed-tasks.component.html ├── app.component.ts └── app.component.html

Verknüpfen wir also die Komponentendateien mit den tatsächlichen Elementen auf dem Drahtmodell oben:

  • header-menu.component und task-list.component sind wiederverwendbare Komponenten, die im Wireframe-Screenshot grün umrandet dargestellt werden;
  • upcoming-tasks.component und „finished completed-tasks.component sind Seiten, die im obigen Wireframe-Screenshot mit einem gelben Rand dargestellt werden; und
  • Schließlich ist app.component die Root-Komponente, die im Wireframe-Screenshot mit einem roten Rand angezeigt wird.

Davon abgesehen können wir für jede einzelne Komponente eine separate Logik und ein separates Design festlegen. Basierend auf den obigen Wireframes haben wir zwei Seiten, die eine Komponente wiederverwenden task-list.component . Es würde sich die Frage stellen: Wie legen wir fest, welche Art von Daten wir auf einer bestimmten Seite anzeigen sollen? Glücklicherweise müssen wir uns darüber keine Gedanken machen, denn wenn Sie eine Komponente erstellen, können Sie auch Eingabe- und Ausgabevariablen angeben.

Eingabevariablen

Eingabevariablen werden innerhalb der Komponenten verwendet, um einige Daten von der übergeordneten Komponente zu übergeben. Im obigen Beispiel könnten wir zwei Eingabeparameter für die task-list.componenttasks und listType . Dementsprechend wären tasks eine Liste von Zeichenfolgen, die jede Zeichenfolge in einer separaten Zeile anzeigen würden, während listType entweder „ coming “ oder „ completed “ wäre, was anzeigen würde, ob das Kontrollkästchen aktiviert ist. Unten finden Sie ein kleines Code-Snippet, wie die eigentliche Komponente aussehen könnte.

 // task-list.component.ts import { Component, Input } from '@angular/core'; @Component({ selector: 'app-task-list', templateUrl: 'task-list.component.html' }) export class TaskListComponent { @Input() tasks: string[] = []; // List of tasks which should be displayed. @Input() listType: 'upcoming' | 'completed' = 'upcoming'; // Type of the task list. constructor() { } }

Ausgabevariablen

Ähnlich wie Eingabevariablen können auch Ausgabevariablen verwendet werden, um einige Informationen zwischen den Komponenten auszutauschen, diesmal jedoch an die übergeordnete Komponente. Beispielsweise könnten wir für task-list.component die Ausgabevariable itemChecked haben. Es würde die übergeordnete Komponente darüber informieren, ob ein Element aktiviert oder deaktiviert wurde. Ausgangsvariablen müssen Ereignis-Emitter sein. Unten finden Sie ein kleines Code-Snippet, wie die Komponente mit einer Ausgabevariablen aussehen könnte.

 // task-list.component.ts import { Component, Input, Output } from '@angular/core'; @Component({ selector: 'app-task-list', templateUrl: 'task-list.component.html' }) export class TaskListComponent { @Input() tasks: string[] = []; // List of tasks which should be displayed. @Input() listType: 'upcoming' | 'completed' = 'upcoming'; // Type of the task list. @Output() itemChecked: EventEmitter<boolean> = new EventEmitter(); @Output() tasksChange: EventEmitter<string[]> = new EventEmitter(); constructor() { } /** * Is called when an item from the list is checked. * @param selected---Value which indicates if the item is selected or deselected. */ onItemCheck(selected: boolean) { this.itemChecked.emit(selected); } /** * Is called when task list is changed. * @param changedTasks---Changed task list value, which should be sent to the parent component. */ onTasksChanged(changedTasks: string[]) { this.taskChange.emit(changedTasks); } }
Möglicher Inhalt von task-list.component.ts nach dem Hinzufügen von Ausgabevariablen

Verwendung von untergeordneten Komponenten und Variablenbindung

Schauen wir uns an, wie diese Komponente innerhalb der übergeordneten Komponente verwendet wird und wie verschiedene Arten von Variablenbindungen durchgeführt werden. In Angular gibt es zwei Möglichkeiten, die Eingabevariablen zu binden – unidirektionale Bindung, was bedeutet, dass die Eigenschaft in eckige Klammern [] eingeschlossen werden muss, und bidirektionale Bindung, was bedeutet, dass Eigenschaft in eckige und runde Klammern eingeschlossen werden muss [()] . Sehen Sie sich das folgende Beispiel an und sehen Sie sich die verschiedenen Möglichkeiten an, wie Daten zwischen Komponenten übergeben werden können.

 <h1>Upcoming Tasks</h1> <app-task-list [(tasks)]="upcomingTasks" [listType]="'upcoming'" (itemChecked)="onItemChecked($event)"></app-task-list>
Möglicher Inhalt upcoming-tasks.component.html

Lassen Sie uns jeden Parameter durchgehen:

  • Der tasks -Parameter wird mit bidirektionaler Bindung übergeben. Das bedeutet, dass, falls der Tasks-Parameter innerhalb der untergeordneten Komponente geändert wird, die übergeordnete Komponente diese Änderungen in der upcomingTasks Variablen widerspiegelt. Um eine bidirektionale Datenbindung zu ermöglichen, müssen Sie einen Ausgabeparameter erstellen, der der Vorlage „[inputParameterName]Change“ folgt, in diesem Fall tasksChange .
  • Der listType -Parameter wird mit unidirektionaler Bindung übergeben. Das bedeutet, dass es innerhalb der untergeordneten Komponente geändert werden kann, sich aber nicht in der übergeordneten Komponente widerspiegelt. Denken Sie daran, dass ich einem Parameter innerhalb der Komponente den Wert 'upcoming' hätte zuweisen und stattdessen diesen übergeben können – es würde keinen Unterschied machen.
  • Schließlich ist der Parameter itemChecked eine Listener-Funktion und wird immer dann aufgerufen, wenn onItemCheck auf der task-list.component ausgeführt wird. Wenn ein Element aktiviert ist, würde $event den Wert true enthalten, aber wenn es nicht aktiviert ist, würde es den Wert false enthalten.

Wie Sie sehen können, bietet Angular im Allgemeinen eine großartige Möglichkeit, Informationen zwischen mehreren Komponenten zu übertragen und auszutauschen, sodass Sie keine Angst haben sollten, sie zu verwenden. Stellen Sie nur sicher, dass Sie sie mit Bedacht einsetzen und nicht überbeanspruchen.

Wann eine separate Winkelkomponente erstellt werden sollte

Wie bereits erwähnt, sollten Sie keine Angst davor haben, Angular-Komponenten zu verwenden, aber Sie sollten sie auf jeden Fall mit Bedacht einsetzen.

Diagramm der Winkelkomponenten in Aktion

Setzen Sie Angular-Komponenten sinnvoll ein

Wann sollten Sie also Angular-Komponenten erstellen?

  • Sie sollten immer eine separate Komponente erstellen, wenn die Komponente an mehreren Stellen wiederverwendet werden kann, wie bei unserer task-list.component . Wir nennen sie wiederverwendbare Komponenten .
  • Sie sollten die Erstellung einer separaten Komponente in Betracht ziehen , wenn die Komponente die übergeordnete Komponente lesbarer macht und es ihr ermöglicht, zusätzliche Testabdeckung hinzuzufügen. Wir können sie Code - Organisationskomponenten nennen .
  • Sie sollten immer dann eine separate Komponente erstellen, wenn Sie einen Teil einer Seite haben, der nicht oft aktualisiert werden muss, und die Leistung erhöhen möchten. Dies hängt mit der Änderungserkennungsstrategie zusammen. Wir können sie Optimierungskomponenten nennen.

Diese drei Regeln helfen mir zu erkennen, ob ich eine neue Komponente erstellen muss, und sie geben mir automatisch eine klare Vorstellung von der Rolle der Komponente. Wenn Sie eine Komponente erstellen, sollten Sie im Idealfall bereits wissen, welche Rolle sie in der Anwendung spielen wird.

Da wir uns bereits mit der Verwendung von wiederverwendbaren Komponenten befasst haben, werfen wir einen Blick auf die Verwendung von Code-Organisationskomponenten . Stellen wir uns vor, wir haben ein Registrierungsformular und am Ende des Formulars haben wir ein Feld mit den Allgemeinen Geschäftsbedingungen . Normalerweise ist die juristische Sprache sehr groß und nimmt viel Platz ein – in diesem Fall in einer HTML-Vorlage. Schauen wir uns also dieses Beispiel an und sehen dann, wie wir es möglicherweise ändern könnten.

Am Anfang haben wir eine Komponente – registration.component – ​​die alles enthält, einschließlich des Registrierungsformulars sowie der Geschäftsbedingungen selbst.

 <h2>Registration</h2> <label for="username">Username</label><br /> <input type="text" name="username" [(ngModel)]="username" /><br /> <label for="password">Password</label><br /> <input type="password" name="password" [(ngModel)]="password" /><br /> <div class="terms-and-conditions-box"> Text with very long terms and conditions. </div> <button (click)="onRegistrate()">Registrate</button>
Anfangszustand vor dem Aufteilen von </code>registration.component</code> in mehrere Komponenten

Die Vorlage sieht jetzt klein aus, aber stellen Sie sich vor, wir würden „Text mit sehr langen Geschäftsbedingungen“ durch einen tatsächlichen Textabschnitt mit mehr als 1000 Wörtern ersetzen – das würde die Bearbeitung der Datei schwierig und unbequem machen. Dafür haben wir eine schnelle Lösung – wir könnten eine neue Komponente terms-and-conditions.component erfinden, die alles enthalten würde, was mit den allgemeinen Geschäftsbedingungen zu tun hat. Schauen wir uns also die HTML-Datei für die terms-and-conditions.component an.

 <div class="terms-and-conditions-box"> Text with very long terms and conditions. </div>
Neu erstellte HTML-Vorlage </code>terms-and-conditions.component</code>

Und jetzt können wir die registration.component anpassen und die terms-and-conditions.component darin verwenden.

 <h2>Registration</h2> <label for="username">Username</label><br /> <input type="text" name="username" [(ngModel)]="username" /><br /> <label for="password">Password</label><br /> <input type="password" name="password" [(ngModel)]="password" /><br /> <app-terms-and-conditions></app-terms-and-conditions> <button (click)="onRegistrate()">Registrate</button>
Aktualisierte </code>registration.component.ts</code>-Vorlage mit Codeorganisationskomponente

Glückwünsche! Wir haben lediglich die Größe der registration.component um Hunderte von Zeilen verringert und das Lesen des Codes erleichtert. Im obigen Beispiel haben wir Änderungen an der Vorlage der Komponente vorgenommen, aber das gleiche Prinzip könnte auf die Logik der Komponente angewendet werden.

Abschließend möchte ich Ihnen für die Optimierungskomponenten dringend empfehlen, diesen Beitrag durchzugehen, da er Ihnen alle Informationen liefert, die zum Verständnis der Änderungserkennung erforderlich sind, und auf welche konkreten Fälle Sie sie anwenden können. Sie werden es nicht oft verwenden, aber es kann einige Fälle geben, und wenn Sie regelmäßige Überprüfungen mehrerer Komponenten überspringen können, wenn dies nicht erforderlich ist, ist dies eine Win-Win-Situation für die Leistung.

Davon abgesehen sollten wir nicht immer separate Komponenten erstellen, also schauen wir uns an, wann Sie es vermeiden sollten, eine separate Komponente zu erstellen.

Wann die Erstellung einer separaten Winkelkomponente vermieden werden sollte

Es gibt drei Hauptpunkte, auf deren Grundlage ich vorschlage, eine separate Angular-Komponente zu erstellen, aber es gibt Fälle, in denen ich es vermeiden würde, auch eine separate Komponente zu erstellen.

Abbildung von zu vielen Komponenten

Zu viele Komponenten werden Sie verlangsamen.

Lassen Sie uns noch einmal einen Blick auf die Aufzählungspunkte werfen, anhand derer Sie leicht verstehen können, wann Sie keine separate Komponente erstellen sollten .

  • Sie sollten keine Komponenten für DOM-Manipulationen erstellen. Für diese sollten Sie Attributdirektiven verwenden.
  • Sie sollten keine Komponenten erstellen, wenn dies Ihren Code chaotischer macht. Dies ist das Gegenteil von Code-Organisationskomponenten .

Schauen wir uns nun etwas genauer an und sehen uns beide Fälle in Beispielen an. Stellen wir uns vor, wir möchten eine Schaltfläche haben, die die Nachricht protokolliert, wenn darauf geklickt wird. Dies könnte ein Fehler sein und es könnte eine separate Komponente für diese spezielle Funktionalität erstellt werden, die eine bestimmte Schaltfläche und Aktionen enthalten würde. Überprüfen wir zunächst den falschen Ansatz:

 // log-button.component.ts import { Component, Input, Output } from '@angular/core'; @Component({ selector: 'app-log-button', templateUrl: 'log-button.component.html' }) export class LogButtonComponent { @Input() name: string; // Name of the button. @Output() buttonClicked: EventEmitter<boolean> = new EventEmitter(); constructor() { } /** * Is called when button is clicked. * @param clicked - Value which indicates if the button was clicked. */ onButtonClick(clicked: boolean) { console.log('I just clicked a button on this website'); this.buttonClicked.emit(clicked); } }
Logik der falschen Komponente log-button.component.ts

Und dementsprechend wird diese Komponente von der folgenden HTML-Ansicht begleitet.

 <button (click)="onButtonClick(true)">{{ name }}</button>
Vorlage der falschen Komponente log-button.component.html

Wie Sie sehen können, würde das obige Beispiel funktionieren, und Sie könnten die obige Komponente in Ihren Ansichten verwenden. Es würde technisch tun, was Sie wollen. Aber die richtige Lösung wäre hier die Verwendung von Direktiven. Dadurch könnten Sie nicht nur die Menge an Code, die Sie schreiben müssen, reduzieren, sondern auch die Möglichkeit hinzufügen, diese Funktionalität auf jedes gewünschte Element anzuwenden, nicht nur auf Schaltflächen.

 import { Directive } from '@angular/core'; @Directive({ selector: "[logButton]", hostListeners: { 'click': 'onButtonClick()', }, }) class LogButton { constructor() {} /** * Fired when element is clicked. */ onButtonClick() { console.log('I just clicked a button on this website'); } }
logButton Direktive, die jedem Element zugewiesen werden kann

Wenn wir nun unsere Direktive erstellt haben, können wir sie einfach in unserer gesamten Anwendung verwenden und sie jedem gewünschten Element zuweisen. Lassen Sie uns zum Beispiel unsere registration.component wiederverwenden.

 <h2>Registration</h2> <label for="username">Username</label><br /> <input type="text" name="username" [(ngModel)]="username" /><br /> <label for="password">Password</label><br /> <input type="password" name="password" [(ngModel)]="password" /><br /> <app-terms-and-conditions></app-terms-and-conditions> <button (click)="onRegistrate()" logButton>Registrate</button>
logButton Direktive, die auf der Registrierungsformular-Schaltfläche verwendet wird

Jetzt können wir uns den zweiten Fall ansehen, in dem wir keine separaten Komponenten erstellen sollten, und das ist das Gegenteil zu den Codeoptimierungskomponenten . Wenn die neu erstellte Komponente Ihren Code komplizierter und größer macht, müssen Sie ihn nicht erstellen. Nehmen wir als Beispiel unsere registration.component . Ein solcher Fall wäre, eine separate Komponente für Beschriftung und Eingabefeld mit einer Unmenge von Eingabeparametern zu erstellen. Werfen wir einen Blick auf diese schlechte Praxis.

 // form-input-with-label.component.ts import { Component, Input} from '@angular/core'; @Component({ selector: 'app-form-input-with-label', templateUrl: 'form-input-with-label.component.html' }) export class FormInputWithLabelComponent { @Input() name: string; // Name of the field @Input() id: string; // Id of the field @Input() label: string; // Label of the field @Input() type: 'text' | 'password'; // Type of the field @Input() model: any; // Model of the field constructor() { } }
Logik der form-input-with-label.component

Und dies könnte die Ansicht für diese Komponente sein.

 <label for="{{ id }}">{{ label }}</label><br /> <input type="{{ type }}" name="{{ name }}" [(ngModel)]="model" /><br />
Ansicht der form-input-with-label.component

Sicher, die Codemenge würde in registration.component reduziert, aber macht es die Gesamtlogik des Codes verständlicher und lesbarer? Ich denke, wir können deutlich sehen, dass der Code dadurch unnötig komplexer wird als zuvor.

Nächste Schritte: Winkelkomponenten 102?

Zusammenfassend: Scheuen Sie sich nicht, Komponenten zu verwenden; Stellen Sie einfach sicher, dass Sie eine klare Vorstellung davon haben, was Sie erreichen möchten. Die Szenarien, die ich oben aufgelistet habe, sind die häufigsten, und ich halte sie für die wichtigsten und häufigsten; Ihr Szenario kann jedoch einzigartig sein, und es liegt an Ihnen, eine fundierte Entscheidung zu treffen. Ich hoffe, Sie haben genug gelernt, um eine gute Entscheidung zu treffen.

Wenn Sie mehr über die Änderungserkennungsstrategien von Angular und die OnPush-Strategie erfahren möchten, empfehle ich die Lektüre von Angular Change Detection and the OnPush Strategy. Es ist eng mit Komponenten verwandt und kann, wie ich bereits im Beitrag erwähnt habe, die Leistung der Anwendung erheblich verbessern.

Da Komponenten nur ein Teil der Direktiven sind, die Angular bereitstellt, wäre es toll, auch Attributdirektiven und Strukturdirektiven kennenzulernen. Das Verständnis aller Direktiven wird es dem Programmierer höchstwahrscheinlich erleichtern, insgesamt besseren Code zu schreiben.