前面我们介绍了有关 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()
函数中添加输出语句来观察调用次数。
至此,我们已经按照需求,基本完成了待办事项应用。