首页 Angular Angular 学习之路 23 – Todo App (6)

Angular 学习之路 23 – Todo App (6)

0 0

前面我们介绍了有关 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()函数中添加输出语句来观察调用次数。

至此,我们已经按照需求,基本完成了待办事项应用。

发表评论

关于我

devbean

devbean

豆子,生于山东,定居南京。毕业于山东大学软件工程专业。软件工程师,主要关注于 Qt、Angular 等界面技术。

主题 Salodad 由 PenciDesign 提供 | 静态文件存储由又拍云存储提供 | 苏ICP备13027999号-2