在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 网络请求又那么清晰。
(完)