Componente unghiulare 101 — o prezentare generală
Publicat: 2022-03-11Componentele au fost disponibile în Angular încă de la început; cu toate acestea, mulți oameni încă folosesc componente incorect. În munca mea, am văzut oameni care nu le folosesc deloc, creând componente în loc de directive de atribute și multe altele. Acestea sunt probleme pe care dezvoltatorii Angular juniori și seniori tind să le facă, inclusiv eu. Și așa, această postare este pentru oameni ca mine pe vremea când învățam Angular, deoarece nici documentația oficială, nici documentația neoficială despre componente nu explică cu cazuri de utilizare cum și când să folosesc componentele.
În acest articol, voi identifica modalitățile corecte și incorecte de utilizare a componentelor Angular, cu exemple. Această postare ar trebui să vă ofere o idee clară despre:
- Definirea componentelor unghiulare;
- Când ar trebui să creați componente unghiulare separate; și
- Când nu ar trebui să creați componente unghiulare separate.
Înainte de a putea trece la utilizarea corectă a componentelor Angular, vreau să ating pe scurt subiectul componentelor în general. În general, fiecare aplicație Angular are cel puțin o componentă implicită - root component
. De acolo, depinde de noi cum să ne proiectăm aplicația. De obicei, ați crea o componentă pentru pagini separate, iar apoi fiecare pagină ar conține o listă de componente separate. De regulă, o componentă trebuie să îndeplinească următoarele criterii:
- Trebuie să aibă o clasă definită, care conține date și logică; și
- Trebuie să fie asociat cu un șablon HTML care afișează informații pentru utilizatorul final.
Să ne imaginăm un scenariu în care avem o aplicație care are două pagini: Sarcini viitoare și Sarcini finalizate . În pagina Sarcini viitoare , putem vizualiza sarcinile viitoare, le putem marca ca „terminate” și, în final, putem adăuga sarcini noi. În mod similar, în pagina Sarcini finalizate , putem vizualiza sarcinile finalizate și le putem marca „anulate”. În cele din urmă, avem legături de navigare, care ne permit să navigăm între pagini. Acestea fiind spuse, putem împărți următoarea pagină în trei secțiuni: componentă rădăcină, pagini, componente reutilizabile.
Pe baza capturii de ecran de mai sus, putem vedea clar că structura aplicației ar arăta cam așa:
── 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
Deci, haideți să legăm fișierele componente cu elementele reale de pe wireframe de mai sus:
-
header-menu.component
șitask-list.component
sunt componente reutilizabile, care sunt afișate cu un chenar verde în captura de ecran wireframe; -
upcoming-tasks.component
șicompleted-tasks.component
sunt pagini, care sunt afișate cu un chenar galben în captura de ecran wireframe de mai sus; și - În cele din urmă,
app.component
este componenta rădăcină, care este afișată cu un chenar roșu în captura de ecran wireframe.
Deci, acestea fiind spuse, putem specifica logica și designul separat pentru fiecare componentă. Pe baza wireframe-urilor de mai sus, avem două pagini care refolosesc o componentă— task-list.component
. Ar apărea întrebarea: cum specificăm ce tip de date ar trebui să afișăm pe o anumită pagină? Din fericire, nu trebuie să ne facem griji pentru asta, deoarece atunci când creați o componentă, puteți specifica și variabilele de intrare și de ieșire .
Variabile de intrare
Variabilele de intrare sunt utilizate în cadrul componentelor pentru a transmite unele date de la componenta părinte. În exemplul de mai sus, am putea avea doi parametri de intrare pentru task-list.component
— tasks
și listType
. În consecință, tasks
ar fi o listă de șiruri care ar afișa fiecare șir pe un rând separat, în timp ce listType
ar fi fie viitoare , fie finalizată , ceea ce ar indica dacă caseta de selectare este bifată. Mai jos, puteți găsi un mic fragment de cod despre cum ar putea arăta componenta reală.
// 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() { } }
Variabile de ieșire
Similar cu variabilele de intrare, variabilele de ieșire pot fi folosite și pentru a transmite unele informații între componente, dar de data aceasta către componenta părinte. De exemplu, pentru task-list.component
, am putea avea variabila de ieșire itemChecked
. Ar informa componenta părinte dacă un articol a fost bifat sau debifat. Variabilele de ieșire trebuie să fie emițători de evenimente. Mai jos, puteți găsi un mic fragment de cod despre cum ar putea arăta componenta cu o variabilă de ieșire.
// 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); } }
task-list.component.ts
după adăugarea variabilelor de ieșireUtilizarea componentelor secundare și legarea variabilelor
Să aruncăm o privire la cum să folosiți această componentă în componenta părinte și cum să faceți diferite tipuri de legături variabile. În Angular, există două moduri de a lega variabilele de intrare: legarea unidirecțională, ceea ce înseamnă că proprietatea trebuie să fie înfășurată în paranteze pătrate []
și legarea în două sensuri, ceea ce înseamnă că proprietatea trebuie să fie înfășurată între paranteze pătrate și rotunde. [()]
. Aruncați o privire la exemplul de mai jos și vedeți diferitele moduri în care datele pot fi transmise între componente.
<h1>Upcoming Tasks</h1> <app-task-list [(tasks)]="upcomingTasks" [listType]="'upcoming'" (itemChecked)="onItemChecked($event)"></app-task-list>
upcoming-tasks.component.html
Să trecem prin fiecare parametru:
- Parametrul
tasks
este transmis utilizând legarea în două sensuri. Aceasta înseamnă că, în cazul în care parametrul tasks este modificat în cadrul componentei fiu, componenta părinte va reflecta acele modificări în variabilaupcomingTasks
. Pentru a permite legarea bidirecțională a datelor, trebuie să creați un parametru de ieșire, care urmează șablonul „[inputParameterName]Change”, în acest caztasksChange
. - Parametrul
listType
este transmis utilizând legarea unidirecțională. Aceasta înseamnă că poate fi schimbat în componenta fiu, dar nu se va reflecta în componenta părinte. Țineți minte că aș fi putut atribui valoarea'upcoming'
unui parametru din componentă și aș fi putut să-l transmit în schimb — n-ar avea nicio diferență. - În cele din urmă, parametrul
itemChecked
este o funcție de ascultare și va fi apelat ori de câte ori onItemCheck este executat petask-list.component
. Dacă un element este bifat,$event
va păstra valoareatrue
, dar, dacă este debifat, ar păstra valoareafalse
.
După cum puteți vedea, în general, Angular oferă o modalitate excelentă de a transmite și de a partaja informații între mai multe componente, așa că nu ar trebui să vă fie teamă să le utilizați. Doar asigurați-vă că le folosiți cu înțelepciune și nu le folosiți excesiv.
Când să creați o componentă unghiulară separată
După cum am menționat anterior, nu ar trebui să vă fie frică să utilizați componentele Angular, dar cu siguranță ar trebui să le folosiți cu înțelepciune.
Deci, când ar trebui să creați componente unghiulare?
- Ar trebui să creați întotdeauna o componentă separată dacă componenta poate fi reutilizată în mai multe locuri, ca în cazul nostru
task-list.component
. Le numim componente reutilizabile . - Ar trebui să luați în considerare crearea unei componente separate dacă componenta va face componenta părinte mai lizibilă și le va permite să adauge o acoperire suplimentară de testare. Le putem numi componente de organizare a codului .
- Ar trebui să creați întotdeauna o componentă separată dacă aveți o parte a unei pagini care nu trebuie actualizată des și doriți să creșteți performanța. Acest lucru este legat de strategia de detectare a schimbării. Le putem numi componente de optimizare .
Aceste trei reguli mă ajută să identific dacă trebuie să creez o nouă componentă și îmi oferă automat o viziune clară asupra rolului pentru componentă. În mod ideal, atunci când creați o componentă, ar trebui să știți deja care va fi rolul acesteia în cadrul aplicației.

Deoarece am analizat deja utilizarea componentelor reutilizabile , să aruncăm o privire asupra utilizării componentelor de organizare a codului . Să ne imaginăm că avem un formular de înregistrare și, în partea de jos a formularului, avem o casetă cu Termeni și Condiții . De obicei, limbajul legal tinde să fie foarte mare, ocupând mult spațiu – în acest caz, într-un șablon HTML. Deci, să ne uităm la acest exemplu și apoi să vedem cum l-am putea schimba.
La început, avem o componentă — registration.component
— care conține totul, inclusiv formularul de înregistrare, precum și termenii și condițiile în sine.
<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>
Șablonul arată acum mic, dar imaginați-vă dacă am înlocui „Text cu termeni și condiții foarte lungi” cu un fragment real de text care are mai mult de 1000 de cuvinte - ar face editarea fișierului dificilă și inconfortabilă. Avem o soluție rapidă pentru asta - am putea inventa o nouă componentă terms-and-conditions.component
, care ar conține tot ce are legătură cu termenii și condițiile. Deci, să aruncăm o privire la fișierul HTML pentru terms-and-conditions.component
.
<div class="terms-and-conditions-box"> Text with very long terms and conditions. </div>
Și acum putem ajusta registration.component
și să folosim terms-and-conditions.component
din cadrul acesteia.
<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>
Felicitări! Tocmai am redus dimensiunea registration.component
cu sute de rânduri și am făcut mai ușor de citit codul. În exemplul de mai sus, am făcut modificări șablonului componentei, dar același principiu ar putea fi aplicat logicii componentei.
În cele din urmă, pentru componentele de optimizare , aș sugera cu tărie să parcurgeți această postare, deoarece vă va oferi toate informațiile necesare pentru a înțelege detectarea schimbărilor și în ce cazuri specifice le puteți aplica. Nu îl veți folosi des, dar ar putea exista unele cazuri și, dacă puteți sări peste verificările regulate ale mai multor componente atunci când nu este necesar, este un câștig pentru performanță.
Acestea fiind spuse, nu ar trebui să creăm întotdeauna componente separate, așa că haideți să vedem când ar trebui să evitați crearea unei componente separate.
Când să evitați crearea unei componente unghiulare separate
Există trei puncte principale pe baza cărora sugerez să creez o componentă Angular separată, dar există cazuri în care aș evita să creez și o componentă separată.
Din nou, să aruncăm o privire la punctele marcatoare care vă vor permite să înțelegeți cu ușurință când nu ar trebui să creați o componentă separată.
- Nu ar trebui să creați componente pentru manipulări DOM. Pentru acestea, ar trebui să utilizați directive de atribut.
- Nu ar trebui să creați componente dacă acest lucru vă va face codul mai haotic. Acesta este opusul componentelor de organizare a codului .
Acum, să aruncăm o privire mai atentă și să vedem ambele cazuri în exemple. Să ne imaginăm că vrem să avem un buton care să înregistreze mesajul atunci când este apăsat. Acest lucru ar putea fi greșit și ar putea fi creată o componentă separată pentru această funcționalitate specifică, care ar menține un anumit buton și acțiuni. Să verificăm mai întâi abordarea incorectă:
// 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); } }
log-button.component.ts
Și, în consecință, această componentă este însoțită de următoarea vizualizare html.
<button (click)="onButtonClick(true)">{{ name }}</button>
log-button.component.html
După cum puteți vedea, exemplul de mai sus ar funcționa și puteți utiliza componenta de mai sus în toate vizualizările dvs. Ar face ce vrei tu, din punct de vedere tehnic. Dar soluția corectă aici ar fi folosirea directivelor. Acest lucru v-ar permite nu numai să reduceți cantitatea de cod pe care trebuie să o scrieți, ci și să adăugați posibilitatea de a aplica această funcționalitate la orice element doriți, nu doar la butoane.
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
, care poate fi atribuită oricărui element Acum, când ne-am creat directiva, o putem folosi pur și simplu în aplicația noastră și o putem atribui oricărui element dorim. De exemplu, să reutilizam registration.component
.
<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
utilizată pe butonul formularelor de înregistrare Acum, putem arunca o privire asupra celui de-al doilea caz, în care nu ar trebui să creăm componente separate, iar acesta este opusul componentelor de optimizare a codului . Dacă componenta nou creată face codul dvs. mai complicat și mai mare, nu este nevoie să o creați. Să luăm ca exemplu registration.component
noastră. Un astfel de caz ar fi crearea unei componente separate pentru etichetă și câmpul de intrare cu o mulțime de parametri de intrare. Să aruncăm o privire la această practică proastă.
// 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() { } }
form-input-with-label.component
Și aceasta ar putea fi punctul de vedere pentru această componentă.
<label for="{{ id }}">{{ label }}</label><br /> <input type="{{ type }}" name="{{ name }}" [(ngModel)]="model" /><br />
form-input-with-label.component
Sigur, cantitatea de cod ar fi redusă în registration.component
, dar face logica generală a codului mai ușor de înțeles și de citit? Cred că putem vedea clar că face codul inutil mai complex decât era înainte.
Următorii pași: Componente unghiulare 102?
Pentru a rezuma: Nu vă fie teamă să folosiți componente; asigurați-vă doar că aveți o viziune clară despre ceea ce doriți să realizați. Scenariile pe care le-am enumerat mai sus sunt cele mai comune și le consider a fi cele mai importante și comune; totuși, scenariul tău poate fi unic și depinde de tine să iei o decizie în cunoștință de cauză. Sper că ai învățat suficient pentru a lua o decizie bună.
Dacă doriți să aflați mai multe despre strategiile Angular de detectare a schimbărilor și strategia OnPush, vă recomand să citiți Angular Change Detection și OnPush Strategy. Este strâns legat de componente și, așa cum am menționat deja în postare, poate îmbunătăți semnificativ performanța aplicației.
Deoarece componentele sunt doar o parte din directivele pe care Angular le oferă, ar fi grozav să cunoaștem și directivele de atribut și directivele structurale . Înțelegerea tuturor directivelor va face, cel mai probabil, mai ușor pentru programator să scrie un cod mai bun.