(三)理解组件体系结构:重构Todo App

在本篇文章中,我们将创建:

  • 用于显示代办项目列表的TodoListComponent
  • 用于显示单个代办项目的TodoListItemComponent
  • 用于创建新的代办项目的TodoListHeaderComponent
  • 用于显示共有多少代办项目的TodoListFooterComponent

我们的应用程序架构会像这样:

本文将讨论标有红色边框的项目,以外的部分会在后续文章中说明。

通过这篇文章,我们将学习到:

  • 基本Angular组件体系结构
  • 如何利用属性绑定向组建传递数据
  • 如何利用组件事件监听器监听事件源
  • 为什么分解成可重用的组件是一个好的做法
  • smart和dumb组件的区别,为什么保持组件dumb(哑)是一个很好的实践。

更新并运行


安装最新版angular命令如下:

npm install -g @angular/cli@latest

如果需要移除过去版本重新安装:

npm uninstall -g @angular/cli angular-cli
npm cache clean --force
npm install -g @angular/cli@latest
*win系统中需要管理者权限执行

已完成 AppComponent


让我们打开已完成的 src/app/app.component.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<section class="todoapp">
<header class="header">
<h1>Todos</h1>
<input class="new-todo" placeholder="What needs to be done?" autofocus="" [(ngModel)]="newTodo.title" (keyup.enter)="addTodo()">
</header>
<section class="main" *ngIf="todos.length > 0">
<ul class="todo-list">
<li *ngFor="let todo of todos" [class.completed]="todo.complete">
<div class="view">
<input class="toggle" type="checkbox" (click)="toggleTodoComplete(todo)" [checked]="todo.complete">
<label>{{todo.title}}</label>
<button class="destroy" (click)="removeTodo(todo)"></button>
</div>
</li>
</ul>
</section>
<footer class="footer" *ngIf="todos.length > 0">
<span class="todo-count"><strong>{{todos.length}}</strong> {{todos.length == 1 ? 'item' : 'items'}} left</span>
</footer>
</section>

类 src/app/app.component.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import { Component } from '@angular/core';
import {Todo} from './todo';
import {TodoDataService} from './todo-data.service';

@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
providers: [TodoDataService]
})
export class AppComponent {

newTodo: Todo = new Todo();

constructor(private todoDataService: TodoDataService) {
}

addTodo() {
this.todoDataService.addTodo(this.newTodo);
this.newTodo = new Todo();
}

toggleTodoComplete(todo) {
this.todoDataService.toggleTodoComplete(todo);
}

removeTodo(todo) {
this.todoDataService.deleteTodoById(todo.id);
}

get todos() {
return this.todoDataService.getAllTodos();
}
}

虽然我们的AppComponent能够正常的执行,但是并不推荐所有的代码都写在一个组件里面。

向Todo应用程序添加更多的功能会使得AppComponent更加复杂,庞大,难于理解。

因此我们推荐按照功能分割成较小的组件。理想情况下,较小的组件是可配置的,这个样我们就不必在业务逻辑更改时重写它们的代码。

例如,在以后的文章中我们将会更改TodoDataService为REST API,而当我们重构TodoDataService的时候,不必更改任何小的组件。

我门来看AppComponent的template,其底层结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- header that lets us create new todo -->
<header></header>

<!-- list that displays todos -->
<ul class="todo-list">

<!-- list item that displays single todo -->
<li>Todo 1</li>

<!-- list item that displays single todo -->
<li>Todo 2</li>
</ul>

<!-- footer that displays statistics -->
<footer></footer>

我们将其变化为Angular组件结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- TodoListHeaderComponent that lets us create new todo -->
<app-todo-list-header></app-todo-list-header>

<!-- TodoListComponent that displays todos -->
<app-todo-list>

<!-- TodoListItemComponent that displays single todo -->
<app-todo-list-item></app-todo-list-item>

<!-- TodoListItemComponent that displays single todo -->
<app-todo-list-item></app-todo-list-item>
</app-todo-list>

<!-- TodoListFooterComponent that displays statistics -->
<app-todo-list-footer></app-todo-list-footer>

让我们看看如何利用Angular的组件驱动开发来实现这一点。

创建TodoListHeaderComponent


进入工程根目录,执行如下命令

$ ng g component todo-list-header

将会生成如下文件:

CREATE src/app/todo-list-header/todo-list-header.component.html (35 bytes)
CREATE src/app/todo-list-header/todo-list-header.component.spec.ts (686 bytes)
CREATE src/app/todo-list-header/todo-list-header.component.ts (307 bytes)
CREATE src/app/todo-list-header/todo-list-header.component.css (0 bytes)

在AppModoule将会自动加入TodoListHeaderComponent

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { TodoListHeaderComponent } from './todo-list-header/todo-list-header.component';

@NgModule({
declarations: [
AppComponent,
TodoListHeaderComponent
],
imports: [
BrowserModule,
FormsModule,
AppRoutingModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }

在AppModoule将会自动加入TodoListHeaderComponent组件声明是为了能够在所有view模板里面使用它。Angular CLI 方便的为我们添加了该组件所以我们无需手动添加它。

如果组件没有被声明就在view模板中使用,Angular将会抛出如下错误:

1
2
3
4
Error: Uncaught (in promise): Error: Template parse errors:
'app-todo-list-header' is not a known element:
1. If 'app-todo-list-header' is an Angular component, then verify that it is part of this module.
2. If 'app-todo-list-header' is a Web Component then add "CUSTOM_ELEMENTS_SCHEMA" to the '@NgModule.schemas' of this component to suppress this message.

现在我们生成了所有TodoListHeaderComponent所需文件,我们接下来移动src/app/app.component.html 的

到 src/app/todo-list-header/todo-list-header.component.html
1
2
3
4
<header class="header">
<h1>Todos</h1>
<input class="new-todo" placeholder="What needs to be done?" autofocus="" [(ngModel)]="newTodo.title" (keyup.enter)="addTodo()">
</header>

添加逻辑src/app/todo-list-header/todo-list-header.component.ts:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { Component, OnInit, Output, EventEmitter } from '@angular/core';
import { Todo } from '../todo';

@Component({
selector: 'app-todo-list-header',
templateUrl: './todo-list-header.component.html',
styleUrls: ['./todo-list-header.component.css']
})
export class TodoListHeaderComponent implements OnInit {
newTodo: Todo = new Todo();

@Output()
add: EventEmitter<Todo> = new EventEmitter();

constructor() { }

addTodo() {
this.add.emit(this.newTodo);
this.newTodo = new Todo();
}

ngOnInit() {
}
}

替换注入TodoDataService保存新的todo, 我们出发一个add事件,并且传递一个新的todo参数。

在Angular 模板中以如下方式捕捉事件:

这将在enter键按下的时候触发addTodo()方法。之所以能正常执行,是因为在angular framework 中 keyup.enter 被定义为一个有效的事件。

尽管如此,我们也经常 通过 EventEmitter 和 @Output() 为一个组件定义自己的事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { Component, Output, EventEmitter } from '@angular/core';
import { Todo } from '../todo';

@Component({
// ...
})
export class TodoListHeaderComponent {

// ...

@Output()
add: EventEmitter<Todo> = new EventEmitter();

addTodo() {
this.add.emit(this.newTodo);
this.newTodo = new Todo();
}

}

然后我们能够在模板中按照如下方式捕捉事件:

每当在 TodoListHeaderComponent 中调用 add.emit(value) 的时候,onAddTodo($event)事件将会被执行,参数$event将会被赋值为value。

这将解耦TodoListHeaderComponent 和 TodoDataService ,当创建一个新的to的时候,允许父组件处理想要做的事情。

修改 AppComponent 模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<section class="todoapp">

<!-- header is now replaced with app-todo-list-header -->
<app-todo-list-header (add)="onAddTodo($event)"></app-todo-list-header>

<section class="main" *ngIf="todos.length > 0">
<ul class="todo-list">
<li *ngFor="let todo of todos" [class.completed]="todo.complete">
<div class="view">
<input class="toggle" type="checkbox" (click)="toggleTodoComplete(todo)" [checked]="todo.complete">
<label>{{todo.title}}</label>
<button class="destroy" (click)="removeTodo(todo)"></button>
</div>
</li>
</ul>
</section>

<footer class="footer" *ngIf="todos.length > 0">
<span class="todo-count"><strong>{{todos.length}}</strong> {{todos.length == 1 ? 'item' : 'items'}} left</span>
</footer>

</section>

在 AppComponent 中添加 onAddTodo()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import {Component} from '@angular/core';
import {Todo} from './todo';
import {TodoDataService} from './todo-data.service';

@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
providers: [TodoDataService]
})
export class AppComponent {

// No longer needed, now handled by TodoListHeaderComponent
// newTodo: Todo = new Todo();

constructor(private todoDataService: TodoDataService) {
}

// No longer needed, now handled by TodoListHeaderComponent
// addTodo() {
// this.todoDataService.addTodo(this.newTodo);
// this.newTodo = new Todo();
// }

// Add new method to handle event emitted by TodoListHeaderComponent
onAddTodo(todo: Todo) {
this.todoDataService.addTodo(todo);
}

toggleTodoComplete(todo: Todo) {
this.todoDataService.toggleTodoComplete(todo);
}

removeTodo(todo: Todo) {
this.todoDataService.deleteTodoById(todo.id);
}

get todos() {
return this.todoDataService.getAllTodos();
}

}

创建TodoListComponent

首先生成 TodoListComponent:

ng g component todo-list

将会产生如下文件:

create src/app/todo-list/todo-list.component.css
create src/app/todo-list/todo-list.component.html
create src/app/todo-list/todo-list.component.spec.ts
create src/app/todo-list/todo-list.component.ts

并且在AppModule里面自动添加了如下声明

1
2
3
4
5
6
7
8
9
10
11
// ...
import { TodoListComponent } from './todo-list/todo-list.component';

@NgModule({
declarations: [
// ...
TodoListComponent
],
// ...
})
export class AppModule { }

打开 src/app/app.component.html:

1
2
3
4
5
6
7
8
9
10
11
<section class="main" *ngIf="todos.length > 0">
<ul class="todo-list">
<li *ngFor="let todo of todos" [class.completed]="todo.complete">
<div class="view">
<input class="toggle" type="checkbox" (click)="toggleTodoComplete(todo)" [checked]="todo.complete">
<label>{{todo.title}}</label>
<button class="destroy" (click)="removeTodo(todo)"></button>
</div>
</li>
</ul>
</section>

将内容移动到 src/app/todo-list/todo-list.component.html:

1
2
3
4
5
6
7
8
9
10
<section class="main" *ngIf="todos.length > 0">
<ul class="todo-list">
<li *ngFor="let todo of todos" [class.completed]="todo.complete">
<app-todo-list-item
[todo]="todo"
(toggleComplete)="onToggleTodoComplete($event)"
(remove)="onRemoveTodo($event)"></app-todo-list-item>
</li>
</ul>
</section>

*TodoListItemComponent 将在后续做成,通过 todo 属性 传递 todo 项目,并且处理在TodoListItemComponent中触发的事件。

打开 src/app/todo-list/todo-list.component.ts 添加如下处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { Todo } from '../todo';

@Component({
selector: 'app-todo-list',
templateUrl: './todo-list.component.html',
styleUrls: ['./todo-list.component.css']
})
export class TodoListComponent {

@Input()
todos: Todo[];

@Output()
remove: EventEmitter<Todo> = new EventEmitter();

@Output()
toggleComplete: EventEmitter<Todo> = new EventEmitter();

constructor() {
}

onToggleTodoComplete(todo: Todo) {
this.toggleComplete.emit(todo);
}

onRemoveTodo(todo: Todo) {
this.remove.emit(todo);
}

}

我们定义了 onToggleTodoComplete(todo) 和 onRemoveTodo(todo) 来处理view的事件
(toggleComplete)=”onToggleTodoComplete($event)” and (remove)=”onRemoveTodo($event)”

注意我们在view中使用 $event 作为参数名字,在方法定义中使用todo作为参数名字。
为了能够在Angular模板中访问到事件的负载(emitted value),我们必须使用 $event作为参数名。

事件在 TodoListItemComponent 产生,经由 TodoListComponent 最终在 AppComponent 中处理
修改 AppComponent

1
2
3
4
5
6
7
8
9
10
11
12
13
export class AppComponent {

// rename from toggleTodoComplete
onToggleTodoComplete(todo: Todo) {
this.todoDataService.toggleTodoComplete(todo);
}

// rename from removeTodo
onRemoveTodo(todo: Todo) {
this.todoDataService.deleteTodoById(todo.id);
}

}

创建TodoListItemComponent

利用Angular CLI 生成 TodoListItemComponent

ng g component todo-list-item

将生成如下文件:

create src/app/todo-list-item/todo-list-item.component.css
create src/app/todo-list-item/todo-list-item.component.html
create src/app/todo-list-item/todo-list-item.component.spec.ts
create src/app/todo-list-item/todo-list-item.component.ts

并且在AppModule里面自动添加了如下声明

1
2
3
4
5
6
7
8
9
10
11
// ...
import { TodoListItemComponent } from './todo-list-item/todo-list-item.component';

@NgModule({
declarations: [
// ...
TodoListItemComponent
],
// ...
})
export class AppModule {

移动原view中的\<li> 到 src/app/todo-list-item.component.html:

1
2
3
4
5
<div class="view">
<input class="toggle" type="checkbox" (click)="toggleTodoComplete(todo)" [checked]="todo.complete">
<label>{{todo.title}}</label>
<button class="destroy" (click)="removeTodo(todo)"></button>
</div>

src/app/todo-list-item/todo-list-item.component.ts:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import { Component, Input, Output, EventEmitter } from '@angular/core';
import { Todo } from '../todo';

@Component({
selector: 'app-todo-list-item',
templateUrl: './todo-list-item.component.html',
styleUrls: ['./todo-list-item.component.css']
})
export class TodoListItemComponent {

@Input() todo: Todo;

@Output()
remove: EventEmitter<Todo> = new EventEmitter();

@Output()
toggleComplete: EventEmitter<Todo> = new EventEmitter();

constructor() {
}

toggleTodoComplete(todo: Todo) {
this.toggleComplete.emit(todo);
}

removeTodo(todo: Todo) {
this.remove.emit(todo);
}

}

更新AppComponent模板:

1
2
3
4
5
6
7
8
9
10
11
12
13
<section class="todoapp">

<app-todo-list-header (add)="onAddTodo($event)"></app-todo-list-header>

<!-- section is now replaced with app-todo-list -->
<app-todo-list [todos]="todos" (toggleComplete)="onToggleTodoComplete($event)"
(remove)="onRemoveTodo($event)"></app-todo-list>

<footer class="footer" *ngIf="todos.length > 0">
<span class="todo-count"><strong>{{todos.length}}</strong> {{todos.length == 1 ? 'item' : 'items'}} left</span>
</footer>

</section>

创建TodoListFooterComponent

生成TodoListFooterComponent

ng g component todo-list-footer

移动 \<footer> 元素 src/app/app.component.html 到 src/app/todo-list-footer/todo-list-footer.component.html:

1
2
3
<footer class="footer" *ngIf="todos.length > 0">
<span class="todo-count"><strong>{{todos.length}}</strong> {{todos.length == 1 ? 'item' : 'items'}} left</span>
</footer>

修改 src/app/todo-list-footer/todo-list-footer.component.ts:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { Component, Input } from '@angular/core';
import { Todo } from '../todo';

@Component({
selector: 'app-todo-list-footer',
templateUrl: './todo-list-footer.component.html',
styleUrls: ['./todo-list-footer.component.css']
})
export class TodoListFooterComponent {

@Input()
todos: Todo[];

constructor() {
}

}

更新 AppComponent 模板:

1
2
3
4
5
6
<section class="todoapp">
<app-todo-list-header (add)="onAddTodo($event)"></app-todo-list-header>
<app-todo-list [todos]="todos" (toggleComplete)="onToggleTodoComplete($event)"
(remove)="onRemoveTodo($event)"></app-todo-list>
<app-todo-list-footer [todos]="todos"></app-todo-list-footer>
</section>

这样我们就成功的重构了AppComponent,由下列组件构成,
TodoListHeaderComponent
TodoListComponent -> TodoListItemComponent
TodoListFooterComponent

以后的章节,我们会继续重构 TodoService 为 REST API。

Author: jimmy367
Link: http://www.ohtudou.com/2019/04/19/learn-angular-part3/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.
支付宝打赏