使用 HTTP
在本章节中,我们将会使用 Angular 的 HttpClient,来实现 HTTP 服务的调用。
启用 HTTP 服务
HttpClient 是 Angular 通过 HTTP 与远程服务器通讯的机制。
要让 HttpClient 在应用中随处可用,请打开根模块 AppModule,从 @angular/common/http
中导入 HttpClientModule,并把它加入 @NgModule.imports 数组中。
完整代码如下:
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { HttpClientModule } from '@angular/common/http';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
import { UsersComponent } from './users/users.component';
import { UserDetailComponent } from './user-detail/user-detail.component';
import { MessagesComponent } from './messages/messages.component';
import { AppRoutingModule } from './/app-routing.module';
import { DashboardComponent } from './dashboard/dashboard.component';
import { HttpClientInMemoryWebApiModule } from 'angular-in-memory-web-api';
import { InMemoryDataService } from './in-memory-data.service';
@NgModule({
declarations: [
AppComponent,
UsersComponent,
UserDetailComponent,
MessagesComponent,
DashboardComponent
],
imports: [
BrowserModule,
FormsModule,
AppRoutingModule,
HttpClientModule,
HttpClientInMemoryWebApiModule.forRoot(
InMemoryDataService, { dataEncapsulation: false }
)
],
providers: [],
bootstrap: [AppComponent]
})
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
来进行独立安装。安装过程如下:
npm install angular-in-memory-web-api --save
> node-sass@4.9.2 install D:\workspaceGithub\angular-tutorial\samples\user-management\node_modules\node-sass
> node scripts/install.js
Downloading binary from https://github.com/sass/node-sass/releases/download/v4.9.2/win32-x64-64_binding.node
Cannot download "https://github.com/sass/node-sass/releases/download/v4.9.2/win32-x64-64_binding.node":
connect ETIMEDOUT 54.231.114.146:443
Timed out whilst downloading the prebuilt binary
Hint: If github.com is not accessible in your location
try setting a proxy via HTTP_PROXY, e.g.
export HTTP_PROXY=http://example.com:1234
or configure npm proxy via
npm config set proxy http://example.com:8080
> node-sass@4.9.2 postinstall D:\workspaceGithub\angular-tutorial\samples\user-management\node_modules\node-sass
> node scripts/build.js
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=
gyp info it worked if it ends with ok
gyp verb cli [ 'D:\\Program Files\\nodejs\\node.exe',
...
+ angular-in-memory-web-api@0.6.1
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
来更新依赖。比如:
ng update
We analyzed your package.json, there are some packages to update:
Name Version Command to update
--------------------------------------------------------------------------------
@angular/core 6.1.0 -> 6.1.6 ng update @angular/core
rxjs 6.2.2 -> 6.3.2 ng update rxjs
There might be additional packages that are outdated.
Or run ng update --all to try to update all at the same time.
注意:有时开发者自己去调整版本号是一件复杂的事情,因为不同版本之间的库存在兼容性问题。如果想把所有的依赖都更新到最新的兼容版本,请执行ng update --all --next --force
。
安装内存 Web API 模块完成之后,就可以在 package.json 中看到该模块的信息:
"dependencies": {
"@angular/animations": "^6.1.0",
"@angular/common": "^6.1.0",
"@angular/compiler": "^6.1.0",
"@angular/core": "^6.1.0",
"@angular/forms": "^6.1.0",
"@angular/http": "^6.1.0",
"@angular/platform-browser": "^6.1.0",
"@angular/platform-browser-dynamic": "^6.1.0",
"@angular/router": "^6.1.0",
"angular-in-memory-web-api": "^0.6.1",
"core-js": "^2.5.4",
"rxjs": "^6.3.2",
"zone.js": "~0.8.26"
},
而后在 app.module.ts 中导入 HttpClientInMemoryWebApiModule 和 InMemoryDataService 类:
import { HttpClientInMemoryWebApiModule } from 'angular-in-memory-web-api';
import { InMemoryDataService } from './in-memory-data.service';
把 HttpClientInMemoryWebApiModule 添加到 @NgModule.imports 数组中(放在 HttpClient 之后), 然后使用 InMemoryDataService 来配置它:
HttpClientModule,
HttpClientInMemoryWebApiModule.forRoot(
InMemoryDataService, { dataEncapsulation: false }
)
forRoot() 配置方法接受一个 InMemoryDataService 类(初期的内存数据库)作为参数。
在应用中创建该 InMemoryDataService 类(src/app/in-memory-data.service.ts),内容如下:
import { InMemoryDbService } from 'angular-in-memory-web-api';
export class InMemoryDataService implements InMemoryDbService {
createDb() {
const users = [
{ id: 11, name: 'Way Lau' },
{ id: 12, name: 'Narco' },
{ id: 13, name: 'Bombasto' },
{ id: 14, name: 'Celeritas' },
{ id: 15, name: 'Magneta' },
{ id: 16, name: 'RubberMan' },
{ id: 17, name: 'Dynama' },
{ id: 18, name: 'Dr IQ' },
{ id: 19, name: 'Magma' },
{ id: 20, name: 'Tornado' }
];
return {users};
}
}
InMemoryDataService 替代了 mock-Useres.ts。
等你真实的服务器就绪时,就可以删除这个内存 Web API,该应用的请求就会直接发给真实的服务器。
使用 HTTP
修改 UserService (src/app/user.service.ts)
import { HttpClient, HttpHeaders } from '@angular/common/http';
把 HttpClient 注入到构造函数中一个名叫 http 的私有属性中。
constructor(
private http: HttpClient,
private messageService: MessageService) { }
保留对 MessageService 的注入。并在 UserService 中添加一个私有的 log 方法中。
private log(message: string) {
this.messageService.add(`UserService: ${message}`);
}
把服务器上用户数据资源的访问地址定义为 usersURL。
private usersURL = 'api/users'; // URL to web api
通过 HttpClient 获取用户
当前的 UserService.getUsers() 使用 RxJS 的 of() 函数来把模拟用户数据返回为 Observable<User[]>
格式:
getUsers(): Observable<User[]> {
this.messageService.add('UserService: 已经获取到用户列表!');
return of(USERS);
}
把该方法转换成使用 HttpClient 的 get 方法,打印消息的方法也做了重构,使用了 log 方法:
getUsers(): Observable<User[]> {
this.log('已经获取到用户列表!');
return this.http.get<User[]>(this.usersURL);
}
刷新浏览器后,用户数据就会从模拟服务器被成功读取。
你用 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 符号,以及你稍后将会用到的其它操作符。
import { catchError, map, tap } from 'rxjs/operators';
现在,使用 .pipe() 方法来扩展 Observable 的结果,并给它一个 catchError() 操作符。
getUsers (): Observable<User[]> {
return this.http.get<User[]>(this.UseresUrl)
.pipe(
catchError(this.handleError('getUsers', []))
);
}
private handleError<T> (operation = 'operation', result?: T) {
return (error: any): Observable<T> => {
console.error(error);
this.log(`${operation} failed: ${error.message}`);
return of(result as T);
};
}
catchError() 操作符会拦截失败的 Observable。 它把错误对象传给错误处理器,错误处理器会处理这个错误。
下面的 handleError() 方法会报告这个错误,并返回一个无害的结果(安全值),以便应用能正常工作。
深入 Observable
UserService 的方法将会窥探 Observable 的数据流,并通过 log() 函数往页面底部发送一条消息。
它们可以使用 RxJS 的 tap 操作符来实现,该操作符会查看 Observable 中的值,使用那些值做一些事情,并且把它们传出来。 这种 tap 回调不会改变这些值本身。
下面是 getUseres 的最终版本,它使用 tap 来记录各种操作。
getUsers(): Observable<User[]> {
this.log('已经获取到用户列表!');
return this.http.get<User[]>(this.usersURL)
.pipe(
tap(Users => this.log('fetched Users')),
catchError(this.handleError('getUsers', []))
);
}
通过 id 获取用户
大多数 web API 都可以通过 api/user/:id 的形式(比如 api/user/:id )支持根据 id 获取单个对象。修改原有的 UserService.getUser() :
getUser(id: number): Observable<User> {
this.messageService.add(`UserService: 已经获取到用户 id=${id}`);
return of(USERS.find(user => user.id === id));
}
改为:
getUser(id: number): Observable<User> {
this.log(`已经获取到用户 id=${id}`);
const url = `${this.usersURL}/${id}`;
return this.http.get<User>(url)
.pipe(
tap(_ => this.log(`fetched user id=${id}`)),
catchError(this.handleError<User>(`getUser id=${id}`))
);
}
同时,import { USERS } from './mock-users';
导入,以及 mock-users.ts 文件都可以删除不用了。
这里和 getUsers() 相比有三个显著的差异。
- 它使用想获取的用户的 id 构建了一个请求 URL。
- 服务器应该使用单个用户作为回应,而不是一个用户数组。
- 所以,getUser 会返回 Observable
(“一个可观察的单个用户对象”),而不是一个可观察的用户对象数组。
运行查看效果
执行 ng serve
命名以启动应用。访问http://localhost:4200/ 效果如下: