angular 入坑记录的笔记第四篇,介绍在 angular 中如何经过 HttpClient 类发起 http 请求,从而完成与后端的数据交互。css
对应官方文档地址:html
配套代码地址:angular-practice/src/http-guide前端
在前端项目与后端进行数据交互时,绝大多数都是经过 HTTP 协议进行的,现代浏览器支持两种方式向后端发起 HTTP 请求:XMLHttpRequest 和 fetchjquery
在之前的项目中,一般使用 jquery 的简化版 ajax 请求向后端请求数据,归根到底最终仍是经过 XMLHttpRequest 与后端进行数据交互git
在 Angular 中, 为了简化 XMLHttpRequest 的使用,框架提供了 HttpClient 类来封装 HTTP API,用来实现前端与后端的数据交互。程序员
在使用以前,首先须要在应用的根模块中,引入 HttpClientModule 模块,并添加到 imports 数组中github
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
// 添加对于 HttpClientModule 模块的引用
import { HttpClientModule } from '@angular/common/http';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
AppRoutingModule,
HttpClientModule // 添加到根应用模块中
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
复制代码
在须要使用到的地方,引入 HttpClient 类,而后经过依赖注入的方式注入到应用类中ajax
在一般状况下,咱们须要将与后端进行交互的行为封装成服务,在这个服务中完成对于获取到的数据的处理,以后再注入到须要使用该服务的组件中,从而确保组件中仅仅包含的是必要的业务逻辑行为typescript
import { Injectable } from '@angular/core';
// 引入 HttpClient 类
import { HttpClient } from '@angular/common/http';
@Injectable({
providedIn: 'root'
})
export class AntiMotivationalQuotesServicesService {
// 经过构造函数注入的方式依赖注入到使用的类中
constructor(private http: HttpClient) { }
}
复制代码
import { Component, OnInit } from '@angular/core';
// 引入服务
import { AntiMotivationalQuotesServicesService } from './../services/anti-motivational-quotes-services.service';
@Component({
selector: 'app-anti-motivational-quotes',
templateUrl: './anti-motivational-quotes.component.html',
styleUrls: ['./anti-motivational-quotes.component.scss']
})
export class AntiMotivationalQuotesComponent implements OnInit {
// 经过构造函数注入的方式使用服务
constructor(private services: AntiMotivationalQuotesServicesService) { }
ngOnInit(): void {
}
}
复制代码
这里使用到的后端接口是掘金上一位朋友开发的毒鸡汤接口(api.tryto.cn/djt/text),全部权归属于小咸鱼丶编程
经过使用 postman 进行接口调用能够发现,接口返回的响应信息以下
在项目中建立一个接口,按照后端返回的数据信息进行属性的定义,用来映射请求的响应信息(Angular 只能将请求响应对象转换成接口类型,不能自动转换成类实例)
ng g interface interfaces/get-quotes-response-model
复制代码
export interface GetQuotesResponseModel {
/** * 接口响应码 */
code: number;
/** * 响应信息 */
msg: string;
/** * 响应数据 */
data: ResponseData;
/** * 响应时间 */
time: number;
}
/** * 接口响应的内容信息 */
interface ResponseData {
/** * 毒鸡汤 */
content: string;
/** * 热度 */
hots: number;
}
复制代码
在服务中,引入请求响应对象的接口定义,而后设定 get 请求的响应对象为 GetQuotesResponseModel,以后在使用时就能够以一种结构化数据的方式获取请求返回的数据信息
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
// 引入 HttpClient 类
import { HttpClient } from '@angular/common/http';
// 引入接口响应类
import { GetQuotesResponseModel } from '../interfaces/get-quotes-response-model';
@Injectable({
providedIn: 'root'
})
export class AntiMotivationalQuotesServicesService {
// 经过构造函数注入的方式依赖注入到使用的类中
constructor(private http: HttpClient) { }
/** * 经过 get 请求获取毒鸡汤信息 */
getAntiMotivationalQuotes(): Observable<GetQuotesResponseModel> {
const url = 'https://api.tryto.cn/djt/text';
return this.http.get<GetQuotesResponseModel>(url);
}
}
复制代码
在组件中,经过调用注入的服务类完成接口数据的获取,由于是以一种结构化对象的形式获取到接口返回的数据,所以这里能够直接经过对象属性获取到指定的属性信息
import { Component, OnInit } from '@angular/core';
// 引入服务
import { AntiMotivationalQuotesServicesService } from './../services/anti-motivational-quotes-services.service';
// 引入接口响应对象
import { GetQuotesResponseModel } from '../interfaces/get-quotes-response-model';
@Component({
selector: 'app-anti-motivational-quotes',
templateUrl: './anti-motivational-quotes.component.html',
styleUrls: ['./anti-motivational-quotes.component.scss']
})
export class AntiMotivationalQuotesComponent implements OnInit {
public quoteResponse: GetQuotesResponseModel;
// 经过构造函数注入的方式使用服务
constructor(private services: AntiMotivationalQuotesServicesService) { }
ngOnInit(): void {
}
/** * 获取毒鸡汤 */
getQuotes() {
this.services.getAntiMotivationalQuotes().subscribe((response: GetQuotesResponseModel) => {
this.quoteResponse = response;
});
}
}
复制代码
由于最终须要的信息是接口返回的响应信息对象中的一个属性,所以这里须要使用安全导航运算符(?)来确保模板的渲染不会由于空指针错误而中断
<p>
<button (click)="getQuotes()">获取毒鸡汤</button>
</p>
<p>
接口返回信息: {{quoteResponse | json}}
</p>
<i>
毒鸡汤:{{quoteResponse?.data?.content}}
</i>
复制代码
在执行服务中的方法时,有时会存在没有回调函数的状况,此时也必须执行 subscribe 方法,不然服务中的 HTTP 请求是没有真正发起的
服务中的 getAntiMotivationalQuotes 只能获取到接口返回的 body 里面的信息,某些状况下须要获取到完整的响应信息,此时须要经过 observe 参数来告诉 HttpClient 此方法须要返回完整的响应信息
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
// 引入 HttpClient 类
import { HttpClient, HttpResponse } from '@angular/common/http';
// 引入接口响应类
import { GetQuotesResponseModel } from '../interfaces/get-quotes-response-model';
@Injectable({
providedIn: 'root'
})
export class AntiMotivationalQuotesServicesService {
// 经过构造函数注入的方式依赖注入到使用的类中
constructor(private http: HttpClient) { }
/** * 获取完整的接口请求信息 */
getAntiMotivationalQuotesResponse(): Observable<HttpResponse<GetQuotesResponseModel>> {
const url = 'https://api.tryto.cn/djt/text';
return this.http.get<GetQuotesResponseModel>(url, { observe: 'response' });
}
}
复制代码
import { HttpResponse } from '@angular/common/http';
import { Component, OnInit } from '@angular/core';
// 引入服务
import { AntiMotivationalQuotesServicesService } from './../services/anti-motivational-quotes-services.service';
// 引入接口响应对象
import { GetQuotesResponseModel } from '../interfaces/get-quotes-response-model';
@Component({
selector: 'app-anti-motivational-quotes',
templateUrl: './anti-motivational-quotes.component.html',
styleUrls: ['./anti-motivational-quotes.component.scss']
})
export class AntiMotivationalQuotesComponent implements OnInit {
public quoteResponseInfo: HttpResponse<GetQuotesResponseModel>;
// 经过构造函数注入的方式使用服务
constructor(private services: AntiMotivationalQuotesServicesService) { }
ngOnInit(): void {
}
/** * 获取毒鸡汤接口完整的请求信息 */
getQuotesResponse() {
this.services.getAntiMotivationalQuotesResponse().subscribe((response: HttpResponse<GetQuotesResponseModel>) => {
this.quoteResponseInfo = response;
});
}
}
复制代码
HttpClient 默认的返回信息格式都是 json 对象,在后端接口返回的并非 json 对象的状况下,须要手动的设置响应类型(text、blob、arraybuffer...)
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
// 引入 HttpClient 类
import { HttpClient, HttpResponse } from '@angular/common/http';
// 引入接口响应类
import { GetQuotesResponseModel } from '../interfaces/get-quotes-response-model';
@Injectable({
providedIn: 'root'
})
export class AntiMotivationalQuotesServicesService {
// 经过构造函数注入的方式依赖注入到使用的类中
constructor(private http: HttpClient) { }
/** * 获取响应类型非 json 对象的信息 */
getYuiterSitemap(): Observable<string> {
const url = 'https://yuiter.com/sitemap.xml';
return this.http.get(url, { responseType: 'text' });
}
}
复制代码
在同后端接口进行交互时,获取数据通常用的是 get 请求,而当进行数据新增、更新、删除时则会使用 post、put、delete 这三个 HTTP 谓词
在毒鸡汤这个接口中,可使用 post 方式调用 api.tryto.cn/djt/submit 进行毒鸡汤的提交
根据 postman 的调用示例,在服务中定义一个方法用来提交毒鸡汤信息,这里的 SetQuotesResponseModel 为接口返回的响应对象
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
// 引入 HttpClient 类
import { HttpClient, HttpResponse } from '@angular/common/http';
// 引入接口响应类
import { SetQuotesResponseModel } from '../interfaces/set-quotes-response-model';
@Injectable({
providedIn: 'root'
})
export class AntiMotivationalQuotesServicesService {
// 经过构造函数注入的方式依赖注入到使用的类中
constructor(private http: HttpClient) { }
/** * 提交毒鸡汤信息 * @param content 毒鸡汤 */
submitAntiMotivationalQuote(content: string): Observable<SetQuotesResponseModel> {
const url = 'https://api.tryto.cn/djt/submit';
return this.http.post<SetQuotesResponseModel>(url, {
content
});
}
}
复制代码
由于这里是以默认的表单提交的方式进行的数据提交,当后端须要修改请求的 body 格式时,则须要咱们修改请求的 MIME 类型
当须要更改请求的 MIME 类型或是须要添加受权访问的 token 信息这一类的操做时,须要在使用 HttpClient 提供的请求方法时添加上 HTTP 请求头配置信息
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
// 引入 HttpClient 类
import { HttpClient, HttpResponse, HttpHeaders } from '@angular/common/http';
@Injectable({
providedIn: 'root'
})
export class AntiMotivationalQuotesServicesService {
// 经过构造函数注入的方式依赖注入到使用的类中
constructor(private http: HttpClient) { }
public httpOptions = {
headers: new HttpHeaders({
'Content-Type': 'application/json',
'Authorization': 'token'
})
};
/** * 修改请求头信息 */
submitWithOptions() {
const url = '';
return this.http.post(url, {
data: ''
}, this.httpOptions);
}
}
复制代码
在涉及到先后端交互的过程当中,不可避免会出现各类情况,在出现错误时,能够在 subscribe 方法中,添加第二个回调方法来获取错误信息
getQuotes() {
this.services.getAntiMotivationalQuotes().subscribe((response: GetQuotesResponseModel) => {
this.quoteResponse = response;
}, error => {
console.error(error);
});
}
复制代码
在处理错误信息的回调方法中,方法返回了一个 HttpErrorResponse 对象来描述错误信息
由于这里的错误更可能是服务在与后端进行通讯产生的错误,所以对于错误信息的捕获和处理更应该放到服务中进行,而在组件处仅显示错误提示
在服务中定义一个错误处理器,用来处理与后端请求中发生的错误
import { Injectable } from '@angular/core';
import { Observable, throwError } from 'rxjs';
import { catchError, retry } from 'rxjs/operators';
// 引入 HttpClient 类
import { HttpClient, HttpResponse, HttpHeaders, HttpErrorResponse } from '@angular/common/http';
@Injectable({
providedIn: 'root'
})
export class AntiMotivationalQuotesServicesService {
// 经过构造函数注入的方式依赖注入到使用的类中
constructor(private http: HttpClient) { }
/** * 经过 get 请求获取毒鸡汤信息 */
getAntiMotivationalQuotes(): Observable<GetQuotesResponseModel> {
const url = 'https://api.tryto.cn/djt/text32';
return this.http.get<GetQuotesResponseModel>(url)
.pipe(
catchError(this.handleError)
);
}
/** * 错误信息捕获处理 * @param error 错误信息 */
private handleError(error: HttpErrorResponse) {
if (error.error instanceof ErrorEvent) {
// 客户端自己引发的错误信息
console.error(`客户端错误:${error.error.message}`);
} else {
// 服务端返回的错误信息
console.error(`服务端错误:HTTP 状态码:${error.status} \n\r 错误信息:${JSON.stringify(error.error)}`);
}
// 反馈给用户的错误信息(用于组件中使用 error 回调时的错误提示)
return throwError('很差的事情发生了,毕竟咱们都有不顺利的时候。。。');
}
}
复制代码
当请求发生错误时,经过在 HttpClient 方法返回的 Observable 对象中使用 pipe 管道将错误传递给自定义的错误处理器,从而完成捕获错误信息的后续操做
某些状况下存在由于特殊缘由致使短期的请求失败,这时能够在 pipe 管道中,当请求失败后,使用 retry 方法进行屡次的请求重试,在进行了屡次重试后仍是没法进行数据通讯后,则进行错误捕获
getAntiMotivationalQuotes(): Observable<GetQuotesResponseModel> {
const url = 'https://api.tryto.cn/djt/text32';
return this.http.get<GetQuotesResponseModel>(url)
.pipe(
retry(3), // 重试三次
catchError(this.handleError) // 捕获错误信息
);
}
复制代码
在向服务器发起请求时,通常是须要咱们在请求头中添加上受权的 token 信息,与其当后端接口返回咱们无权访问时再来处理,是否是能够在发起请求前去进行拦截判断,若是不包含 token 信息,则将容许访问的 token 信息添加到请求中
一样的,当已经定义好后端返回什么信息表明请求出错 or 直接根据后端返回的请求状态码判断请求出错时,彻底能够经过对接口返回的响应进行拦截,直接拦截掉请求出错的状况,从而不须要在后续的业务逻辑代码中再进行判断请求是否成功
在 Angular 中能够新建一个继承于 HttpInterceptor 接口的拦截器类,经过实现 intercept 方法来对请求进行拦截处理
与 ASP.NET Core 中的中间件类似,咱们能够在请求中添加多个的拦截器,构成一个拦截器链。当一个拦截器已经处理完成时,须要经过 next 对象将 HTTP 请求传递到下一个拦截器,不然,整个请求将会中断。若是当前的拦截器已是整个拦截器链的最后一个,则会将请求发送到后端接口
import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpResponse } from '@angular/common/http';
import { Observable } from 'rxjs/internal/Observable';
import { Injectable } from '@angular/core';
import { tap, finalize } from 'rxjs/operators';
/** * 经过添加 Injectable 特性,代表能够经过依赖注入的方式进行建立 */
@Injectable()
export class LoggingInterceptor implements HttpInterceptor {
/** * 请求拦截 * @param req http 请求 * @param next 下一个拦截器 */
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
// 开始时间
const started = Date.now();
let msg: string;
// 将 http 请求信息传递给下一个拦截器
return next.handle(req)
.pipe(
tap(
// 捕获当前请求是否成功 or 失败
// 一、经过判断响应的类型是否为 HttpResponse 来判断请求是否成功
event => msg = event instanceof HttpResponse ? '请求成功' : '请求失败',
// 二、若是存在了 error 回调,则请求失败
error => msg = '请求失败'
), finalize(() => {
const elapsed = Date.now() - started;
console.log(`请求方式:${req.method} 请求地址:${req.urlWithParams} 响应耗时:${elapsed} ms 请求结果:${msg}`);
}));
}
}
复制代码
当定义好拦截器后,与其它的自定义服务同样,咱们须要添加到根模块的 providers 中,由于可能会存在定义多个拦截器的状况,这里能够经过定义一个 typescript 文件用来导出咱们须要添加的拦截器信息
由于会存在定义多个拦截器的状况,因此这里须要指定 multi 属性为 true
import { HTTP_INTERCEPTORS } from '@angular/common/http';
// 须要添加的拦截器
import { LoggingInterceptor } from './logging-interceptor';
// 返回的拦截器数组
export const HttpInterceptorProviders = [
{ provide: HTTP_INTERCEPTORS, useClass: LoggingInterceptor, multi: true }
];
复制代码
因为拦截器具备将发送到服务端的 HTTP 请求进行监视、转化,以及拦截请求的响应信息的双重效果,所以当咱们注册了多个拦截器时,在发送请求时会按照咱们添加的顺序进行执行,而在接受到请求响应时,则是按照反过来的顺序进行执行
获取到导出的拦截器信息,就能够在根模块中去导入须要注册的拦截器
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
// 添加自定义拦截器
import { HttpInterceptorProviders } from './http-interceptors/http-interceptor-providers';
@NgModule({
declarations: [
AppComponent,
AntiMotivationalQuotesComponent
],
imports: [
BrowserModule,
AppRoutingModule,
FormsModule,
HttpClientModule // 添加到根应用模块中
],
providers: [
HttpInterceptorProviders
],
bootstrap: [AppComponent]
})
export class AppModule { }
复制代码
因为一个请求可能会存在重试发起的状况,为了确保屡次发起请求时的请求信息的不变性,对于 HttpRequest 和 HttpResponse 咱们是不能够修改原始的对象属性值的
当咱们须要对请求进行修改时,例如在请求的 header 中添加上 token 信息,此时咱们须要先克隆一个原始的请求对象,在这个克隆后的请求上进行操做,最终将这个克隆后的请求传递给下一个拦截器
import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpResponse } from '@angular/common/http';
import { Observable } from 'rxjs/internal/Observable';
import { Injectable } from '@angular/core';
import { tap, finalize } from 'rxjs/operators';
/** * 经过添加 Injectable 特性,代表能够经过依赖注入的方式进行建立 */
@Injectable()
export class LoggingInterceptor implements HttpInterceptor {
/** * 请求拦截 * @param req http 请求 * @param next 下一个拦截器 */
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
// 开始时间
const started = Date.now();
let msg: string;
// 打印原始的请求信息
console.log(`原始的请求信息:${JSON.stringify(req.headers)}`);
// 获取请求中的 token 信息
const token = req.headers.get('Authorization') || '';
// 克隆请求信息
const authReq = req.clone({
headers: token === '' ? req.headers.set('Authorization', '123456') : req.headers
});
// 打印修改后的请求信息
console.log(`克隆后的请求信息:${JSON.stringify(authReq.headers)}`);
// 将克隆后的 http 请求信息传递给下一个拦截器
return next.handle(authReq)
.pipe(
tap(
// 捕获当前请求是否成功 or 失败
// 一、经过判断响应的类型是否为 HttpResponse 来判断请求是否成功
event => msg = event instanceof HttpResponse ? '请求成功' : '请求失败',
// 二、若是存在了 error 回调,则请求失败
error => msg = '请求失败'
), finalize(() => {
const elapsed = Date.now() - started;
console.log(`请求方式:${req.method} 请求地址:${req.urlWithParams} 响应耗时:${elapsed} ms 请求结果:${msg}`);
}));
}
}
复制代码
占坑
做者:墨墨墨墨小宇
我的简介:96年生人,出生于安徽某四线城市,毕业于Top 10000000 院校。.NET程序员,枪手死忠,喵星人。于2016年12月开始.NET程序员生涯,微软.NET技术的坚决坚持者,立志成为云养猫的少年中面向谷歌编程最厉害的.NET程序员。
我的博客:yuiter.com
博客园博客:www.cnblogs.com/danvic712