浅谈Angular网络请求

在Angular网络请求是一个最常见的应用之一,下列我将以 ng-alain 项目为基础描述 Angular 网络请求。

注:示例中代码都以简化的形式出现。

写在前面

Angular发起一个请求再简单不过即使用 HttpClient 类的各种方法,然在开始之前我们应退一小步,先从如何构建一个 Restful API 开始,后端的API设计将很大程度决定前后端如何更优雅的开发有着非常大的关键性作用。

一、RESTful API 设计

私以为API的设计分为请求与输出两个部分。而连接二者是依靠URL,关于URL如何更合理的设计可以参考阮一峰-RESTful API 设计指南

这一部分要谈另一个可能大家容易忽略的细节,请求体与返回体规范。这一点淘宝开放平台是一个非常好的典范,例如所有异常返回体:

{
    "sub_msg":"非法参数",
    "code":50,
    "sub_code":"isv.invalid-parameter",
    "msg":"Remote service error"
}

所有这些规则可以由内部自行决议,再比如我们中后台经常使用的是一种方式,所有返回体不管成功与否都包含以下对象:

{
    "msg": "ok",
    "data": null
}

msg 来判断 ok 值表示成功,对于其他值表示允许直接显示给用户错误文本异常文本。

对于提交 POST 请求体的数据格式(content-type)主要两种比较常见:表单格式和JSON格式,二者也可能根据不同场景情况使用特别是文件上传动作;当然对于大部分场景而言 JSON 格式最优先的形式,不管你是使用 Angular 表单的HTML模板或响应式驱动表单都是直接跟JSON打交道。

二、请求流程

在 ng-alain 中,一个完整的 Angular 应用从前端 UI 交互到服务端处理流程是这样的:

1、首次启动 Angular 执行 APP_INITIALIZER
2、UI 组件交互操作;
3、使用 HttpClient 发送请求;
4、触发用户认证拦截器 @delon/auth,统一加入 token 参数;
a、若未存在 token 或已过期中断后续请求,直接跳转至登录页;
5、触发默认拦截器,统一处理前缀等信息;
6、获取服务端返回;
7、触发默认拦截器,统一处理请求异常、业务异常等;
8、数据更新,并刷新 UI。

本文我们不介绍渲染方面,因此 2,6,8 三点将不做介绍。

1、APP_INITIALIZER

应用初始化是在应用启动过程中有且只执行一次,一般来讲我们需要在应用一启动时加载一些数据:应用信息、通用数据字典、用户数据等。

只需要向 APP_INITIALIZER 注册一个带有 Promise 返回值即可;例如:

{
  provide: APP_INITIALIZER,
  useValue: () => new Promise(() => {}),
  multi: true
}

正因为是一个 Promise 异步,我们就可以在这里利用 HttpClient 做网络请求,从而实现在 Angular 启动之前通过网络请求获取一个启用后一开始就需要的数据。

注:当然在这里发起的网络请求拦截器依然有效,若拦截器包含一些用户 Token 的有效性校验而导致跳转至登录页时,可能要小心处理了。

但不管如何最终你想启动 Angular 都必须确保 Promise 正确的调用 resolve()

2、HttpClient

HttpClient 是 Angular 封装了一个简化的 API 来实现 HTTP 客户端功能,例如一个 get 请求:

constructor(http: HttpClient) {
  http.get('/user/1').subscribe((user) => {
    console.log(user);
  });
}

另一个 post 请求:

constructor(http: HttpClient) {
  http.post('/user/1', { a: 1 }).subscribe((user) => {
    console.log(user);
  });
}

所有请求类型返回的结果都是 Observable<any> 类型,意味着不管如果你都必须调用 subscribe 才会真正的发起请求。大多数情况下你可能会觉得很麻烦,但当你需要一些节流或数据转换时就显得 rxjs 的魅力,有关更多细节自行Google rxjs

3、拦截器

拦截网络请求或响应,用于统一处理请求或响应结果数据。并且可以运用多个拦截器且按顺序执行,类似于 Node 中间件。

一个简单示例

只需要简单实现 HttpInterceptor 接口即可:

export class SimpleInterceptor implements HttpInterceptor {
  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<any> {
    const newReq = req.clone();
    return next.handle(newReq).pipe()
  }
}

拦截器返回的结果是一个 Observable 值,这意味着同一个拦截器代码包含着请求和响应两个部分的处理,所有在 Angular 拦截器里并没有明确区分请求和响应处理,这也是 rxjs 的魅力。

使用 req.clone() 克隆一些新的请求体,当然请求体包含着所有 HttpClient 发起数据及参数。例如给所有请求体的 headers 加入用户 Token 值。

const newReq = req.clone({
  setHeaders: { Authorization: `Bearer ${this.token}` },
});

当响应体网络状态码非 401 时,打算跳转至登录页,则:

return next.handle(newReq)
           .pipe(
             catchError(err => {
               if (err.status === 401) {
                 this.injector.get(Router).navigateByUrl('/login');
               }
             })
           )

最后,在模块里注册,若你希望在整个应用有效可以在根模块里注册:

{ provide: HTTP_INTERCEPTORS, useClass: SimpleInterceptor, multi: true },

拦截器顺序

拦截器可以注册在任何模块里,而一个网络请求所经过拦截器从模块向上查找至根模块,若一个模块包含多个拦截器时按代码顺序执行。

三、ng-alain 请求处理

ng-alain 默认装载了两个拦截器:@delon/auth 用户认证和默认拦截器。

1、用户认证

本身是为 ng-alain 脚手架提供的一个用户认证模块,包含主流的 JWT(Json Web Token)和一个相对通用 Simple Web Token,而其核心是对认证过程进一步处理。而通常其核心在于用户 Token 的获取、使用环节。

同时,@delon/auth 并不会关心用户界面是怎么样,只需要当登录成功后将后端返回的数据交给 ITokenService,它会帮你存储在 localStorage(默认) 当中;当发起一个网络请求时,它会在自动在 header(默认) 当中加入相应的 token 信息。

因此,@delon/auth 不限于 ng-alain 脚手架,任何 Angular 项目都可以使用它。

默认装载了 SimpleInterceptor 拦截器,意味者一开始使用 ng-alain 为什么会无缘无故无法正确请求,而是直接抛出异常。

ng-alain 是一个完整且可直接运用项目的脚手架,因此所有默认配置都尽可能生产环境中代码,其实理解这一点很重要,因为大部分一开始总希望使用一个 Hello World 请求来决定是不是真的可以使用。

有关更多细节请参考文档

2、默认拦截器

DefaultInterceptor 拦截器,它是一个默认拦截器示例代码,包含请求体和响应体的处理。

例如当我们统一响应体如下:

{
    "msg": "ok",
    "data": { id: 1, name: "cipchk" }
}

对于 subscribe 结果来说只需要关心 data 部分,因此可以在拦截器进一步转化:

return of(new HttpResponse(Object.assign(event, { body: body.data })));

使在订阅结果时给保持一个最简单有效数据:

http.get('/user/1').subscribe(user => console.log(user));
// output: { id: 1, name: "cipchk" }

更多做法,例如:统一处理异常消息等,可以参考 default.interceptor.ts 的写法。

总结

Angular 网络请求看起来就像一个简化版的 Web 服务,发起的请求经过一道道关卡后,接收响应结果时又经过原先经过的一道道关卡最后交给用户。

当然这一切的本质还是 rxjs 带来的。曾经有人提过为什么 ng-alain 不采用 Redux 形式,但我实在找不到有什么理由要这么做,大部分中后台都以网络请求来完成大部分事务,而 Angular 网络请求又那么清晰。

(完)

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,905评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,140评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,791评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,483评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,476评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,516评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,905评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,560评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,778评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,557评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,635评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,338评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,925评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,898评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,142评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,818评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,347评论 2 342

推荐阅读更多精彩内容