Komponenty kątowe 101 — przegląd
Opublikowany: 2022-03-11Komponenty były dostępne w Angular od samego początku; jednak wiele osób nadal niewłaściwie używa komponentów. W swojej pracy widziałem ludzi, którzy w ogóle ich nie używają, tworząc komponenty zamiast dyrektyw atrybutów i nie tylko. Są to problemy, które zarówno młodsi, jak i starsi programiści Angulara, łącznie ze mną, robią. I tak ten post jest dla ludzi takich jak ja, kiedy uczyłem się Angulara, ponieważ ani oficjalna dokumentacja, ani nieoficjalna dokumentacja komponentów nie wyjaśniają z przypadkami użycia, jak i kiedy używać komponentów.
W tym artykule zidentyfikuję poprawne i niepoprawne sposoby wykorzystania komponentów Angular wraz z przykładami. Ten post powinien dać ci jasne wyobrażenie o:
- Definicja komponentów kątowych;
- Kiedy powinieneś tworzyć oddzielne komponenty Angular; oraz
- Kiedy nie powinieneś tworzyć oddzielnych komponentów Angulara.
Zanim przejdziemy do prawidłowego użycia komponentów Angulara, chciałbym krótko poruszyć temat komponentów w ogóle. Ogólnie rzecz biorąc, każda aplikacja Angulara ma domyślnie co najmniej jeden składnik — root component . Od tego momentu od nas zależy, jak zaprojektujemy naszą aplikację. Zwykle tworzysz komponent dla oddzielnych stron, a następnie każda strona zawiera listę oddzielnych komponentów. Z reguły komponent musi spełniać następujące kryteria:
- Musi mieć zdefiniowaną klasę, która przechowuje dane i logikę; oraz
- Musi być powiązany z szablonem HTML, który wyświetla informacje dla użytkownika końcowego.
Wyobraźmy sobie scenariusz, w którym mamy aplikację, która ma dwie strony: Nadchodzące zadania i Ukończone zadania . Na stronie Nadchodzące zadania możemy przeglądać nadchodzące zadania, oznaczać je jako „zrobione”, a na koniec dodawać nowe zadania. Podobnie na stronie Ukończone zadania możemy wyświetlić ukończone zadania i oznaczyć je jako „niewykonane”. Wreszcie mamy linki nawigacyjne, które pozwalają nam nawigować między stronami. Mając to na uwadze, możemy podzielić następującą stronę na trzy sekcje: komponent główny, strony, komponenty wielokrotnego użytku.
Na podstawie powyższego zrzutu ekranu widać wyraźnie, że struktura aplikacji wyglądałaby mniej więcej tak:
── 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.htmlPołączmy więc pliki składowe z rzeczywistymi elementami na powyższym modelu szkieletowym:
-
header-menu.componentitask-list.componentto komponenty wielokrotnego użytku, które są wyświetlane z zielonym obramowaniem na zrzucie ekranu szkieletowego; -
upcoming-tasks.componenticompleted-tasks.componentto strony, które są wyświetlane z żółtą ramką na zrzucie ekranu powyżej; oraz - Wreszcie
app.componentto główny składnik, który jest wyświetlany z czerwoną ramką na zrzucie ekranu szkieletowego.
Mając to na uwadze, możemy określić oddzielną logikę i projekt dla każdego komponentu. W oparciu o powyższe makiety mamy dwie strony, które ponownie wykorzystują jeden komponent task-list.component . Powstałoby pytanie – jak określić, jakie dane powinniśmy pokazywać na konkretnej stronie? Na szczęście nie musimy się o to martwić, ponieważ podczas tworzenia komponentu możesz również określić zmienne wejściowe i wyjściowe .
Zmienne wejściowe
Zmienne wejściowe są używane w komponentach do przekazywania niektórych danych z komponentu nadrzędnego. W powyższym przykładzie moglibyśmy mieć dwa parametry wejściowe dla task-list.component — tasks i listType . W związku z tym tasks byłyby listą ciągów, która wyświetlałaby każdy ciąg w osobnym wierszu, podczas gdy listType byłaby albo nadchodząca , albo zakończona , co wskazywałoby, czy pole wyboru jest zaznaczone. Poniżej możesz znaleźć mały fragment kodu pokazujący, jak mógłby wyglądać rzeczywisty komponent.
// 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() { } }Zmienne wyjściowe
Podobnie jak zmienne wejściowe, zmienne wyjściowe mogą być również wykorzystywane do przekazywania pewnych informacji między komponentami, ale tym razem do komponentu nadrzędnego. Na przykład dla task-list.component możemy mieć zmienną wyjściową itemChecked . Poinformuje komponent nadrzędny, jeśli element został zaznaczony lub odznaczony. Zmienne wyjściowe muszą być emiterami zdarzeń. Poniżej znajduje się mały fragment kodu pokazujący, jak komponent może wyglądać ze zmienną wyjściową.
// 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 po dodaniu zmiennych wyjściowychUżycie komponentu potomnego i powiązanie zmiennych
Przyjrzyjmy się, jak używać tego komponentu w komponencie nadrzędnym i jak wykonywać różne typy powiązań zmiennych. W Angular istnieją dwa sposoby wiązania zmiennych wejściowych — wiązanie jednokierunkowe, co oznacza, że właściwość musi być owinięta w nawiasy kwadratowe [] i wiązanie dwukierunkowe, co oznacza, że właściwość musi być owinięta w nawiasy kwadratowe i okrągłe [()] . Spójrz na poniższy przykład i zobacz różne sposoby przekazywania danych między komponentami.
<h1>Upcoming Tasks</h1> <app-task-list [(tasks)]="upcomingTasks" [listType]="'upcoming'" (itemChecked)="onItemChecked($event)"></app-task-list>upcoming-tasks.component.htmlPrzeanalizujmy każdy parametr:
- Parametr
tasksjest przekazywany przy użyciu wiązania dwukierunkowego. Oznacza to, że w przypadku zmiany parametru task w komponencie potomnym, komponent nadrzędny odzwierciedli te zmiany wupcomingTaskszmiennejTasks. Aby zezwolić na dwukierunkowe wiązanie danych, musisz utworzyć parametr wyjściowy zgodny z szablonem „[inputParameterName]Change”, w tym przypadkutasksChange. - Parametr
listTypejest przekazywany przy użyciu powiązania jednokierunkowego. Oznacza to, że można to zmienić w komponencie potomnym, ale nie zostanie odzwierciedlone w komponencie nadrzędnym. Pamiętaj, że mogłem przypisać wartość'upcoming'do parametru w komponencie i przekazać ją zamiast tego — nie miałoby to żadnego znaczenia. - Wreszcie parametr
itemCheckedjest funkcją nasłuchującą i będzie wywoływany za każdym razem, gdy onItemCheck jest wykonywany na składnikutask-list.component. Jeśli element jest zaznaczony,$eventbędzie zawierał wartośćtrue, ale jeśli nie jest zaznaczony, będzie zawierał wartośćfalse.
Jak widać, ogólnie Angular zapewnia świetny sposób na przekazywanie i udostępnianie informacji między wieloma komponentami, więc nie powinieneś bać się ich używać. Tylko upewnij się, że używasz ich mądrze i nie nadużywasz.
Kiedy tworzyć oddzielny komponent kątowy?
Jak już wcześniej wspomnieliśmy, nie należy bać się używania komponentów Angulara, ale zdecydowanie należy z nich korzystać mądrze.
Kiedy więc należy tworzyć komponenty Angulara?
- Zawsze powinieneś utworzyć osobny komponent, jeśli komponent może być ponownie użyty w wielu miejscach, tak jak w przypadku naszego
task-list.component. Nazywamy je komponentami wielokrotnego użytku . - Powinieneś rozważyć utworzenie oddzielnego komponentu, jeśli komponent sprawi, że komponent nadrzędny będzie bardziej czytelny i pozwoli na dodanie dodatkowego pokrycia testowego. Możemy nazwać je komponentami organizacji kodu .
- Zawsze powinieneś utworzyć osobny komponent, jeśli masz część strony, która nie wymaga częstej aktualizacji i chcesz zwiększyć wydajność. Wiąże się to ze strategią wykrywania zmian. Możemy nazwać je komponentami optymalizacyjnymi .
Te trzy zasady pomagają mi określić, czy muszę utworzyć nowy komponent, i automatycznie dają mi jasną wizję roli dla tego komponentu. Idealnie, kiedy tworzysz komponent, powinieneś już wiedzieć, jaka będzie jego rola w aplikacji.

Ponieważ przyjrzeliśmy się już wykorzystaniu komponentów wielokrotnego użytku , przyjrzyjmy się wykorzystaniu komponentów organizacji kodu . Wyobraźmy sobie, że mamy formularz rejestracyjny, a na dole formularza mamy pole z Regulaminem . Zwykle język prawniczy jest bardzo duży i zajmuje dużo miejsca — w tym przypadku w szablonie HTML. Spójrzmy więc na ten przykład i zobaczmy, jak możemy go potencjalnie zmienić.
Na początku mamy jeden komponent — registration.component — który zawiera wszystko, łącznie z formularzem rejestracyjnym oraz samym regulaminem.
<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> Szablon wygląda teraz na mały, ale wyobraź sobie, że zamienilibyśmy „Tekst z bardzo długimi warunkami” na rzeczywisty fragment tekstu, który ma ponad 1000 słów — utrudniałoby to edytowanie pliku. Mamy na to szybkie rozwiązanie — moglibyśmy wymyślić nowy komponent terms-and-conditions.component , który zawierałby wszystko, co dotyczy warunków. Przyjrzyjmy się więc plikowi HTML dla terms-and-conditions.component .
<div class="terms-and-conditions-box"> Text with very long terms and conditions. </div> A teraz możemy dostosować komponent registration.component i użyć w nim terms-and-conditions.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()">Registrate</button> Gratulacje! Właśnie zmniejszyliśmy rozmiar registration.component o setki wierszy i ułatwiliśmy odczytywanie kodu. W powyższym przykładzie dokonaliśmy zmian w szablonie komponentu, ale tę samą zasadę można zastosować do logiki komponentu.
Na koniec, jeśli chodzi o komponenty optymalizacyjne , zdecydowanie sugeruję, abyś przeczytał ten post, ponieważ zawiera on wszystkie informacje wymagane do zrozumienia wykrywania zmian oraz do konkretnych przypadków, w których możesz je zastosować. Nie będziesz go często używać, ale mogą wystąpić pewne przypadki, a jeśli możesz pominąć regularne kontrole wielu komponentów, gdy nie jest to konieczne, jest to korzystne dla wydajności.
Biorąc to pod uwagę, nie powinniśmy zawsze tworzyć oddzielnych komponentów, więc spójrzmy, kiedy powinieneś unikać tworzenia oddzielnych komponentów.
Kiedy unikać tworzenia oddzielnego komponentu kątowego?
Istnieją trzy główne punkty, na podstawie których proponuję utworzyć osobny komponent Angulara, ale są też przypadki, w których unikałbym tworzenia osobnego komponentu.
Znowu przyjrzyjmy się punktom, które pozwolą łatwo zrozumieć, kiedy nie należy tworzyć osobnego komponentu.
- Nie należy tworzyć komponentów do manipulacji DOM. W tym celu powinieneś użyć dyrektyw atrybutów.
- Nie powinieneś tworzyć komponentów, jeśli sprawi to, że Twój kod stanie się bardziej chaotyczny. Jest to przeciwieństwo komponentów organizacji kodu .
Teraz przyjrzyjmy się bliżej i sprawdźmy oba przypadki na przykładach. Wyobraźmy sobie, że chcemy mieć przycisk, który rejestruje wiadomość po kliknięciu. Mogłoby to być pomyłką i można by utworzyć osobny komponent dla tej konkretnej funkcjonalności, który zawierałby określony przycisk i akcje. Sprawdźmy najpierw nieprawidłowe podejście:
// 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.tsW związku z tym temu komponentowi towarzyszy następujący widok HTML.
<button (click)="onButtonClick(true)">{{ name }}</button>log-button.component.htmlJak widać, powyższy przykład zadziała i możesz użyć powyższego komponentu we wszystkich widokach. Technicznie zrobi to, co chcesz. Ale właściwym rozwiązaniem byłoby tutaj użycie dyrektyw. Pozwoliłoby to nie tylko zmniejszyć ilość kodu, który trzeba napisać, ale także dodać możliwość zastosowania tej funkcjonalności do dowolnego elementu, a nie tylko przycisków.
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 , którą można przypisać do dowolnego elementu Teraz, gdy stworzyliśmy naszą dyrektywę, możemy po prostu użyć jej w całej naszej aplikacji i przypisać ją do dowolnego elementu, który chcemy. Na przykład użyjmy ponownie naszego 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 używana na przycisku formularzy rejestracyjnych Teraz możemy przyjrzeć się drugiemu przypadkowi, w którym nie powinniśmy tworzyć oddzielnych komponentów, a jest to przeciwieństwo komponentów optymalizacji kodu . Jeśli nowo utworzony komponent sprawia, że Twój kod jest bardziej skomplikowany i większy, nie ma potrzeby go tworzyć. Weźmy jako przykład nasz registration.component . Jednym z takich przypadków byłoby utworzenie oddzielnego komponentu dla etykiety i pola wejściowego z mnóstwem parametrów wejściowych. Przyjrzyjmy się tej złej praktyce.
// 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.componentI to może być widok tego komponentu.
<label for="{{ id }}">{{ label }}</label><br /> <input type="{{ type }}" name="{{ name }}" [(ngModel)]="model" /><br />form-input-with-label.component Oczywiście ilość kodu zostałaby zmniejszona w registration.component , ale czy dzięki temu ogólna logika kodu jest łatwiejsza do zrozumienia i bardziej czytelna? Myślę, że wyraźnie widzimy, że sprawia to, że kod jest niepotrzebnie bardziej złożony niż wcześniej.
Następne kroki: komponenty kątowe 102?
Podsumowując: nie bój się używać komponentów; po prostu upewnij się, że masz jasną wizję tego, co chcesz osiągnąć. Scenariusze, które wymieniłem powyżej, są najczęstszymi i uważam je za najważniejsze i najczęstsze; jednak Twój scenariusz może być wyjątkowy i od Ciebie zależy podjęcie świadomej decyzji. Mam nadzieję, że nauczyłeś się wystarczająco dużo, aby podjąć dobrą decyzję.
Jeśli chcesz dowiedzieć się więcej o strategiach wykrywania zmian Angular i strategii OnPush, polecam lekturę Angular Change Detection i strategii OnPush. Jest ściśle powiązany z komponentami i, jak już wspomniałem we wpisie, może znacząco poprawić wydajność aplikacji.
Ponieważ komponenty są tylko częścią dyrektyw, które zapewnia Angular, dobrze byłoby poznać również dyrektywy atrybutów i dyrektywy strukturalne . Zrozumienie wszystkich dyrektyw najprawdopodobniej ułatwi programiście napisanie lepszego kodu.
