在继续学习如何在前台获取后台返回的token以前,先带领大家了解下RxJS操作符。
可能你还不清楚什么是RxJS,但实际上你早早的就已经用上它了。每写一次`this.httpClient().subscribe()`,你便使用了一次RxJS。
至于RxJS具体是什么,又有着什么样的功能。怕是需要一个单独的教程来使用较长的篇幅来介绍它。而我们在此需要清楚的是RxJS是一种编程的模式(思想),具体来讲是一种面向数据流的编程思想。
# 生活中的数据流
笔者所在的院校至今还保持着订阅报刊的习惯,如果对某些报刊有需求可以选择在开学初向教务员提出订阅申请,在预算允许的情况下。此后每个月(月刊)或每半个月(半月刊)都会收到在开学初订阅的报刊。不仅如何,有时候还会收到一些意想不到的增刊(临时增加印刷的)。而这个报刊订阅便像极了RxJS的数据流。
对笔者而言,教务员便是数据流的来源;而教务员是不提供发行报刊的服务的,所以其订阅报刊时需要找到校级部门,而报刊最终也是由校级部门进行分发;同样的,校级部门向上可以需要找邮局,依次累推最终达到杂志社。下发报刊的过程也相同,杂志社将报刊向下交付,然后逐级的交到笔者的手中。而最终交付的报刊就是刚刚提到的**数据**,多份报刊不定期的由杂志社向读者传递,便是**数据流**。
如果RxJS中的数据流和现实生活中的报刊订阅是一个概念的话,那么我猜想它一定具有以下的性质:
1. 在进行报刊订阅时,由于是逐级向上订阅的,所以订阅过程可能失败(比如领导自认为该报刊不适合订阅或经费不足);那么RxJS的数据订阅过程也必然会面临失败的情况。
2. 报刊发行后,可能由于其内容过于吸引人,可能会被校级或院级教务员扣留,待其阅读后再分发给笔者;那么RxJS的数据发行,也必然可能出现被中间的转发者暂缓扣留而未得到及时转发的可能。
3. 报刊发行后,由于校级教务员的疏忽,可能导致忘记分发给笔者了;那么RxJS也必然存在在发行过程中,数据中断的情况。
4. 基于3,当教务员收到下一期的报刊时,突然发现上一期的还在这呢,此时一并发给笔者;那么RxJS也必然存在将两份数据员合并后发送给读者的情况。
5. 笔者订阅的两份报刊总是先后隔一天送达,教务员感觉两天送两次太难受,所以每次都是等两份报刊到齐了以后再统一发送给笔者;那么RxJS也必然存在订阅了两个数据源后,只有当满足两个数据源全部返回后才下发数据的情况。
6. 报刊发行后,教务员提前浏览的时候,不小心将报刊损坏或不经意的在报刊的内容上进行了批注,然后将损坏或批注的报刊给了笔者;那么RxJS中在进行数据传递的过程中,必然会出现数据被修改后才下发的情况。
7. 报刊发行后,教务员对自己感兴趣的报刊进行阅读后再下发给笔者,而对不感兴趣的报刊选择直接下发给笔者;那么RxJS中必然可以设置某些条件,当满足条件时对数据进行处理,而当不满足条件时直接下发数据。
没错,只要在现实生活中能想到的报刊订阅的情况RxJS均可满足!RxJS中提供了丰富的**操作符**来操作返回的数据流,每个操作符都有着其特殊的功能。此外如果RxJS中提供的操作符不能满足一些对数据流处理的要求,我们还可以自定义自己的操作符。
# 在响应header中获取auth-token
拦截器的本质正是生活中的"教务员",它接收其它订阅者的订阅,并将此后的数据按订阅的情况分发给对应的订阅者,对于"院教务员"而言,报刊订阅反馈到代码中如下:
src/app/core/auth-token-interceptor.ts
```javascript
intercept(req: HttpRequest<any> ➊, next: HttpHandler ➋): Observable<HttpEvent<any>> ➌ {
return➎ next.handle(reqClone) ➍
}
```
* ➊ 向其发起订阅的笔者
* ➋ 用以发起订阅的校教务员
* ➌ 订阅的报刊种类
* ➍ 向校教务员发起订阅
* ➎ 将校教务员送达的报刊发送给笔者
# pipe()
在RxJS中,可以使用pipe()方法处理一些分发的数据。pipe方法接收一个或多个参数,每个参数都是一个**操作符**,被分发的数据在转发以前,依次通过pipe中规定的几个**操作符**。比如:
```javascript
return next.handle(reqClone).pipe(操作符1(传入的数据0) => {处理后的数据1}, 操作符2(传入的数据1) => {处理后的数据2})
```
![](https://img.kancloud.cn/e9/bf/e9bfd21cd06593527c8a110d8d72bfca_558x90.png)
## demo
订阅、发送报刊与订阅、发送数字是一个道理,在此简单展示下RxJS是如何发送由数字组成的数据流以及如何使用pipe操作即将下发的数据流的。
情景:每1秒种发送一个0-100的随机数字,如果生成的是数字尾数是1则终止发送。有三个操作符来影响它,第一个操作符:将得到的数字进行平方,再将平方以后的数字向后发送;第二个操作符:对得到的数据与10进行模运算,将取模后的值向后发送;第三个操作符:如果得到的数据是奇数,则向后发送,否则不发送。则示例代码如下:
src/app/core/auth-token-interceptor.ts
```javascript
import {Observable, Subject} from 'rxjs';
import {filter, map} from 'rxjs/operators';
...
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const sendNumbers = new Subject<number>(); ➊
const pipeNumber = sendNumbers.pipe➋(map➌((x) => {
console.log('1接收到的数字为 ' + x);
return x * x;
}), map(x => {
console.log('2接收到的数字为 ' + x);
return x % 10;
}), filter➍(x => {
console.log('3接收到的数字为 ' + x);
return x % 2 === 0; ➎
}));
pipeNumber.subscribe((value) => {
console.log('接收到了 ' + value);
});
const sendNumber = () => setTimeout(() => { ➏
const n = Math.floor(Math.random() * 100);
console.log('生成的数字是' + n.toString());
sendNumbers.next(n); ➐
if (n % 10 !== 1) {
sendNumber();
}
}, 1000);
sendNumber(); ➑
const reqClone = req.clone({
setHeaders: {'auth-token': 'af1c0c77-67d0-4ec2-8321-2f88e32f76af'}
});
return next.handle(reqClone);
}
```
* ➊ 定义了一个可以发送数字的数据源
* ➋ 使用pipe对接收到的数据进行**操作**
* ➌ map是一个**操作符**,它的作用时:接收数据、处理数字、转发处理后的数据
* ➍ filter也是一个**操作符**,它的作用时:当接收到的值满足其中规定的条件时,则转发数据。否则不转发。
* ➎ 此条件为true时➍转发数据,为false时不转发数据。
* ➏ 每1秒钟发送1个数据
* ➐ 发送数据
运行结果如下:
```
auth-token-interceptor.ts:28 生成的数字是42
auth-token-interceptor.ts:12 1接收到的数字为 42
auth-token-interceptor.ts:15 2接收到的数字为 1764
auth-token-interceptor.ts:18 3接收到的数字为 4
auth-token-interceptor.ts:23 接收到了 4
auth-token-interceptor.ts:28 生成的数字是65
auth-token-interceptor.ts:12 1接收到的数字为 65
auth-token-interceptor.ts:15 2接收到的数字为 4225
auth-token-interceptor.ts:18 3接收到的数字为 5
auth-token-interceptor.ts:28 生成的数字是74
auth-token-interceptor.ts:12 1接收到的数字为 74
auth-token-interceptor.ts:15 2接收到的数字为 5476
auth-token-interceptor.ts:18 3接收到的数字为 6
auth-token-interceptor.ts:23 接收到了 6
auth-token-interceptor.ts:28 生成的数字是95
auth-token-interceptor.ts:12 1接收到的数字为 95
auth-token-interceptor.ts:15 2接收到的数字为 9025
auth-token-interceptor.ts:18 3接收到的数字为 5
auth-token-interceptor.ts:28 生成的数字是91
auth-token-interceptor.ts:12 1接收到的数字为 91
auth-token-interceptor.ts:15 2接收到的数字为 8281
auth-token-interceptor.ts:18 3接收到的数字为 1
```
# 获取auth-token
终于到了使用pipe来获取token的时候了:
src/app/core/auth-token-interceptor.ts
```javascript
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const reqClone = req.clone({
setHeaders: {'auth-token': 'af1c0c77-67d0-4ec2-8321-2f88e32f76af'}
});
return next.handle(reqClone).pipe➊(map➋((httpEvent) => {
console.log(httpEvent); ➌
return httpEvent;
}));
}
```
* ➊ 使用pipe设置操作数据的操作符
* ➋ map操作符的作用的是:将return的结果进行转发
* ➌ 打印个日志看看
控制台显示该变量的值类型为HttpResponse,对应的基本信息如下:
```
{
"headers": {
"normalizedNames": {},
"lazyUpdate": null
},
"status": 200,
"statusText": "OK",
"url": "http://localhost:8080/Teacher/",
"ok": true,
"type": 4,
"body": [
{
"id": 1,
"name": "panjie",
"sex": false,
"username": "yqac",
"email": "3792535@qq.com",
"createTime": 0,
"updateTime": 0
}
]
}
```
近一步进行类型转化来获取auth-token的值。
src/app/core/auth-token-interceptor.ts
```javascript
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const reqClone = req.clone({
setHeaders: {'auth-token': 'af1c0c77-67d0-4ec2-8321-2f88e32f76af'}
});
return next.handle(reqClone).pipe(map((httpEvent) => {
if (httpEvent instanceof HttpResponse) { ➊
const httpResponse = httpEvent as HttpResponse<any>; ➋
const authToken = httpResponse.headers.get('auth-token');
console.log('获取到的authToken为' + authToken);
}
return httpEvent;
}));
}
```
* ➊ 使用if进行类型判断,以防止异常(HttpEvent不止HttpResponse一种类型)
* ➋ 类型转换
* ➌ 获取authToken
测试:
![](https://img.kancloud.cn/ec/aa/ecaa06331f1e34cd57c4e0109eb0423c_313x71.png)
很失望,竟然获取到了null,查看网络确认是否真的返回了auth-token呢?
![](https://img.kancloud.cn/c0/21/c02115ded23e1568ffb0809c9d9070f9_468x312.png)
诡异的事情发生了,在控制台中明明返回了auth-token,但为什么获取不到呢?莫非`httpResponse.headers.get('auth-token');`这个写法不正确吗?答案并不是这样的。`httpResponse.headers.get('auth-token');`这个写法完全正确,之所以在此时获取不到`auth-token`是由浏览器的安全策略决定的。为了某些安全方面的问题,浏览器仅允许js获`Cache-Control`,`Content-Language`,`Content-Type`,`Expires`,`Last-Modified`以及`Pragma`几个header信息。而若要获取其它的header的信息,则需要由在header中返回`Access-Control-Expose-Headers 访问控制允许暴露的headers`来指定。但当前的后台并未指定允许暴露的任何header信息,所以此时获取`auth-token`是不被浏览器所允许的。
解决的方法是打开后台,设置`Access-Control-Expose-Headers`并将`auth-token`加入其中。
config/WebConfig.java
```java
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://localhost:4200")
.allowedMethods("PUT", "DELETE", "POST", "GET", "PATCH")
.exposedHeaders("auth-token"); ➊
}
```
* ➊ 设置允许前台获取的额外header信息
重新启动后台后再次测试:
![](https://img.kancloud.cn/c1/c0/c1c0800f5fe22144e67d105dcfa7f183_529x110.png)
# 缓存auth-token
行百里者,半九十。最后建立缓存服务把获取到的token信息缓存起来,以便在下起发起请求的时候,加入此auth-token信息。这样便能够达到在非首次请求中使用首次请求获取到的auth-token的目的。
```
panjiedeMac-Pro:web-app panjie$ cd src/app/service/
panjiedeMac-Pro:service panjie$ ng g s cache
CREATE src/app/service/cache.service.spec.ts (328 bytes)
CREATE src/app/service/cache.service.ts (134 bytes)
```
初始化如下:
src/app/service/cache.service.ts
```javascript
import {Injectable} from '@angular/core';
/**
* 缓存
*/
@Injectable({
providedIn: 'root'
})
export class CacheService {
/** 认证令牌 */
private static authToken: string = undefined;
constructor() {
}
static setAuthToken(token: string) {
CacheService.authToken = token;
}
static getAuthToken() {
if (CacheService.authToken === undefined) {
return '';
}
return CacheService.authToken;
}
}
```
在拦截器中使用缓存
src/app/core/auth-token-interceptor.ts
```javascript
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const reqClone = req.clone({
setHeaders: {'auth-token': CacheService.getAuthToken()➊}
});
return next.handle(reqClone).pipe(map((httpEvent) => {
if (httpEvent instanceof HttpResponse) {
const httpResponse = httpEvent as HttpResponse<any>;
const authToken = httpResponse.headers.get('auth-token');
CacheService.setAuthToken(authToken)➋;
}
return httpEvent;
}));
}
```
先后点击次教师管理进行测试:
![](https://img.kancloud.cn/fc/df/fcdfc9685189147b27eea0824cf1952d_765x561.gif)
如上图所示:
首次访问时传入了空的auth-token,接收到了后台分发的auth-token: `70fb8229-7cf9-4326-b5a6-dbe911bf8e51`。进行第二次访问时,前台自动携带了该auth-token向后台发起了请求,同时后台在响应将发起的请示的token原值返回。这符合我们设定的令牌认证逻辑:
![](https://img.kancloud.cn/2a/cd/2acdc94ee4440132f4a062dbf7232b9b_809x471.png)
# 参考文档
| 名称 | 链接 | 预计学习时长(分) |
| --- | --- | --- |
| 源码地址 | [https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step5.2.5](https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step5.2.5) | - |
| Access-Control-Expose-Headers | [https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Headers) | - |
|RxJS 库 | [https://angular.cn/guide/rx-library](https://angular.cn/guide/rx-library) | 15 |
- 序言
- 第一章:Hello World
- 第一节:Angular准备工作
- 1 Node.js
- 2 npm
- 3 WebStorm
- 第二节:Hello Angular
- 第三节:Spring Boot准备工作
- 1 JDK
- 2 MAVEN
- 3 IDEA
- 第四节:Hello Spring Boot
- 1 Spring Initializr
- 2 Hello Spring Boot!
- 3 maven国内源配置
- 4 package与import
- 第五节:Hello Spring Boot + Angular
- 1 依赖注入【前】
- 2 HttpClient获取数据【前】
- 3 数据绑定【前】
- 4 回调函数【选学】
- 第二章 教师管理
- 第一节 数据库初始化
- 第二节 CRUD之R查数据
- 1 原型初始化【前】
- 2 连接数据库【后】
- 3 使用JDBC读取数据【后】
- 4 前后台对接
- 5 ng-if【前】
- 6 日期管道【前】
- 第三节 CRUD之C增数据
- 1 新建组件并映射路由【前】
- 2 模板驱动表单【前】
- 3 httpClient post请求【前】
- 4 保存数据【后】
- 5 组件间调用【前】
- 第四节 CRUD之U改数据
- 1 路由参数【前】
- 2 请求映射【后】
- 3 前后台对接【前】
- 4 更新数据【前】
- 5 更新某个教师【后】
- 6 路由器链接【前】
- 7 观察者模式【前】
- 第五节 CRUD之D删数据
- 1 绑定到用户输入事件【前】
- 2 删除某个教师【后】
- 第六节 代码重构
- 1 文件夹化【前】
- 2 优化交互体验【前】
- 3 相对与绝对地址【前】
- 第三章 班级管理
- 第一节 JPA初始化数据表
- 第二节 班级列表
- 1 新建模块【前】
- 2 初识单元测试【前】
- 3 初始化原型【前】
- 4 面向对象【前】
- 5 测试HTTP请求【前】
- 6 测试INPUT【前】
- 7 测试BUTTON【前】
- 8 @RequestParam【后】
- 9 Repository【后】
- 10 前后台对接【前】
- 第三节 新增班级
- 1 初始化【前】
- 2 响应式表单【前】
- 3 测试POST请求【前】
- 4 JPA插入数据【后】
- 5 单元测试【后】
- 6 惰性加载【前】
- 7 对接【前】
- 第四节 编辑班级
- 1 FormGroup【前】
- 2 x、[x]、{{x}}与(x)【前】
- 3 模拟路由服务【前】
- 4 测试间谍spy【前】
- 5 使用JPA更新数据【后】
- 6 分层开发【后】
- 7 前后台对接
- 8 深入imports【前】
- 9 深入exports【前】
- 第五节 选择教师组件
- 1 初始化【前】
- 2 动态数据绑定【前】
- 3 初识泛型
- 4 @Output()【前】
- 5 @Input()【前】
- 6 再识单元测试【前】
- 7 其它问题
- 第六节 删除班级
- 1 TDD【前】
- 2 TDD【后】
- 3 前后台对接
- 第四章 学生管理
- 第一节 引入Bootstrap【前】
- 第二节 NAV导航组件【前】
- 1 初始化
- 2 Bootstrap格式化
- 3 RouterLinkActive
- 第三节 footer组件【前】
- 第四节 欢迎界面【前】
- 第五节 新增学生
- 1 初始化【前】
- 2 选择班级组件【前】
- 3 复用选择组件【前】
- 4 完善功能【前】
- 5 MVC【前】
- 6 非NULL校验【后】
- 7 唯一性校验【后】
- 8 @PrePersist【后】
- 9 CM层开发【后】
- 10 集成测试
- 第六节 学生列表
- 1 分页【后】
- 2 HashMap与LinkedHashMap
- 3 初识综合查询【后】
- 4 综合查询进阶【后】
- 5 小试综合查询【后】
- 6 初始化【前】
- 7 M层【前】
- 8 单元测试与分页【前】
- 9 单选与多选【前】
- 10 集成测试
- 第七节 编辑学生
- 1 初始化【前】
- 2 嵌套组件测试【前】
- 3 功能开发【前】
- 4 JsonPath【后】
- 5 spyOn【后】
- 6 集成测试
- 7 @Input 异步传值【前】
- 8 值传递与引入传递
- 9 @PreUpdate【后】
- 10 表单验证【前】
- 第八节 删除学生
- 1 CSS选择器【前】
- 2 confirm【前】
- 3 功能开发与测试【后】
- 4 集成测试
- 5 定制提示框【前】
- 6 引入图标库【前】
- 第九节 集成测试
- 第五章 登录与注销
- 第一节:普通登录
- 1 原型【前】
- 2 功能设计【前】
- 3 功能设计【后】
- 4 应用登录组件【前】
- 5 注销【前】
- 6 保留登录状态【前】
- 第二节:你是谁
- 1 过滤器【后】
- 2 令牌机制【后】
- 3 装饰器模式【后】
- 4 拦截器【前】
- 5 RxJS操作符【前】
- 6 用户登录与注销【后】
- 7 个人中心【前】
- 8 拦截器【后】
- 9 集成测试
- 10 单例模式
- 第六章 课程管理
- 第一节 新增课程
- 1 初始化【前】
- 2 嵌套组件测试【前】
- 3 async管道【前】
- 4 优雅的测试【前】
- 5 功能开发【前】
- 6 实体监听器【后】
- 7 @ManyToMany【后】
- 8 集成测试【前】
- 9 异步验证器【前】
- 10 详解CORS【前】
- 第二节 课程列表
- 第三节 果断
- 1 初始化【前】
- 2 分页组件【前】
- 2 分页组件【前】
- 3 综合查询【前】
- 4 综合查询【后】
- 4 综合查询【后】
- 第节 班级列表
- 第节 教师列表
- 第节 编辑课程
- TODO返回机制【前】
- 4 弹出框组件【前】
- 5 多路由出口【前】
- 第节 删除课程
- 第七章 权限管理
- 第一节 AOP
- 总结
- 开发规范
- 备用