0%

angular2 HttpClient

大多数前端应用都需要通过http协议来和后端服务器通信。现代浏览器提供两种不同的API发起http请求:XMLHttpRequest接口和fetch API。

基于浏览器的XMLHttpRequest接口,@angular/common/http中的HttpClient类为应用程序提供了一个简化的API来实现http功能。HttpClient带来的其他优点包括:可测试性、强类型的请求和响应对象、发起请求与接收响应时的拦截器支持,以及更好的、基于可观察(Observeable)对象的错误处理机制。

安装http模块

在使用HttpClient之前,需要先安装HttpClientModule以提供它。可以在应用模块中引用,并只需要引入一次。

1
2
3
4
5
6
7
8
9
import { NgModule } from '@angular/core';
import { HttpClientModule } from '@angular/common/http';

@NgModule({
imports: [
HttpClientModule,
],
})
export class HeroRoutingModule { }

上面我们把HttpClientModule引入到应用模块,并用imports注入,这样就可以把HttpClient注入到组件和服务中去了。

发起一个get请求

确认先在顶部引入HttpClient:

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

发起get请求:

1
2
3
4
5
getHeroes(): void {
this.http.get('http://localhost:8080/tp-api/public/index.php/hero/index').subscribe(data => {
this.heroes = data['result'].list;
});
}

上面使用get请求从url中获取数据,然后使用箭头函数的回调进行处理。上面将返回数据的result放在了方括号里面,这是为了防止TS不知道返回数据的属性而报错。HttpClient将json格式的响应内容解析为了一个对象,但是它并不知道这个对象的形态。我们可以告诉HttpClient这个响应体类型是什么。

响应体的类型检查

为了告诉HttpClient响应体的类型是什么,我们需要定义一个接口来描述这个类型的正确形态。
定义响应体的接口:

1
2
3
4
5
interface HeroResponse {
errcode: number;
errmsg: string;
result: any;
}

当发起get请求时,传入一个类型参数:

1
2
3
4
5
getHeroes(): void {
this.http.get<HeroResponse>('http://localhost:8080/tp-api/public/index.php/hero/index').subscribe(data => {
this.heroes = data.result.list;
});
}

可以看下get方法的参数定义:

1
2
3
4
5
6
7
8
get(url: string, options?: {
headers?: HttpHeaders;
observe?: 'body';
params?: HttpParams;
reportProgress?: boolean;
responseType?: 'json';
withCredentials?: boolean;
}): Observable<Object>;

可以自定义一些option参数传过去。

读取完整的响应体

有时候服务器会返回一个特殊的响应头或状态码,已标记出特定的条件,我们怎读取它?我们需要通过observe选项来告诉HttpClient,我们需要完整的响应体:

1
2
3
4
5
getHeroes(): void {
this.http.get('http://localhost:8080/tp-api/public/index.php/hero/index', {observe: 'response'}).subscribe(resp => {
console.log(resp);
});
}

这样resp里面包含了请求的所有东西。

错误处理

如果这个请求的目标地址错误怎么办?或者在网络不好的情况下超时了怎么办?HttpClient就会返回一个错误的响应。我们可以在subscribe()调用中添加一个错误处理器。

1
2
3
4
5
6
7
8
9
getHeroes(): void {
this.http.get<HeroResponse>('http://localhost:8080/tp-api/public/index.php/hero/index1').subscribe(
data => {
this.heroes = data.result.list;
},
err => {
console.log(err);
});
}

当错误发生的时候,会进入到第二个箭头函数的回调里面去,回调函数的参数err是一个HttpErrorResponse类型的对象,它包含了这个错误中一些很有用的信息。

可能发生的错误有两种,一个是后端服务器返回了一个失败的返回码(如404、500、502等),它会返回一个错误。一个是在客户端这边出现了错误(比如在RxJS操作符中抛出的异常或某些阻碍完成这个请求的网络错误),就会抛出一个Error类型的异常。

解决的方案之一就是简单的重试这次请求,这种策略对临时性的而且不大可能重复发生的错误会很有用。可以使用RxJS中的retry()操作符来解决这种问题。它会在遇到问题的时候重新订阅这个可观察对象,也就会导致再次发送这个请求。
先导入:

1
import 'rxjs/add/operator/retry';

然后使用在可观察对象上:

1
2
3
4
5
6
7
8
9
10
getHeroes(): void {
this.http.get<HeroResponse>('http://localhost:8080/tp-api/public/index.php/hero/index1').retry(3).subscribe(
data => {
this.heroes = data.result.list;
},
err => {
console.log(err);
}
);
}

post请求

常用的操作之一就是使用post请求将数据发送到服务器,比如提交表单。看下这个post请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
getHeroes(): void {
this.http.post<HeroResponse>(
'http://localhost:8080/tp-api/public/index.php/hero/index',
{
id: 3,
name: 'tony_post',
arr: [
{test: 1},
{test: 2},
]
}, {
params: new HttpParams().set('params_id', '3').set('params_name', 'tony')
}).subscribe(
data => {
this.heroes = data.result.list;
},
err => {
console.log(err);
});
}

我们在这个post请求里面,加入了请求字符串,我们看看post的参数结构:

1
2
3
4
5
6
7
8
 post(url: string, body: any | null, options?: {
headers?: HttpHeaders;
observe?: 'body';
params?: HttpParams;
reportProgress?: boolean;
responseType?: 'json';
withCredentials?: boolean;
}): Observable<Object>;

可以看到post有三个参数:urlbodyoptionsbody是对象形式,可以传递json形式的复杂数据,而options里面的params是键值对形式,会加在url后面。

看来这里很好的处理了在angular1里面使用post传值的时候无法接收到参数的问题。

是时候展示真正的技术了!

掌握了上面的知识我们来开始实践应用。发送请求的频率还是很高的,我们不可能在每个请求里面都重复写下同样的处理,所以我们来构造个请求的api服务。上方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
* 处理请求方法
* @param url
* @param method
* @param params
* @returns {any}
*/
private requestMethod(url, method, params?) {
// 拼接url
url = environment.baseUrl + environment.apiUrl + url;
let resutObs = null;
switch (method) {
case 'get':
// 处理参数
if (params) {
params = this.transformRequest(params);
}
resutObs = this.http.get(url, {responseType: 'json', params: params, withCredentials: true}, ).pipe(
catchError(this.handleError)
);
break;
case 'post':
resutObs = this.http.post(url, params, {withCredentials: true}).pipe(
catchError(this.handleError)
);
break;
}
return resutObs;
}

这里写了一个基础方法,在里面发送get请求或者post请求,这里需要注意的是,我们的客户端和服务端是不同的域名,涉及到跨域,所以需要在请求的头部里面带上withCredentials参数。为了方便我们使用get请求的时候传入json数据,写了个转换参数的方法:

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 将对象参数格式化为字符串形式
* @param data
* @returns {string}
*/
private transformRequest(data) {
let str = [];
for (const s of Object.keys(data)) {
str.push(encodeURIComponent(s) + '=' + encodeURIComponent(data[s]));
}
return str.join('&');
}

以及一个错误处理机制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 错误处理机制
*
* 错误分为两种。
* 如果后端返回一个失败的返回码(404,502,500等)它会返回一个错误的响应体。
* 如果在客户端这边出现了错误,比如某一个异常情况等,就抛出一个Error类型异常。
* @param {HttpErrorResponse} error HttpClient会在HTTPErrorResponse中捕获所有类型的错误信息
* @returns {ErrorObservable} 返回一个带有用户友好提示的错误信息的ErrorObservable对象
*/
private handleError(error: HttpErrorResponse) {
if (error.error instanceof ErrorEvent) {
console.log('客户端错误:', error.error.message);
} else {
console.log(
`后端错误
错误码为:${error.status}
错误消息:${error.error}
`);
}
return _throw('系统异常,请重试');
}

全部代码:

apiUntil.service.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
import { Injectable } from '@angular/core';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { _throw } from 'rxjs/observable/throw';
import { catchError } from 'rxjs/operators';
import {environment} from '../../environments/environment';


@Injectable()

export class ApiUntilService {

constructor(private http: HttpClient) {}

/**
* 暴露给外部的方法
* @param url
* @param params
* @returns {any}
*/
public get(url, params?) {
return this.requestMethod(url, 'get', params);
}

/**
* 暴露给外部的方法
* @param url
* @param params
* @returns {any}
*/
public post(url, params?) {
return this.requestMethod(url, 'post', params);
}

/**
* 处理请求方法
* @param url
* @param method
* @param params
* @returns {any}
*/
private requestMethod(url, method, params?) {

// 拼接url
url = environment.baseUrl + environment.apiUrl + url;

let resutObs = null;

switch (method) {
case 'get':
// 处理参数
if (params) {
params = this.transformRequest(params);
}
resutObs = this.http.get(url, {responseType: 'json', params: params, withCredentials: true}, ).pipe(
catchError(this.handleError)
);
break;
case 'post':
resutObs = this.http.post(url, params, {withCredentials: true}).pipe(
catchError(this.handleError)
);
break;
}

return resutObs;
}

/**
* 将对象参数格式化为字符串形式
* @param data
* @returns {string}
*/
private transformRequest(data) {
let str = [];
for (const s of Object.keys(data)) {
str.push(encodeURIComponent(s) + '=' + encodeURIComponent(data[s]));
}
return str.join('&');
}

/**
* 错误处理机制
*
* 错误分为两种。
* 如果后端返回一个失败的返回码(404,502,500等)它会返回一个错误的响应体。
* 如果在客户端这边出现了错误,比如某一个异常情况等,就抛出一个Error类型异常。
* @param {HttpErrorResponse} error HttpClient会在HTTPErrorResponse中捕获所有类型的错误信息
* @returns {ErrorObservable} 返回一个带有用户友好提示的错误信息的ErrorObservable对象
*/
private handleError(error: HttpErrorResponse) {
if (error.error instanceof ErrorEvent) {
console.log('客户端错误:', error.error.message);
} else {
console.log(
`后端错误
错误码为:${error.status}
错误消息:${error.error}
`);
}
return _throw('系统异常,请重试');
}
}

api.service.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { Injectable } from '@angular/core';

import { ApiUntilService } from './apiUntil.service';


@Injectable()

export class ApiService {

constructor(private apiUnit: ApiUntilService) {

}
/**
* 登录
* @param params
* @returns {any}
*/
login(params?) {
return this.apiUnit.post('/login/index', params);
}
}

login.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { ApiService } from '../config/api.service';
@Component({
selector: 'app-login',
templateUrl: './login.component.html',
styleUrls: ['./login.component.less'],
providers: [ApiService],
})
export class LoginComponent implements OnInit {
validateForm: FormGroup;
get name() {
return this.validateForm.getRawValue().userName;
}

get password() {
return this.validateForm.getRawValue().password;
}

constructor(
private api: ApiService,
private router: Router,
private fb: FormBuilder,
) {

}

ngOnInit() {
this.validateForm = this.fb.group({
userName: [ null, [ Validators.required ] ],
password: [ null, [ Validators.required ] ],
remember: [ true ],
});

}

/**
* 登录
*/
submitForm(): void {
// 发送请求
this.api.login({
name: this.name,
password: this.password,
}).subscribe((data) => {
if (data.code === 0) {
// 登录成功
this.router.navigate(['/home', {}]);

} else {
// 发生错误

return false;
}
});
}
}
码字辛苦,打赏个咖啡☕️可好?💘