基于 HTTP 实现用户信息的 CURD

本章节,我们将基于 HTTP 实现用户信息的 CURD,即查询、修改、创建、删除用户。

修改用户

在用户详情视图中编辑用户的名字。 随着输入,用户的名字也跟着在页面顶部的标题区更新了。 但是当你点击“后退”按钮时,这些修改都丢失了。

如果你希望保留这些修改,就要把它们写回到服务器。

在用户详情模板(user-detail.component.html)的底部添加一个保存按钮,它绑定了一个 click 事件,事件绑定会调用组件中一个名叫 save() 的新方法:

  1. <button (click)="save()">save</button>

在 user-detail.component.t 中添加如下的 save() 方法,它使用用户服务中的 updateUser() 方法来保存对用户名字的修改,然后导航回前一个视图。

  1. save(): void {
  2. this.userService.updateUser(this.user)
  3. .subscribe(() => this.goBack());
  4. }

添加 UserService.updateUser()

updateUser() 的总体结构和 getusers() 很相似,但它会使用 http.put() 来把修改后的用户保存到服务器上。

  1. updateUser (user: User): Observable<any> {
  2. return this.http.put(this.usersURL, user, this.httpOptions).pipe(
  3. tap(_ => this.log(`updated user id=${user.id}`)),
  4. catchError(this.handleError<any>('updateUser'))
  5. );
  6. }

HttpClient.put() 方法接受三个参数

  • URL 地址
  • 要修改的数据(这里就是修改后的用户)
  • 选项

URL 没变。用户 Web API 通过用户对象的 id 就可以知道要修改哪个用户。

用户 Web API 期待在保存时的请求中有一个特殊的头。 这个头是在 UserService 的 httpOptions 常量中定义的。

  1. private httpOptions:Object = {
  2. headers: new HttpHeaders({ 'Content-Type': 'application/json' })
  3. };

刷新浏览器,修改用户名,保存这些修改,然后点击“后退”按钮。 现在,改名后的用户已经显示在列表中了。

添加新用户

要添加用户,本应用中只需要用户的名字。你可以使用一个和添加按钮成对的 input 元素。

把下列代码插入到 UsersComponent 模板(src/app/users/users.component.html)中标题的紧后面:

  1. <div>
  2. <label>User Name:
  3. <input #userName />
  4. </label>
  5. <button (click)="add(userName.value); userName.value=''">
  6. add
  7. </button>
  8. </div>

当点击事件触发时,调用组件的点击处理器,然后清空这个输入框,以便用来输入另一个名字。修改 src/app/users/users.component.ts 添加 add 方法:

  1. add(name: string): void {
  2. name = name.trim();
  3. if (!name) { return; }
  4. this.userService.addUser({ name } as User)
  5. .subscribe(user => {
  6. this.users.push(user);
  7. });
  8. }

当指定的名字非空时,这个处理器会用这个名字创建一个类似于 User 的对象(只缺少 id 属性),并把它传给服务的 addUser() 方法。

当 addUser 保存成功时,subscribe 的回调函数会收到这个新用户,并把它追加到 users 列表中以供显示。

此时,我们还需要创建 UserService.addUser() 方法。

添加 UserService.addUser()

往 UserService 类中添加 addUser() 方法。修改 src/app/User.service.ts 添加 addUser 方法:

  1. addUser (user: User): Observable<User> {
  2. return this.http.post<User>(this.usersURL, user, this.httpOptions).pipe(
  3. tap((user: User) => this.log(`added user id=${user.id}`)),
  4. catchError(this.handleError<User>('addUser'))
  5. );
  6. }

UserService.addUser() 和 updateUser 有两点不同。

它调用 HttpClient.post() 而不是 put()。

它期待服务器为这个新的用户生成一个 id,然后把它通过 Observable 返回给调用者。

刷新浏览器,并添加一些用户。

基于 HTTP 实现用户信息的 CURD - 图1

基于 HTTP 实现用户信息的 CURD - 图2

删除用户

用户列表中的每个用户都有一个删除按钮。点击删除按钮,就能删除用户。

把下列按钮(button)元素添加到 UsersComponent 的模板中,就在每个 <li> 元素中的用户名字后方。

  1. <button class="delete" title="delete user"
  2. (click)="delete(user)">x</button>

这样,完整的用户列表(src/app/users/users.component.html)的 HTML 应该是这样的:

  1. <h2>我的用户</h2>
  2. <ul class="users">
  3. <li *ngFor="let user of users">
  4. <a routerLink="/detail/{{user.id}}">
  5. <span class="badge">{{user.id}}</span> {{user.name}}
  6. </a>
  7. <button class="delete" title="delete user"
  8. (click)="delete(user)">x</button>
  9. </li>
  10. </ul>
  11. <div>
  12. <label>User Name:
  13. <input #userName />
  14. </label>
  15. <button (click)="add(userName.value); userName.value=''">
  16. add
  17. </button>
  18. </div>

要把删除按钮定位在每个用户条目的最右边,就要往 users.component.css 中添加一些 CSS:

  1. button {
  2. background-color: #eee;
  3. border: none;
  4. padding: 5px 10px;
  5. border-radius: 4px;
  6. cursor: pointer;
  7. cursor: hand;
  8. font-family: Arial;
  9. }
  10. button:hover {
  11. background-color: #cfd8dc;
  12. }
  13. button.delete {
  14. position: relative;
  15. left: 194px;
  16. top: -32px;
  17. background-color: gray !important;
  18. color: white;
  19. }

在 src/app/users/users.component.ts 中添加 delete() 方法:

  1. delete(user: User): void {
  2. this.users = this.users.filter(h => h !== user);
  3. this.userService.deleteUser(user).subscribe();
  4. }

虽然这个组件把删除用户的逻辑委托给了 userService,但仍保留了更新它自己的用户列表的职责。组件的 delete() 方法会在 userService 对服务器的操作成功之前,会先从列表中移除要删除的用户。

组件与 userService.delete() 返回的 Observable 还完全没有关联,所以必须订阅它。

添加 UserService.deleteUser() 方法

把 deleteuser() 方法添加到 UserService (src/app/user.service.ts)中,代码如下。

  1. deleteUser (user: User | number): Observable<User> {
  2. const id = typeof user === 'number' ? user : user.id;
  3. const url = `${this.usersURL}/${id}`;
  4. return this.http.delete<User>(url, this.httpOptions).pipe(
  5. tap(_ => this.log(`deleted user id=${id}`)),
  6. catchError(this.handleError<User>('deleteUser'))
  7. );
  8. }

其中:

  • 它调用了 HttpClient.delete。
  • URL 就是用户的资源 URL 加上要删除的用户的 id。
  • 你不用像 put 和 post 中那样需要发送数据。
  • 你仍要发送 httpOptions。

刷新浏览器,并试一下这个新的删除功能。

效果如下:

基于 HTTP 实现用户信息的 CURD - 图3

根据名字搜索用户

你将往仪表盘中加入用户搜索特性。 当用户在搜索框中输入名字时,你会不断发送根据名字过滤用户的 HTTP 请求。 你的目标是仅仅发出尽可能少的必要请求。

添加 UserService.searchUsers

先把 searchUsers 方法添加到 UserService (src/app/user.service.ts)中。

  1. searchUsers(term: string): Observable<User[]> {
  2. if (!term.trim()) {
  3. return of([]);
  4. }
  5. return this.http.get<User[]>(`${this.usersURL}/?name=${term}`).pipe(
  6. tap(_ => this.log(`found Users matching "${term}"`)),
  7. catchError(this.handleError<User[]>('searchUsers', []))
  8. );
  9. }

如果没有搜索词,该方法立即返回一个空数组。 剩下的部分和 getUsers() 很像。 唯一的不同点是 URL,它包含了一个由搜索词组成的查询字符串。

为仪表盘添加搜索功能

打开 DashboardComponent 的模板并且把用于搜索用户的元素 <app-user-search> 添加到 DashboardComponent 模板(src/app/dashboard/dashboard.component.html)的底部。

  1. <h3>Top Users</h3>
  2. <div class="grid grid-pad">
  3. <a *ngFor="let user of users" class="col-1-4"
  4. routerLink="/detail/{{user.id}}">
  5. <div class="module user">
  6. <h4>{{user.name}}</h4>
  7. </div>
  8. </a>
  9. </div>
  10. <app-user-search></app-user-search>

但目前,UserSearchComponent 还不存在,因此,Angular 找不到哪个组件的选择器能匹配上 <app-user-search>

创建 UserSearchComponent

使用 CLI 创建一个 UserSearchComponent。

  1. ng generate component user-search

CLI 生成了 UserSearchComponent 相关的几个文件,并把该组件添加到了 AppModule 的声明中。以下是控制台输出:

  1. ng generate component user-search
  2. CREATE src/app/user-search/user-search.component.html (30 bytes)
  3. CREATE src/app/user-search/user-search.component.spec.ts (657 bytes)
  4. CREATE src/app/user-search/user-search.component.ts (288 bytes)
  5. CREATE src/app/user-search/user-search.component.css (0 bytes)
  6. UPDATE src/app/app.module.ts (1283 bytes)

把生成的 UserSearchComponent 的模板(src/app/user-search/user-search.component.html ),改成一个输入框和一个匹配到的搜索结果的列表。代码如下:

  1. <div id="search-component">
  2. <h4>User Search</h4>
  3. <input #searchBox id="search-box" (keyup)="search(searchBox.value)" />
  4. <ul class="search-result">
  5. <li *ngFor="let user of users$ | async" >
  6. <a routerLink="/detail/{{user.id}}">
  7. {{user.name}}
  8. </a>
  9. </li>
  10. </ul>
  11. </div>

修改 User-search.component.css ,添加相关的样式:

  1. .search-result li {
  2. border-bottom: 1px solid gray;
  3. border-left: 1px solid gray;
  4. border-right: 1px solid gray;
  5. width:195px;
  6. height: 16px;
  7. padding: 5px;
  8. background-color: white;
  9. cursor: pointer;
  10. list-style-type: none;
  11. }
  12. .search-result li:hover {
  13. background-color: #607D8B;
  14. }
  15. .search-result li a {
  16. color: #888;
  17. display: block;
  18. text-decoration: none;
  19. }
  20. .search-result li a:hover {
  21. color: white;
  22. }
  23. .search-result li a:active {
  24. color: white;
  25. }
  26. #search-box {
  27. width: 200px;
  28. height: 20px;
  29. }
  30. ul.search-result {
  31. margin-top: 0;
  32. padding-left: 0;
  33. }

当用户在搜索框中输入时,一个 keyup 事件绑定会调用该组件的 search() 方法,并传入新的搜索框的值。

AsyncPipe

观察:

  1. <li *ngFor="let user of users$ | async" >

如你所愿,*ngFor 遍历渲染出了这些用户。

仔细看,你会发现*ngFor 是在一个名叫 users$ 的列表上迭代,而不是 users。

$是一个命名惯例,用来表明 users$ 是一个 Observable,而不是数组。

*ngFor 不能直接使用 Observable。 不过,它后面还有一个管道字符(|),后面紧跟着一个 async,它表示 Angular 的 AsyncPipe。

AsyncPipe 会自动订阅到 Observable,这样你就不用再在组件类中订阅了。

修正 UserSearchComponent 类

修改所生成的 UserSearchComponent 类及其元数据(src/app/user-search/user-search.component.ts ),代码如下:

  1. import { Component, OnInit } from '@angular/core';
  2. import { Observable, Subject } from 'rxjs';
  3. import {
  4. debounceTime, distinctUntilChanged, switchMap
  5. } from 'rxjs/operators';
  6. import { User } from '../user';
  7. import { UserService } from '../user.service';
  8. @Component({
  9. selector: 'app-user-search',
  10. templateUrl: './user-search.component.html',
  11. styleUrls: ['./user-search.component.css']
  12. })
  13. export class UserSearchComponent implements OnInit {
  14. users$: Observable<User[]>;
  15. private searchTerms = new Subject<string>();
  16. constructor(private userService: UserService) {}
  17. search(term: string): void {
  18. this.searchTerms.next(term);
  19. }
  20. ngOnInit(): void {
  21. this.users$ = this.searchTerms.pipe(
  22. // 等待 300ms
  23. debounceTime(300),
  24. // 忽略与前一次搜索内容相同的数据
  25. distinctUntilChanged(),
  26. // 当搜索的内容变更时,切换到新的搜索Observable
  27. switchMap((term: string) => this.userService.searchUsers(term)),
  28. );
  29. }
  30. }

其中,

  • users$ 声明为一个 Observable;
  • searchTerms 属性声明成了 RxJS 的 Subject 类型。 Subject 既是可观察对象的数据源,本身也是 Observable。 你可以像订阅任何 Observable 一样订阅 Subject。 你还可以通过调用它的 next(value) 方法往 Observable 中推送一些值,就像 search() 方法中一样。
  • search() 是通过对文本框的 keystroke 事件的事件绑定来调用的。 每当用户在文本框中输入时,这个事件绑定就会使用文本框的值(搜索词)调用 search() 函数。 searchTerms 变成了一个能发出搜索词的稳定的流。

串联 RxJS 操作符

如果每当用户击键后就直接调用 searchUseres() 将导致创建海量的 HTTP 请求,浪费服务器资源并消耗大量网络流量。

应该怎么做呢?ngOnInit() 往 searchTerms 这个可观察对象的处理管道中加入了一系列 RxJS 操作符,用以缩减对 searchUsers() 的调用次数,并最终返回一个可及时给出用户搜索结果的可观察对象(每次都是 User[] )。详细观察如下代码:

  1. this.users$ = this.searchTerms.pipe(
  2. // 等待 300ms
  3. debounceTime(300),
  4. // 忽略与前一次搜索内容相同的数据
  5. distinctUntilChanged(),
  6. // 当搜索的内容变更时,切换到新的搜索Observable
  7. switchMap((term: string) => this.userService.searchUsers(term)),
  8. );

在这段代码中,

  • 在传出最终字符串之前,debounceTime(300) 将会等待,直到新增字符串的事件暂停了 300 毫秒。 这样你实际发起请求的间隔永远不会小于 300ms,减少了请求次数。
  • distinctUntilChanged() 会确保只在过滤条件变化时才发送请求。
  • switchMap() 会为每个从 debounce 和 distinctUntilChanged 中通过的搜索词调用搜索服务。 它会取消并丢弃以前的搜索可观察对象,只保留最近的。
  • 借助 switchMap 操作符, 每个有效的击键事件都会触发一次 HttpClient.get() 方法调用。 即使在每个请求之间都有至少 300ms 的间隔,仍然可能会同时存在多个尚未返回的 HTTP 请求。
  • switchMap() 会记住原始的请求顺序,只会返回最近一次 HTTP 方法调用的结果。 以前的那些请求都会被取消和舍弃。
  • 注意,取消前一个 searchUseres() 可观察对象并不会中止尚未完成的 HTTP 请求。 那些不想要的结果只会在它们抵达应用代码之前被舍弃。

运行

再次运行本应用。在这个仪表盘中,在搜索框中输入一些文字,比如本例中的“l”。如果你输入的字符匹配上了任何现有用户的名字,你将会看到如下效果:

基于 HTTP 实现用户信息的 CURD - 图4