HttpClient

Angular作为前台要和后台通讯就要用到HttpClient
现代浏览器支持使用两种不同的 API 发起 HTTP 请求:XMLHttpRequest 接口和 fetch() API。

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

准备工作

要想使用 HttpClient,就要先导入 Angular 的 HttpClientModule。大多数应用都会在根模块 AppModule 中导入它。

app.module.ts:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { HttpClientModule } from '@angular/common/http';    //导入模块

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    HttpClientModule,    //导入模块
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

新建一个HttpService,来处理所有http的请求:

1
ng generate service Http

然后在新建的http.service.ts类中进行依赖注入:

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

@Injectable({
  providedIn: 'root'
})
export class HttpService {

  constructor(private http: HttpClient) { }
}

获取JSON数据

应用通常会从服务器上获取 JSON 数据。 比如,该应用可能要从服务器上获取配置文件 config.json,其中指定了一些特定资源的 URL。

在assets文件夹下新建一个json文件:

assets/config.json:

1
2
3
4
{
    "heroesUrl": "api/heroes",
    "textfile": "assets/textfile.txt"
}

然后HttpService通过HttpClient的get方法获取这个文件:

http.service.ts:

1
2
3
4
5
configUrl = 'assets/config.json';

getConfig() {
  return this.http.get(this.configUrl);
}

然后用对话框将其显示出来:

app.component.ts:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { Component, OnInit } from '@angular/core';
import { HttpService } from './http.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit{
  constructor(private http: HttpService) { }

  ngOnInit(){  
    this.http.getConfig()
    .subscribe((data: Config) => 
      alert("heroesUrl:"+data['heroesUrl']+" textfile:"+data['textfile'])   //取得到数据后直接用对话框将内容显示出来
      );
  }
}

class Config{   //定义一个Config类来存放数据
  heroesUrl: string;
  textfile: string;
}

这里使用了一个subscribe方法,这个方法会在http完成异步数据请求后调用,可以在这个方法中来处理接收到数据后的操作

lambada表达式中,(data: Config)定义了数据的类型,然后通过data['heroesUrl']的方式来取得数据,除此之外,也可以直接定义get方法返回的数据类型

将http.service.ts改成:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Config } from 'protractor';

@Injectable({
  providedIn: 'root'
})
export class HttpService {
  configUrl = 'assets/config.json';

  constructor(private http: HttpClient) { }

  getConfig() {
    return this.http.get<Config>(this.configUrl);
  }
}

这样angular就知道这个get方法请求的数据类型是Config,在app.component.ts可以直接写成:

1
2
3
4
5
6
  ngOnInit(){  
    this.http.getConfig()
    .subscribe((data: Config) => 
      alert("heroesUrl:"+data.heroesUrl+" textfile:"+data.textfile)
      );
  }

读取完整的响应体

响应体可能并不包含你需要的全部信息。有时候服务器会返回一个特殊的响应头或状态码,以标记出特定的条件,因此读取它们可能是必要的。

要这样做,你就要通过 observe 选项来告诉 HttpClient,你想要完整的响应信息,而不是只有响应体:

http.service.ts:

1
2
3
4
getConfigResponse(): Observable<HttpResponse<Config>> {
  return this.http.get<Config>(
    this.configUrl, { observe: 'response' });
}

现在 HttpClient.get() 会返回一个 HttpResponse 类型的 Observable,而不只是 JSON 数据。

该组件的 showConfigResponse() 方法会像显示配置数据一样显示响应头:

app.component.ts:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
showConfigResponse() {
  this.http.getConfigResponse()
    // resp is of type `HttpResponse<Config>`
    .subscribe(resp => {
      // display its headers
      const keys = resp.headers.keys();
      this.headers = keys.map(key =>
        `${key}: ${resp.headers.get(key)}`);

      // access the body directly, which is typed as `Config`.
      this.config = { ... resp.body };
    });
}

该响应对象具有一个带有正确类型的 body 属性。

错误处理

如果这个请求导致了服务器错误怎么办?甚至,在烂网络下请求都没到服务器该怎么办?HttpClient 就会返回一个错误(error)而不再是成功的响应。

通过在 .subscribe() 中添加第二个回调函数,你可以在组件中处理它:

app.component.ts:

1
2
3
4
5
6
7
showConfig() {
  this.http.getConfig()
    .subscribe(
      (data: Config) => this.config = { ...data }, // success path
      error => this.error = error // error path
    );
}

检测错误的发生是第一步,不过如果知道具体发生了什么错误才会更有用。上面例子中传给回调函数的 err 参数的类型是 HttpErrorResponse,它包含了这个错误中一些很有用的信息。

可能发生的错误分为两种。如果后端返回了一个失败的返回码(如 404、500 等),它会返回一个错误响应体。

或者,如果在客户端这边出了错误(比如在 RxJS 操作符 (operator) 中抛出的异常或某些阻碍完成这个请求的网络错误),就会抛出一个 Error 类型的异常。

HttpClient 会在 HttpErrorResponse 中捕获所有类型的错误信息,你可以查看这个响应体以了解到底发生了什么。

错误的探查、解释和解决是你应该在服务中做的事情,而不是在组件中。

你可能首先要设计一个错误处理器,就像这样:

http.service.ts:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
private handleError(error: HttpErrorResponse) {
  if (error.error instanceof ErrorEvent) {
    // A client-side or network error occurred. Handle it accordingly.
    console.error('An error occurred:', error.error.message);
  } else {
    // The backend returned an unsuccessful response code.
    // The response body may contain clues as to what went wrong,
    console.error(
      `Backend returned code ${error.status}, ` +
      `body was: ${error.error}`);
  }
  // return an observable with a user-facing error message
  return throwError(
    'Something bad happened; please try again later.');
}

注意,该处理器返回一个带有用户友好的错误信息的 RxJS ErrorObservable 对象。 该服务的消费者期望服务的方法返回某种形式的 Observable,就算是“错误的”也可以。

现在,你获取了由 HttpClient 方法返回的 Observable,并把它们通过管道传给错误处理器。

http.service.ts:

1
2
3
4
5
6
getConfig() {
  return this.http.get<Config>(this.configUrl)
    .pipe(
      catchError(this.handleError)
    );
}

有时候,错误只是临时性的,只要重试就可能会自动消失。 比如,在移动端场景中可能会遇到网络中断的情况,只要重试一下就能拿到正确的结果。

RxJS 库提供了几个 retry 操作符,它们值得仔细看看。 其中最简单的是 retry(),它可以对失败的 Observable 自动重新订阅几次。对 HttpClient 方法调用的结果进行重新订阅会导致重新发起 HTTP 请求。

把它插入到 HttpClient 方法结果的管道中,就放在错误处理器的紧前面。

http.service.ts:

1
2
3
4
5
6
7
getConfig() {
  return this.http.get<Config>(this.configUrl)
    .pipe(
      retry(3), // retry a failed request up to 3 times
      catchError(this.handleError) // then handle the error
    );
}

把数据发送到服务器

除了从服务器获取数据之外,HttpClient 还支持修改型的请求,也就是说,通过 PUT、POST、DELETE 这样的 HTTP 方法把数据发送到服务器。

本指南中的这个范例应用包括一个简化版本的《英雄指南》,它会获取英雄数据,并允许用户添加、删除和修改它们。

下面的这些章节中包括该范例的 HeroesService 中的一些方法片段。

添加请求头

很多服务器在进行保存型操作时需要额外的请求头。 比如,它们可能需要一个 Content-Type 头来显式定义请求体的 MIME 类型。 也可能服务器会需要一个认证用的令牌(token)。

HeroesService 在 httpOptions 对象中就定义了一些这样的请求头,并把它传给每个 HttpClient 的保存型方法。

http.service.ts:

1
2
3
4
5
6
7
8
import { HttpHeaders } from '@angular/common/http';

const httpOptions = {
  headers: new HttpHeaders({
    'Content-Type':  'application/json',
    'Authorization': 'my-auth-token'
  })
};

发起一个 POST 请求

应用经常把数据 POST 到服务器。它们会在提交表单时进行 POST。 下面这个例子中,HeroesService 在把英雄添加到数据库中时,就会使用 POST。

http.service.ts:

1
2
3
4
5
6
7
/** POST: add a new hero to the database */
addHero (hero: Hero): Observable<Hero> {
  return this.http.post<Hero>(this.heroesUrl, hero, httpOptions)
    .pipe(
      catchError(this.handleError('addHero', hero))
    );
}

addHero方法中,post方法的第一个参数(this.heroesUrl)代表要请求的链接
第二个参数(hero)代表将传送给服务器的数据
第三个参数(httpOptions)代表了发送给服务器的http请求头

发起 DELETE 请求

该应用可以把英雄的 id 传给 HttpClient.delete 方法的请求 URL 来删除一个英雄。

http.service.ts:

1
2
3
4
5
6
7
8
/** DELETE: delete the hero from the server */
deleteHero (id: number): Observable<{}> {
  const url = `${this.heroesUrl}/${id}`; // DELETE api/heroes/42
  return this.http.delete(url, httpOptions)
    .pipe(
      catchError(this.handleError('deleteHero'))
    );
}

发起 PUT 请求

应用可以发送 PUT 请求,来使用修改后的数据完全替换掉一个资源。 下面的 Service 例子和 POST 的例子很像。

http.service.ts:

1
2
3
4
5
6
7
/** PUT: update the hero on the server. Returns the updated hero upon success. */
updateHero (hero: Hero): Observable<Hero> {
  return this.http.put<Hero>(this.heroesUrl, hero, httpOptions)
    .pipe(
      catchError(this.handleError('updateHero', hero))
    );
}