详解如何在Angular优雅编写HTTP请求


Posted in Javascript onDecember 05, 2018

引言

基本上当下的应用都会分为前端与后端,当然这种前端定义不在限于桌面浏览器、手机、APP等设备。一个良好的后端会通过一套所有前端都通用的 RESTful API 序列接口作为前后端之间的通信。

这其中对于身份认证都不可能再依赖传统的Session或Cookie;转而使用诸如OAuth2、JWT等这种更适合API接口的认证方式。当然本文并不讨论如何去构建它们。

一、API 设计

首先虽然并不会讨论身份认证的技术,但不管是OAuth2还是JWT本质上身份认证都全靠一个 Token 来维持;因此,下面统一以 token 来表示身份认证所需要的值。

一套合理的API规则,会让前端编码更优雅。因此,希望在编写Angular之前,能与后端相互达成一种“协议”也很有必要。可以尝试从以下几点进行考虑。

版本号

可以在URL(例:https://demo.com/v1/)或Header(例:headers: { version: 'v1' } )中体现,相比较我更喜欢前者的直接。

业务节点

以一个节点来表示某个业务,比如:

  • 商品 https://demo.com/v1/product/
  • 商品SKU https://demo.com/v1/product/sku/

动作

由HTTP动词来表示:

  • GET 请求一个商品 /product/${ID}
  • POST 新建一个商品 /product
  • PUT 修改一个商品 /product/${ID}
  • DELETE 删除一个商品 /product/${ID}

统一响应

这一点非常重要,特别是当我们新建一个商品时,商品的属性非常多,但如果我们缺少某个属性时。可以使用这样的一种统一的响应格式:

{
  "code": 100, // 0 表示成功
  "errors": { // 错误明细
    "title": "商品名称必填"
  }
}

其中 code 不管成功与否都会有该属性。

状态码

后端响应一个请求是包括状态码和响应内容,而每一种状态码又包含着不同的含义。

  • 200 成功返回请求数据
  • 401 无权限
  • 404 无效资源

二、如何访问Http?

首先,需要导入 HttpClientModule 模块。

import { HttpClientModule } from '@angular/common/http';

@NgModule({
  imports: [
    HttpClientModule
  ]
})

然后,在组件类注入 HttpClient。

export class IndexComponent {
  constructor(private http: HttpClient) { }
}

最后,请求点击某个按钮发送一次GET请求。

user: Observable<User>;
getUser() {
  this.user = this.http.get<User>('/assets/data/user.json');
}

打印结果:

{{ user | async | json }}

三个简单的步骤,就是一个完整的HTTP请求步骤。

然后,现实与实际是有一些距离,比如说身份认证、错误处理、状态码处理等问题,在上面并无任何体现。

可,上面已经足够优雅,要让我破坏这种优雅那么此文就变得无意义了!

因此……

三、拦截器

1、HttpInterceptor 接口

正如其名,我们在不改变上面应用层面的代码下,允许我们把身份认证、错误处理、状态码处理问题给解决了!

写一个拦截器也是非常的优雅,只需要实现 HttpInterceptor 接口即可,而且只有一个 intercept 方法。

@Injectable()
export class JWTInterceptor implements HttpInterceptor {

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpSentEvent | HttpHeaderResponse | HttpProgressEvent | HttpResponse<any> | HttpUserEvent<any>> {
    // doing
  }

}

intercept 方法有两个参数,它几乎所当下流行的中间件概念一般,req 表示当前请求数据(包括:url、参数、header等),next 表示调用下一个“中间件”。

2、身份认证

req 有一个 clone 方法,允许对当前的请求参数进行克隆并且这一过程会自行根据一些参数推导,不管如何用它来产生一个新的请求数据,并在这个新数据中加入我们期望的数据,比如:token。

const jwtReq = req.clone({
  headers: req.headers.set('token', 'xxxxxxxxxxxxxxxxxxxxx')
});

当然,你可以再折腾更多请求前的一些配置。

最后,把新请求参数传递给下一个“中间件”。

return next.handle(jwtReq);

等等,都 return 了,说好的状态码、异常处理呢?

3、异常处理

仔细再瞧 next.handle 返回的是一个 Observable 类型。看到 Observable 我们会想到什么?mergeMap、catch 等一大堆东西。

因此,我们可以利用这些操作符来改变响应的值。

mergeMap

请求过程中会会有一些过程状态,比如请求前、上传进度条、请求结束等,Angular在每一次这类动作中都会触次 next。因此,我们只需要在返回 Observable 对象加上 mergeMap 来观察这些值的变更,这样有非常大的自由空间想象。

return next.handle(jwtReq).mergeMap((event: any) => {
    if (event instanceof HttpResponse && event.body.code !== 0) {
      return Observable.create(observer => observer.error(event));
    }
    return Observable.create(observer => observer.next(event));
  })

只会在请求成功才会返回一个 HttpResponse 类型,因此,我们可以大胆判断是否来源于 HttpResponse 来表示HTTP请求已经成功。

这里,统一对业务层级的错误 code !== 0 产生一个错误信号的 Observable。反之,产生一个成功的信息。

catch

catch 来捕获非200以外的其他状态码的错误,比如:401。同时,前面的 mergeMap 所产生的错误信号,也会在这里被捕获到。

.catch((res: HttpResponse<any>) => {
  switch (res.status) {
    case 401:
      // 权限处理
      location.href = ''; // 重新登录
      break;
    case 200:
      // 业务层级错误处理
      alert('业务错误:' + res.body.code);
      break;
    case 404:
      alert('API不存在');
      break;
  }
  return Observable.throw(res);
})

4、完整代码

至此,拦截器所要包括的身份认证token、统一响应处理、异常处理都解决了。

@Injectable()
export class JWTInterceptor implements HttpInterceptor {

  constructor(private notifySrv: NotifyService) {}

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpSentEvent | HttpHeaderResponse | HttpProgressEvent | HttpResponse<any> | HttpUserEvent<any>> {
    console.log('interceptor')
    const jwtReq = req.clone({
      headers: req.headers.set('token', 'asdf')
    });
    return next
      .handle(jwtReq)
      .mergeMap((event: any) => {
        if (event instanceof HttpResponse && event.body.code !== 0) {
          return Observable.create(observer => observer.error(event));
        }
        return Observable.create(observer => observer.next(event));
      })
      .catch((res: HttpResponse<any>) => {
        switch (res.status) {
          case 401:
            // 权限处理
            location.href = ''; // 重新登录
            break;
          case 200:
            // 业务层级错误处理
            this.notifySrv.error('业务错误', `错误代码为:${res.body.code}`);
            break;
          case 404:
            this.notifySrv.error('404', `API不存在`);
            break;
        }
        // 以错误的形式结束本次请求
        return Observable.throw(res);
      })
  }
}

发现没有,我们并没有加一大堆并不认识的事物,单纯都只是对数据流的各种操作而已。

NotifyService 是一个无须依赖HTML模板、极简Angular通知组件。

5、注册拦截器

拦截器构建后,还需要将其注册至 HTTP_INTERCEPTORS 标识符中。

import { HttpClientModule } from '@angular/common/http';

@NgModule({
  imports: [
    HttpClientModule
  ],
  providers: [
    { provide: HTTP_INTERCEPTORS, useClass: JWTInterceptor, multi: true}
  ]
})

以上是拦截器的所有内容,在不改变原有的代码的情况下,我们只是利用短短几行的代码实现了身份认证所需要的TOKEN、业务级统一响应处理、错误处理动作。

四、async 管道

一个 Observable 必须被订阅以后才会真正的开始动作,前面在HTML模板中我们利用了 async 管道简化了这种订阅过程。

{{ user | async | json }}

它相当于:

let user: User;
get() {
  this.http.get<User>('/assets/data/user.json').subscribe(res => {
    this.user = res;
  });
}
{{ user | json }}

然而,async 这种简化,并不代表失去某些自由度,比如说当在获取数据过程中显示【加载中……】,怎么办?

<div *ngIf="user | async as user; else loading">
  {{ user | json }}
</div>
<ng-template #loading>加载中……</ng-template>

恩!

五、结论

Angular在HTTP请求过程中使用 Observable 异步数据流控制数据,而利用 rxjs 提供的大量操作符,来改变最终值;从而获得在应用层面最优雅的编码风格。

当我们说到优雅使用HTTP这件事时,易测试是一个非常重要,因此,我建议将HTTP从组件类中剥离并将所有请求放到 Service 当中。当对某个组件编写测试代码时,如果受到HTTP请求结果的限制会让测试更困难。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
通过隐藏option实现select的联动效果
Nov 10 Javascript
javascript在事件监听方面的兼容性小结
Apr 07 Javascript
jquery查找父元素、子元素(个人经验总结)
Apr 09 Javascript
JavaScript onkeypress事件入门实例(按下或按住一个键盘按键)
Oct 17 Javascript
jquery实现公告翻滚效果
Feb 27 Javascript
js实现鼠标点击左上角滑动菜单效果代码
Sep 06 Javascript
javascript鼠标右键菜单自定义效果
Dec 08 Javascript
js实现拖拽上传图片功能
Aug 01 Javascript
js实现随机8位验证码
Jul 24 Javascript
JS 设计模式之:工厂模式定义与实现方法浅析
May 06 Javascript
vue内置组件component--通过is属性动态渲染组件操作
Jul 28 Javascript
javascript使用canvas实现饼状图效果
Sep 08 Javascript
JS+HTML5 canvas绘制验证码示例
Dec 05 #Javascript
详解关于Angular4 ng-zorro使用过程中遇到的问题
Dec 05 #Javascript
JS实现简单的点赞与踩功能示例
Dec 05 #Javascript
node.js实现为PDF添加水印的示例代码
Dec 05 #Javascript
vue组件之间通信实例总结(点赞功能)
Dec 05 #Javascript
JS获取今天是本月第几周、本月共几周、本月有多少天、是今年的第几周、是今年的第几天的示例代码
Dec 05 #Javascript
JS获取月的第几周和年的第几周实例代码
Dec 05 #Javascript
You might like
PHP设计模式之装饰者模式
2012/02/29 PHP
php中替换字符串中的空格为逗号','的方法
2014/06/09 PHP
使用PHP生成二维码的方法汇总
2015/07/22 PHP
php实现替换手机号中间数字为*号及隐藏IP最后几位的方法
2016/11/16 PHP
PHP7 新增功能
2021/03/09 PHP
JavaScript 学习笔记一些小技巧
2010/03/28 Javascript
js function定义函数的几种不错方法
2014/02/27 Javascript
jQuery菜单插件用法实例
2015/07/25 Javascript
百度地图API之本地搜索与范围搜索
2015/07/30 Javascript
js支持键盘控制的左右切换立体式图片轮播效果代码分享
2015/08/26 Javascript
利用jQuery实现漂亮的圆形进度条倒计时插件
2015/09/30 Javascript
第二次聊一聊JS require.js模块化工具的基础知识
2016/04/17 Javascript
全面理解JavaScript中的闭包
2016/05/12 Javascript
JavaScript基础知识点归纳(推荐)
2016/07/09 Javascript
JS实现遍历不规则多维数组的方法
2018/03/21 Javascript
微信小程序实现图片上传放大预览删除代码
2020/06/28 Javascript
vue解决使用webpack打包后keep-alive不生效的方法
2018/09/01 Javascript
javascript自定义右键菜单插件
2019/12/16 Javascript
一文秒懂nodejs中的异步编程
2021/01/28 NodeJs
[00:47]DOTA2荣耀之路6:天火,天火!
2018/05/30 DOTA
详解python中的json的基本使用方法
2016/12/21 Python
python检测IP地址变化并触发事件
2018/12/26 Python
python 二维矩阵转三维矩阵示例
2019/11/30 Python
python GUI库图形界面开发之PyQt5简单绘图板实例与代码分析
2020/03/08 Python
strlen的几种不同实现方法
2013/05/31 面试题
高等教育学自荐书范文
2014/02/10 职场文书
秋天的怀念教学反思
2014/04/28 职场文书
调查研究项目计划书
2014/04/29 职场文书
2014离婚协议书范文
2014/09/10 职场文书
清明节扫墓活动总结
2015/02/09 职场文书
建筑安全员岗位职责
2015/02/15 职场文书
《迟到》教学反思
2016/02/24 职场文书
vue实现简单数据双向绑定
2021/04/28 Vue.js
win10+anaconda安装yolov5的方法及问题解决方案
2021/04/29 Python
2022新作动画《福星小子》释出宣传影片 加入内田真礼&宫野真守配音演出
2022/04/08 日漫
python使用BeautifulSoup 解析HTML
2022/04/24 Python