Angular 組件 101 — 概述
已發表: 2022-03-11組件從一開始就在 Angular 中可用; 然而,許多人仍然發現自己錯誤地使用了組件。 在我的工作中,我看到人們根本不使用它們,創建組件而不是屬性指令等等。 這些是初級和高級 Angular 開發人員都傾向於提出的問題,包括我自己。 因此,這篇文章是為像我一樣學習 Angular 的人準備的,因為關於組件的官方文檔和非官方文檔都沒有用用例解釋如何以及何時使用組件。
在本文中,我將通過示例確定使用 Angular 組件的正確和錯誤方法。 這篇文章應該讓您清楚地了解:
- Angular組件的定義;
- 何時應該創建單獨的 Angular 組件; 和
- 當你不應該創建單獨的 Angular 組件時。
在我們進入 Angular 組件的正確用法之前,我想簡要介紹一下組件主題。 一般來說,每個 Angular 應用程序都默認至少有一個組件—— root component
。 從那裡開始,由我們決定如何設計我們的應用程序。 通常,您會為單獨的頁面創建一個組件,然後每個頁面將包含一個單獨的組件列表。 根據經驗,組件必須滿足以下條件:
- 必須定義一個類,其中包含數據和邏輯; 和
- 必須與為最終用戶顯示信息的 HTML 模板相關聯。
讓我們想像一個場景,我們有一個有兩個頁面的應用程序: Upcoming tasks和Completed tasks 。 在即將到來的任務頁面中,我們可以查看即將到來的任務,將它們標記為“完成”,最後添加新任務。 同樣,在“已完成任務”頁面中,我們可以查看已完成的任務並將其標記為“未完成”。 最後,我們有導航鏈接,它允許我們在頁面之間導航。 話雖如此,我們可以將以下頁面分為三個部分:根組件、頁面、可重用組件。
根據上面的截圖,我們可以清楚地看到應用程序的結構如下所示:
── 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
因此,讓我們將組件文件與上麵線框中的實際元素鏈接起來:
-
header-menu.component
和task-list.component
是可重用組件,在線框截圖中顯示為綠色邊框; -
upcoming-tasks.component
和completed-tasks.component
是頁面,在上面的線框截圖中顯示為黃色邊框; 和 - 最後,
app.component
是根組件,在線框截圖中以紅色邊框顯示。
因此,話雖如此,我們可以為每個組件指定單獨的邏輯和設計。 基於上面的線框圖,我們有兩個頁面重用了一個組件task-list.component
。 就會出現一個問題——我們如何指定我們應該在特定頁面上顯示什麼類型的數據? 幸運的是,我們不必擔心這一點,因為當您創建組件時,您還可以指定輸入和輸出變量。
輸入變量
輸入變量在組件內用於傳遞來自父組件的一些數據。 在上面的示例中,我們可以為task-list.component
提供兩個輸入參數tasks
和listType
。 因此, tasks
將是一個字符串列表,它將在單獨的行上顯示每個字符串,而listType
將是即將到來的或已完成的,這將指示複選框是否被選中。 下面,您可以找到實際組件外觀的一小段代碼。
// 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() { } }
輸出變量
與輸入變量類似,輸出變量也可用於在組件之間傳遞一些信息,但這次是傳遞給父組件。 例如,對於task-list.component
,我們可以有輸出變量itemChecked
。 如果項目已被選中或未選中,它將通知父組件。 輸出變量必須是事件發射器。 下面,您可以找到一個小代碼片段,說明該組件在輸出變量時的外觀。
// 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
內容子組件使用和變量綁定
下面我們來看看如何在父組件中使用這個組件,以及如何做不同類型的變量綁定。 在 Angular 中,有兩種綁定輸入變量的方法——單向綁定,這意味著屬性必須用方括號[]
包裹;雙向綁定,這意味著屬性必須被包裹在方括號和圓括號中[()]
。 查看下面的示例,了解在組件之間傳遞數據的不同方式。
<h1>Upcoming Tasks</h1> <app-task-list [(tasks)]="upcomingTasks" [listType]="'upcoming'" (itemChecked)="onItemChecked($event)"></app-task-list>
upcoming-tasks.component.html
內容讓我們來看看每個參數:
-
tasks
參數是使用雙向綁定傳遞的。 這意味著,如果在子組件中更改了 tasks 參數,父組件將在upcomingTasks
的Tasks 變量上反映這些更改。 要允許雙向數據綁定,您必須創建一個輸出參數,該參數遵循模板“[inputParameterName]Change”,在本例中tasksChange
。 -
listType
參數使用單向綁定傳遞。 這意味著它可以在子組件中更改,但不會反映在父組件中。 請記住,我可以將值'upcoming'
分配給組件內的一個參數,然後將其傳遞——這沒有任何區別。 - 最後,
itemChecked
參數是一個監聽函數,只要在task-list.component
上執行 onItemCheck 就會調用它。 如果一個項目被選中,$event
將保持值true
,但是,如果它未被選中,它將保持值false
。
如您所見,一般來說,Angular 提供了一種在多個組件之間傳遞和共享信息的好方法,因此您不必害怕使用它們。 只要確保明智地使用它們,不要過度使用它們。
何時創建單獨的 Angular 組件
如前所述,您不應該害怕使用 Angular 組件,但絕對應該明智地使用它們。
那麼什麼時候應該創建 Angular 組件呢?
- 如果組件可以在多個地方重用,您應該始終創建一個單獨的組件,例如我們的
task-list.component
。 我們稱它們為可重用組件。 - 如果組件將使父組件更具可讀性並允許它們添加額外的測試覆蓋率,則應考慮創建單獨的組件。 我們可以稱它們為代碼組織組件。
- 如果您有一個頁面的一部分不需要經常更新並且想要提高性能,您應該始終創建一個單獨的組件。 這與變化檢測策略有關。 我們可以稱它們為優化組件。
這三個規則幫助我確定是否需要創建一個新組件,並且它們自動讓我對組件的角色有一個清晰的認識。 理想情況下,當您創建一個組件時,您應該已經知道它在應用程序中的作用。
既然我們已經了解了可重用組件的用法,那麼讓我們來看看代碼組織組件的用法。 假設我們有一個註冊表單,在表格的底部,我們有一個帶有條款和條件的框。 通常,法律術語往往非常大,佔用大量空間——在這種情況下,在 HTML 模板中。 所以,讓我們看一下這個例子,然後看看我們如何改變它。
一開始,我們有一個組件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 /> <div class="terms-and-conditions-box"> Text with very long terms and conditions. </div> <button (click)="onRegistrate()">Registrate</button>
該模板現在看起來很小,但想像一下,如果我們將“具有非常長的條款和條件的文本”替換為實際包含 1000 多個單詞的文本,這會使文件的編輯變得困難和不舒服。 我們有一個快速的解決方案——我們可以發明一個新的組件terms-and-conditions.component
,它將包含與條款和條件相關的所有內容。 因此,讓我們看一下terms-and-conditions.component
的 HTML 文件。

<div class="terms-and-conditions-box"> Text with very long terms and conditions. </div>
現在我們可以調整registration.component
並使用其中的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>
恭喜! 我們剛剛將registration.component
的大小減少了數百行,並使代碼更易於閱讀。 在上面的示例中,我們對組件的模板進行了更改,但同樣的原則也可以應用於組件的邏輯。
最後,對於優化組件,我強烈建議您閱讀這篇文章,因為它將為您提供理解更改檢測所需的所有信息,以及您可以將其應用於哪些特定情況。 您不會經常使用它,但在某些情況下,如果您可以在不需要時跳過對多個組件的定期檢查,這對性能來說是雙贏的。
話雖如此,我們不應該總是創建單獨的組件,所以讓我們看看何時應該避免創建單獨的組件。
何時避免創建單獨的 Angular 組件
我建議基於三個要點創建一個單獨的 Angular 組件,但在某些情況下我也會避免創建一個單獨的組件。
同樣,讓我們看一下要點,這將使您可以輕鬆理解何時不應創建單獨的組件。
- 您不應該為 DOM 操作創建組件。 對於那些,您應該使用屬性指令。
- 如果它會使您的代碼更加混亂,則不應創建組件。 這與代碼組織組件相反。
現在,讓我們仔細看看並在示例中檢查這兩種情況。 讓我們想像一下,我們想要一個按鈕來記錄被點擊的消息。 這可能是錯誤的,並且可以為此特定功能創建一個單獨的組件,該組件將包含特定的按鈕和操作。 讓我們首先檢查不正確的方法:
// 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
的邏輯因此,該組件伴隨著以下 html 視圖。
<button (click)="onButtonClick(true)">{{ name }}</button>
log-button.component.html
的模板如您所見,上面的示例可以工作,並且您可以在整個視圖中使用上面的組件。 從技術上講,它會做你想做的事。 但這裡正確的解決方案是使用指令。 這不僅可以讓您減少必須編寫的代碼量,還可以增加將此功能應用於您想要的任何元素的可能性,而不僅僅是按鈕。
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
指令,可以分配給任何元素現在,當我們創建了指令後,我們可以簡單地在我們的應用程序中使用它並將它分配給我們想要的任何元素。 例如,讓我們重用我們的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
指令現在,我們可以看看第二種情況,我們不應該創建單獨的組件,這與代碼優化組件相反。 如果新創建的組件使你的代碼更複雜更大,則無需創建它。 讓我們以我們的registration.component
為例。 一種這樣的情況是使用大量輸入參數為標籤和輸入字段創建一個單獨的組件。 讓我們來看看這個不好的做法。
// 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
這可能是該組件的視圖。
<label for="{{ id }}">{{ label }}</label><br /> <input type="{{ type }}" name="{{ name }}" [(ngModel)]="model" /><br />
form-input-with-label.component
當然, registration.component
中的代碼量會減少,但它是否使代碼的整體邏輯更容易理解和可讀? 我認為我們可以清楚地看到它使代碼比以前更加複雜。
下一步:Angular 組件 102?
總結一下:不要害怕使用組件; 只要確保你對你想要實現的目標有一個清晰的願景。 我上面列出的場景是最常見的,我認為它們是最重要和最常見的; 但是,您的情況可能是獨一無二的,您需要做出明智的決定。 我希望你已經學到了足夠的知識來做出一個好的決定。
如果你想了解更多關於 Angular 的變更檢測策略和 OnPush 策略,我推薦閱讀 Angular Change Detection 和 OnPush 策略。 它與組件密切相關,正如我在帖子中已經提到的,它可以顯著提高應用程序的性能。
由於組件只是 Angular 提供的指令的一部分,因此了解屬性指令和結構指令會很棒。 了解所有指令很可能使程序員更容易編寫更好的代碼。