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 提供的指令的一部分,因此了解属性指令和结构指令会很棒。 了解所有指令很可能使程序员更容易编写更好的代码。