前面我们介绍了有关 Angular 路由的基本内容。现在,我们就可以完成我们所需要的部分了:
路由
所有实现都应该有路由。如果框架支持路由,使用框架内建功能。否则,使用/assets文件夹中提供的 Flatiron Director 路由库。应该实现下面的路由:#/ (所有 - 默认);#/active 和 #/completed (也可以使用 #!/)。当路由发生改变时,待办列表应该在模型层次过滤,然后在过滤结果的链接上添加 selected 类。在过滤结果中修改项目,应该同时更新。例如,在过滤结果 Active 中,项目被选中,则其应该被隐藏。记得在重新加载时要激活当前过滤结果。
这应该是我们之前的需求列表中最后一部分了。
为了添加路由,首先,我们应该创建一个路由模块。
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { TodoListComponent } from './todo-list/todo-list.component';
const routes: Routes = [
{ path: '', component: TodoListComponent },
{ path: ':status', component: TodoListComponent },
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { } 这里,我们将空路由''映射到TodoListComponent,同时添加了一个:status路由,也映射到这一组件。之所以这样设计,是因为按照需求里面的要求,路由只是为了切换待办事项的列表显示。那么,这意味着路由应该是同一组件的不同显示,而不是不同的组件。
注意,不要忘记在AppModule中引入这个模块:
@NgModule({
declarations: [
AppComponent,
HeaderComponent,
FooterComponent,
TodoListComponent,
AutofocusDirective,
TodoFilterPipe
],
imports: [
BrowserModule,
AppRoutingModule,
FormsModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { } 现在我们已经添加了路有模块,最后,需要修改AppComponent组件的 HTML 模板,添加一个路由的占位符,用于显示路由对应的组件:
<!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Template • TodoMVC</title> <!-- <link rel="stylesheet" href="node_modules/todomvc-common/base.css">--> <!-- <link rel="stylesheet" href="node_modules/todomvc-app-css/index.css">--> <!-- CSS overrides - remove if you don't need it --> <!-- <link rel="stylesheet" href="css/app.css">--> </head> <body> <section class="todoapp"> <app-header></app-header> <!-- This section should be hidden by default and shown when there are todos --> <router-outlet></router-outlet> </section> <app-footer></app-footer> <!-- Scripts here. Don't remove ↓ --> <!--<script src="node_modules/todomvc-common/base.js"></script>--> <!--<script src="js/app.js"></script>--> </body> </html>
现在刷新页面,显示应该和之前的一致。
下面需要修改的是TodoListComponent组件,使其能够按照路由参数的不同,过滤显示不同的待办事项列表。
路由中的:status参数用于过滤不同状态的待办事项。我们可以在TodoListComponent组件中定义status属性如下:
... status: '' | 'active' | 'completed' = ''; ...
我们当然可以把status类型定义为string,但考虑到最小化类型,我们选择使用字面量常量的联合去定义(也可以选择使用枚举类型)。默认值为'',可以显示全部待办事项。
构造函数也需要做相应修改:
...
constructor(
public readonly todoService: TodoService,
private readonly route: ActivatedRoute
) {
this.route.paramMap
.subscribe(params => {
this.status = (params.get('status') as 'active' | 'completed') ?? '';
});
}
... 注意这里,params.get('status')的返回值类型是string | null,需要强制转换为'active' | 'completed'类型,因为这里的类型其实是收窄了的。
这里我们使用了简单粗暴地强制类型转换,但真实工作中,由于params.get('status')的返回值可以是'active' | 'completed'以外的字符串类型,所以这里应该选择使用更全面的字符串判断。
接下来,修改组件的模板文件,增加路由的支持:
...
<ul class="filters">
<li>
<a [ngClass]="{ selected: status === '' }" routerLink="/">All</a>
</li>
<li>
<a [ngClass]="{ selected: status === 'active' }" routerLink="/active">Active</a>
</li>
<li>
<a [ngClass]="{ selected: status === 'completed' }" routerLink="/completed">Completed</a>
</li>
</ul>
... ngClass指令可以根据status值的不同,选择为哪一个链接增加selected类。<a>标签的href属性被替换为routerLink,后者正是 Angular 路由中提供的用于前端路由跳转的指令。运行页面,注意观察点击不同链接之后,selected类被应用到不同的链接,并且浏览器地址栏的地址也有变化。
那么,该怎么由status值过滤待办事项列表的显示呢?我们回忆一下这个列表是如何渲染的:
...
<ul class="todo-list">
<!-- These are here just to show the structure of the list items -->
<!-- List items should get the class `editing` when editing and `completed` when marked as completed -->
<li *ngFor="let todo of todoService.todoList"
[ngClass]="{
completed: todo.completed,
editing: todo.editing
}">
<div class="view">
<input #toggleOne class="toggle" type="checkbox"
[(ngModel)]="todo.completed"
(change)="toggleComplete(toggleOne.checked, todo)">
<label (dblclick)="todo.editing=true">{{ todo.content }}</label>
<button class="destroy" (click)="deleteTodo(todo)"></button>
</div>
<input *ngIf="todo.editing"
#editingInput
class="edit" type="text" appAutofocus
[ngModel]="todo.content"
(blur)="stopEditing(todo, editingInput.value)"
(keyup.enter)="stopEditing(todo, editingInput.value)"
(keyup.escape)="cancelEditing(todo)">
</li>
</ul>
... 这里,我们使用ngFor循环渲染todoService.todoList的值;而这个值保存的就是全部的待办列表。那么,很简单的一个想法是,我们在TodoService中创建一个新的函数,按照status的值返回过滤之后的结果不就可以了?
...
getFilteredTodoList(status: '' | 'active' | 'completed'): Todo[] {
switch (status) {
case 'active':
return this.#todoList.filter(it => !it.completed);
case 'completed':
return this.#todoList.filter(it => it.completed);
default:
return this.#todoList;
}
}
... 这个函数很简单,接收一个status参数,然后返回根据这个参数过滤之后的待办事项列表。然后,我们修改之前的ngFor:
...
<li *ngFor="let todo of todoService.getFilteredTodoList(status)"
[ngClass]="{
completed: todo.completed,
editing: todo.editing
}">
... 然后运行一下看看结果。至此,一切正常。
那么,这就完成了吗?
并不。
我们的实现有一个很不恰当的地方。
我们可以在getFilteredTodoList()中添加一个输出语句:
...
getFilteredTodoList(status: '' | 'active' | 'completed'): Todo[] {
console.log('getFilteredTodoList');
switch (status) {
case 'active':
return this.#todoList.filter(it => !it.completed);
case 'completed':
return this.#todoList.filter(it => it.completed);
default:
return this.#todoList;
}
}
... 注意此时控制台的输出。可以看到,这个函数被调用了很多次。照理说,我们期望是,每次更改路由的时候,getFilteredTodoList()调用一次,用于重新计算过滤之后的待办事项列表的值。但现在我们发现,这个函数一下被调用多次。想象一下,如果这是一个非常耗时的函数,比如包含了复杂计算,或者请求网络,那么,资源消耗会有多大。
Angular 里面有一个最佳实践:不要在组件模板代码中调用函数。
该最佳实践目的就是为了防止这种情况的发生。至于为什么模板中的函数会被调用多次,这与 Angular 的变更检测机制有关,我们会在后面的更深入的章节去详细阐述这一问题。目前只需要记住,不要在组件模板代码中调用函数。
那么,我们的实现就是有问题的。但这个思路很明显没有问题,那么,如何规避这一问题呢?我们要引入 Angular 的另外一种机制:管道(pipe)。
管道通常的用法是规范化数据的显示。比如,服务器以Fri Apr 15 1988 00:00:00 GMT-0700 (Pacific Daylight Time)格式返回的数据,我们希望在页面显示为1988-04-15。除了要求服务器后端程序修改返回格式外,可以考虑使用 Angular 管道。管道一般不会对原始数据进行修改,仅仅是修改其显示格式。这看起来就是我们过滤之后的待办事项列表,因此,选择使用管道是合适的。
下面,我们使用命令创建一个管道:
ng g pipe todo-filter
修改创建的 todo-filter.pipe.ts 的代码如下:
import { Pipe, PipeTransform } from '@angular/core';
import { Todo } from './todo.model';
@Pipe({
name: 'todoFilter',
pure: true
})
export class TodoFilterPipe implements PipeTransform {
transform(todoList: Todo[], status: '' | 'active' | 'completed'): Todo[] {
switch (status) {
case 'active':
return todoList.filter(it => !it.completed);
case 'completed':
return todoList.filter(it => it.completed);
default:
return todoList;
}
}
} Angular 的管道使用@Pipe修饰器,同时要实现PipeTransform接口。该接口需要实现一个函数:transform()。函数的第一个参数是管道的输入,后面是不定参数,作为管道的参数。在这里,第一个参数类型是Todo[],即待办事项列表;第二个参数类型是'' | 'active' | 'completed',即待办事项的状态。其实现很简单,即根据状态返回过滤之后的列表。
注意,@Pipe修饰器有一个pure参数,用于指定该管道是不是纯管道。如果是纯管道,那么,Angular 只监听基本类型参数的变化或者引用类型引用的变化。管道默认都是纯管道。由于我们的status就是基本数据类型,所以,只需要使用纯管道即可。
接下来,修改TodoListComponent的模板:
...
<li *ngFor="let todo of todoService.todoList | todoFilter:status"
[ngClass]="{
completed: todo.completed,
editing: todo.editing
}">
... 当管道的输入值todoService.todoList或者参数status发生变化时,管道才会被重新计算,所以,使用管道就可以完美规避之前提到的,不要在模板中调用函数这一限制。类似的,我们可以通过在管道的transform()函数中添加输出语句来观察调用次数。
至此,我们已经按照需求,基本完成了待办事项应用。