使用 HTTP

在本章节中,我们将会使用 Angular 的 HttpClient,来实现 HTTP 服务的调用。

启用 HTTP 服务

HttpClient 是 Angular 通过 HTTP 与远程服务器通讯的机制。

要让 HttpClient 在应用中随处可用,请打开根模块 AppModule,从 @angular/common/http 中导入 HttpClientModule,并把它加入 @NgModule.imports 数组中。

完整代码如下:

  1. import { BrowserModule } from '@angular/platform-browser';
  2. import { FormsModule } from '@angular/forms';
  3. import { HttpClientModule } from '@angular/common/http';
  4. import { NgModule } from '@angular/core';
  5. import { AppComponent } from './app.component';
  6. import { UsersComponent } from './users/users.component';
  7. import { UserDetailComponent } from './user-detail/user-detail.component';
  8. import { MessagesComponent } from './messages/messages.component';
  9. import { AppRoutingModule } from './/app-routing.module';
  10. import { DashboardComponent } from './dashboard/dashboard.component';
  11. import { HttpClientInMemoryWebApiModule } from 'angular-in-memory-web-api';
  12. import { InMemoryDataService } from './in-memory-data.service';
  13. @NgModule({
  14. declarations: [
  15. AppComponent,
  16. UsersComponent,
  17. UserDetailComponent,
  18. MessagesComponent,
  19. DashboardComponent
  20. ],
  21. imports: [
  22. BrowserModule,
  23. FormsModule,
  24. AppRoutingModule,
  25. HttpClientModule,
  26. HttpClientInMemoryWebApiModule.forRoot(
  27. InMemoryDataService, { dataEncapsulation: false }
  28. )
  29. ],
  30. providers: [],
  31. bootstrap: [AppComponent]
  32. })
  33. export class AppModule { }

模拟数据服务器

我们将使用内存 Web API(In-memory Web API) 来模拟出的远程数据服务器通讯。这个内存 Web API给测试带来了极大的便利,因为我们不用真实的去实现一个 RESTful 服务去提供给 HttpClient 来调用。

这个内存 Web API 模块与 Angular 中的 HTTP 模块无并联系。要使用内存 Web API 模块,需要执行npm install angular-in-memory-web-api --save来进行独立安装。安装过程如下:

  1. npm install angular-in-memory-web-api --save
  2. > node-sass@4.9.2 install D:\workspaceGithub\angular-tutorial\samples\user-management\node_modules\node-sass
  3. > node scripts/install.js
  4. Downloading binary from https://github.com/sass/node-sass/releases/download/v4.9.2/win32-x64-64_binding.node
  5. Cannot download "https://github.com/sass/node-sass/releases/download/v4.9.2/win32-x64-64_binding.node":
  6. connect ETIMEDOUT 54.231.114.146:443
  7. Timed out whilst downloading the prebuilt binary
  8. Hint: If github.com is not accessible in your location
  9. try setting a proxy via HTTP_PROXY, e.g.
  10. export HTTP_PROXY=http://example.com:1234
  11. or configure npm proxy via
  12. npm config set proxy http://example.com:8080
  13. > node-sass@4.9.2 postinstall D:\workspaceGithub\angular-tutorial\samples\user-management\node_modules\node-sass
  14. > node scripts/build.js
  15. Building: D:\Program Files\nodejs\node.exe D:\workspaceGithub\angular-tutorial\samples\user-management\node_modules\node-gyp\bin\node-gyp.js rebuild --verbose --libsass_ext= --libsass_cflags= --libsass_ldflags= --libsass_library=
  16. gyp info it worked if it ends with ok
  17. gyp verb cli [ 'D:\\Program Files\\nodejs\\node.exe',
  18. ...
  19. + angular-in-memory-web-api@0.6.1
  20. added 1 package and removed 61 packages in 132.041s

限于篇幅,这里只展示了主要的过程。

注意:如果安装过程中有异常,请先执行npm install -g node-gyp,且更新 RxJS 到 6.3.2版本。该问题详见https://github.com/ReactiveX/rxjs/issues/4090

注意:做了版本号的修改,需要执行下ng update来更新依赖。比如:

  1. ng update
  2. We analyzed your package.json, there are some packages to update:
  3. Name Version Command to update
  4. --------------------------------------------------------------------------------
  5. @angular/core 6.1.0 -> 6.1.6 ng update @angular/core
  6. rxjs 6.2.2 -> 6.3.2 ng update rxjs
  7. There might be additional packages that are outdated.
  8. Or run ng update --all to try to update all at the same time.

注意:有时开发者自己去调整版本号是一件复杂的事情,因为不同版本之间的库存在兼容性问题。如果想把所有的依赖都更新到最新的兼容版本,请执行ng update --all --next --force

安装内存 Web API 模块完成之后,就可以在 package.json 中看到该模块的信息:

  1. "dependencies": {
  2. "@angular/animations": "^6.1.0",
  3. "@angular/common": "^6.1.0",
  4. "@angular/compiler": "^6.1.0",
  5. "@angular/core": "^6.1.0",
  6. "@angular/forms": "^6.1.0",
  7. "@angular/http": "^6.1.0",
  8. "@angular/platform-browser": "^6.1.0",
  9. "@angular/platform-browser-dynamic": "^6.1.0",
  10. "@angular/router": "^6.1.0",
  11. "angular-in-memory-web-api": "^0.6.1",
  12. "core-js": "^2.5.4",
  13. "rxjs": "^6.3.2",
  14. "zone.js": "~0.8.26"
  15. },

而后在 app.module.ts 中导入 HttpClientInMemoryWebApiModule 和 InMemoryDataService 类:

  1. import { HttpClientInMemoryWebApiModule } from 'angular-in-memory-web-api';
  2. import { InMemoryDataService } from './in-memory-data.service';

把 HttpClientInMemoryWebApiModule 添加到 @NgModule.imports 数组中(放在 HttpClient 之后), 然后使用 InMemoryDataService 来配置它:

  1. HttpClientModule,
  2. HttpClientInMemoryWebApiModule.forRoot(
  3. InMemoryDataService, { dataEncapsulation: false }
  4. )

forRoot() 配置方法接受一个 InMemoryDataService 类(初期的内存数据库)作为参数。 在应用中创建该 InMemoryDataService 类(src/app/in-memory-data.service.ts),内容如下:

  1. import { InMemoryDbService } from 'angular-in-memory-web-api';
  2. export class InMemoryDataService implements InMemoryDbService {
  3. createDb() {
  4. const users = [
  5. { id: 11, name: 'Way Lau' },
  6. { id: 12, name: 'Narco' },
  7. { id: 13, name: 'Bombasto' },
  8. { id: 14, name: 'Celeritas' },
  9. { id: 15, name: 'Magneta' },
  10. { id: 16, name: 'RubberMan' },
  11. { id: 17, name: 'Dynama' },
  12. { id: 18, name: 'Dr IQ' },
  13. { id: 19, name: 'Magma' },
  14. { id: 20, name: 'Tornado' }
  15. ];
  16. return {users};
  17. }
  18. }

InMemoryDataService 替代了 mock-Useres.ts。

等你真实的服务器就绪时,就可以删除这个内存 Web API,该应用的请求就会直接发给真实的服务器。

使用 HTTP

修改 UserService (src/app/user.service.ts)

  1. import { HttpClient, HttpHeaders } from '@angular/common/http';

把 HttpClient 注入到构造函数中一个名叫 http 的私有属性中。

  1. constructor(
  2. private http: HttpClient,
  3. private messageService: MessageService) { }

保留对 MessageService 的注入。并在 UserService 中添加一个私有的 log 方法中。

  1. private log(message: string) {
  2. this.messageService.add(`UserService: ${message}`);
  3. }

把服务器上用户数据资源的访问地址定义为 usersURL。

  1. private usersURL = 'api/users'; // URL to web api

通过 HttpClient 获取用户

当前的 UserService.getUsers() 使用 RxJS 的 of() 函数来把模拟用户数据返回为 Observable<User[]> 格式:

  1. getUsers(): Observable<User[]> {
  2. this.messageService.add('UserService: 已经获取到用户列表!');
  3. return of(USERS);
  4. }

把该方法转换成使用 HttpClient 的 get 方法,打印消息的方法也做了重构,使用了 log 方法:

  1. getUsers(): Observable<User[]> {
  2. this.log('已经获取到用户列表!');
  3. return this.http.get<User[]>(this.usersURL);
  4. }

刷新浏览器后,用户数据就会从模拟服务器被成功读取。

你用 http.get 替换了 of,没有做其它修改,但是应用仍然在正常工作,这是因为这两个函数都返回了 Observable

Http 方法返回单个值

所有的 HttpClient 方法都会返回某个值的 RxJS Observable。

通常,Observable 可以在一段时间内返回多个值。 但来自 HttpClient 的 Observable 总是发出一个值,然后结束,再也不会发出其它值。

具体到这次 HttpClient.get 调用,它返回一个 Observable<User[]>,顾名思义就是“一个用户数组的可观察对象”。在实践中,它也只会返回一个用户数组。

HttpClient.get 返回响应数据

HttpClient.get 默认情况下把响应体当做无类型的 JSON 对象进行返回。 如果指定了可选的模板类型 <User[]>,就会给返回你一个类型化的对象。

JSON 数据的具体形态是由服务器的数据 API 决定的。 这里我们的 API 会把用户数据作为一个数组进行返回。

其它 API 可能在返回对象中深埋着你想要的数据。 你可能要借助 RxJS 的 map 操作符对 Observable 的结果进行处理,以便把这些数据挖掘出来。比如下面将要讨论的 getUserNo404() 方法中找到一个使用 map 操作符的例子。

错误处理

凡事皆会出错,特别是当你从远端服务器获取数据的时候。 UserService.getUsers() 方法应该捕获错误,并做适当的处理。

要捕获错误,你就要使用 RxJS 的 catchError() 操作符来建立对 Observable 结果的处理管道(pipe)。

从 rxjs/operators 中导入 catchError 符号,以及你稍后将会用到的其它操作符。

  1. import { catchError, map, tap } from 'rxjs/operators';

现在,使用 .pipe() 方法来扩展 Observable 的结果,并给它一个 catchError() 操作符。

  1. getUsers (): Observable<User[]> {
  2. return this.http.get<User[]>(this.UseresUrl)
  3. .pipe(
  4. catchError(this.handleError('getUsers', []))
  5. );
  6. }
  7. private handleError<T> (operation = 'operation', result?: T) {
  8. return (error: any): Observable<T> => {
  9. console.error(error);
  10. this.log(`${operation} failed: ${error.message}`);
  11. return of(result as T);
  12. };
  13. }

catchError() 操作符会拦截失败的 Observable。 它把错误对象传给错误处理器,错误处理器会处理这个错误。

下面的 handleError() 方法会报告这个错误,并返回一个无害的结果(安全值),以便应用能正常工作。

深入 Observable

UserService 的方法将会窥探 Observable 的数据流,并通过 log() 函数往页面底部发送一条消息。

它们可以使用 RxJS 的 tap 操作符来实现,该操作符会查看 Observable 中的值,使用那些值做一些事情,并且把它们传出来。 这种 tap 回调不会改变这些值本身。

下面是 getUseres 的最终版本,它使用 tap 来记录各种操作。

  1. getUsers(): Observable<User[]> {
  2. this.log('已经获取到用户列表!');
  3. return this.http.get<User[]>(this.usersURL)
  4. .pipe(
  5. tap(Users => this.log('fetched Users')),
  6. catchError(this.handleError('getUsers', []))
  7. );
  8. }

通过 id 获取用户

大多数 web API 都可以通过 api/user/:id 的形式(比如 api/user/:id )支持根据 id 获取单个对象。修改原有的 UserService.getUser() :

  1. getUser(id: number): Observable<User> {
  2. this.messageService.add(`UserService: 已经获取到用户 id=${id}`);
  3. return of(USERS.find(user => user.id === id));
  4. }

改为:

  1. getUser(id: number): Observable<User> {
  2. this.log(`已经获取到用户 id=${id}`);
  3. const url = `${this.usersURL}/${id}`;
  4. return this.http.get<User>(url)
  5. .pipe(
  6. tap(_ => this.log(`fetched user id=${id}`)),
  7. catchError(this.handleError<User>(`getUser id=${id}`))
  8. );
  9. }

同时,import { USERS } from './mock-users';导入,以及 mock-users.ts 文件都可以删除不用了。

这里和 getUsers() 相比有三个显著的差异。

  • 它使用想获取的用户的 id 构建了一个请求 URL。
  • 服务器应该使用单个用户作为回应,而不是一个用户数组。
  • 所以,getUser 会返回 Observable(“一个可观察的单个用户对象”),而不是一个可观察的用户对象数组。

运行查看效果

执行 ng serve 命名以启动应用。访问http://localhost:4200/ 效果如下:

使用 HTTP - 图1