在前面的章节中,我们学习父子组件传值的两种方法。当前的需求是将请求发生401的错误信息及时地通知`Index`组件。而`UnAuthInterceptor`本身就不是个组件,所以前面学习过的组件传值的方法并不适用于当前需求。
在Angular中如果需要将值在不同的单元(组件、指令、过滤器、拦截器等)传递,应该使用单例模式的Service。
我们当前大概在实现这么一张图:
![image-20210409175249812](https://img.kancloud.cn/27/da/27da648a7f3fe6c283a73c101596bdd3_1770x248.png)
## 单例模式
单例模式是继观察者模式、依赖注入模式后我们学习的又一新的模式。简单来说如果某个单元是单例的,则表式在整个应用,这个单元至多存在一个。
程序中的单例,往往具有**共享**的特性,比如同班同时共享着一个C语言老师,所以对于该班同学而言,C语言老师是单例的;程序中的单例,在同一时间仅能服务一个对象,比如当张三问C语言老师问题时,李四则只能等待;程序中的单例,将被所有的对象共享状态,比如张三惹老师生气后,老师在与李四接触时,还保持着生气的状态。
单例模式被丰富地应用于各种框架中,其最大的优点就是节约资源。比如我们被一个班级配备一个班主任,那个这个班主任就是在节约资源下的单例(试想下为每位同学配备一位班主任将是怎么的情景)。
在Angular中,有一个单元被称为Service,该Serivce在整个应用中便是单例的。它符合单例模式的所有特性:
- 有且至多有一个
- 被其它单元共享
- 共享其状态
- 节约资源
## 初始化
初始化Service与初始化其它的Angular单元相同,同样是使用`ng g`命令,为此我们在`src/app`文件夹中新建service文件夹,并初始化一个`AuthService`:
```bash
panjie@panjies-iMac app % pwd
/Users/panjie/github/mengyunzhi/angular11-guild/first-app/src/app
panjie@panjies-iMac app % mkdir service
panjie@panjies-iMac app % cd service
panjie@panjies-iMac service % pwd
/Users/panjie/github/mengyunzhi/angular11-guild/first-app/src/app/service
panjie@panjies-iMac service % ng g s auth
CREATE src/app/service/auth.service.spec.ts (347 bytes)
CREATE src/app/service/auth.service.ts (133 bytes)
```
如此我们便快速的创建第一个service:
```typescript
import {Injectable} from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class AuthService {
constructor() {
}
}
```
## 注入Service
有了service,便可以向注入HttpClient一样,将其注入到任意我们想注入的位置了,比如当前想交其注入到`UnAuthInterceptor`中:
```typescript
+++ b/first-app/src/app/un-auth.interceptor.ts
@@ -7,11 +7,13 @@ import {
} from '@angular/common/http';
import {Observable, throwError} from 'rxjs';
import {catchError} from 'rxjs/operators';
+import {AuthService} from './service/auth.service';
@Injectable()
export class UnAuthInterceptor implements HttpInterceptor {
- constructor() {
+ constructor(private authService: AuthService) {
+ console.log('authService注入成功', authService);
}
intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
```
在控制台中成功的打印了注入的`AuthService`
![image-20210409173247874](https://img.kancloud.cn/55/34/553449de94d3f93e8975528de1347bf0_990x78.png)
此时回想一下注入`HttpClient`的过程,上述注入是不是显得过于简单了?
同样,在组件中也可以非常轻松的注入服务:
```typescript
+++ b/first-app/src/app/index/index.component.ts
@@ -1,5 +1,6 @@
import {Component, OnInit} from '@angular/core';
import {Teacher} from '../entity/teacher';
+import {AuthService} from '../service/auth.service';
@Component({
selector: 'app-index',
@@ -10,7 +11,8 @@ export class IndexComponent implements OnInit {
login = false;
- constructor() {
+ constructor(private authService: AuthService) {
+ console.log('index组件成功注入authService', authService);
}
ngOnInit(): void {
```
![image-20210409175755650](https://img.kancloud.cn/36/b1/36b1faa4ef52d14715c420ba0b0b96d2_1106x82.png)
当前服务与组件的配合情况如下图所示:
![image-20210409175617703](https://img.kancloud.cn/3e/02/3e0284bea41997f106638c4e6feff8c4_1748x362.png)
为了证明拦截器与组件中的`AuthService`的确是一个,我们在`AuthService`中增加一个属性,并在构造函数中使用随机值来设置该属性:
```typescript
+++ b/first-app/src/app/service/auth.service.ts
@@ -4,7 +4,8 @@ import {Injectable} from '@angular/core';
providedIn: 'root'
})
export class AuthService {
-
+ private key;
constructor() {
+ this.key = Math.random();
}
}
```
查看控制台:
![image-20210409180228511](https://img.kancloud.cn/41/ca/41caaa6de90b0c3db4d83734d218a5d5_1800x444.png)
构造函数仅被调用1次,在组件及拦截器中打印的`key`值是相同的,这充分的说明了拦截器及组件中的AuthService的确就是一个。
## 再谈DI
为什么就这么简单的注入成功了呢?再彻底的弄明白这个问题,还需要复习一下这张图:
![image-20210228173649880](https://img.kancloud.cn/03/03/03032b8d25dc3ab8bf395f7c3c1dc124_2126x656.png)
上图展现了`App组件`成功注入`HttpClient`的过程。之所以注入成功,是由于当前模块的`imports`中引入了`HttpClientModule` ,而`HttpClientModule`有提供`HttpClient`的能力。
而当前代码并未为任何模块声明任何能力,为何能注入成功呢?这是由于Angular有个叫做`root`的根模块,而所有的模块原则上都属于根模块的子模块,所以所有的子模块都可以无条件的使用`root`根模块上的资源:
![image-20210409180550615](https://img.kancloud.cn/ec/b1/ecb134829c80e95e8b65b862ec9e168c_1864x392.png)
AuthService之所有能够成为`root`根模块可以提供的资源,本质上是由以下代码决定的:
```typescript
import {Injectable} from '@angular/core';
@Injectable①({
providedIn②: 'root' 👈
})
export class AuthService {
constructor() {
}
}
```
- ① 表示该服务可被用于注入(本服务是个资源)
- ②`providedIn`标识了可被用于注入的范围
- `root`表示根模块。由于所有的模块都属于根模块,所以`root`也可理解为全部范围。即在当前应用的任意位置上均可注入当前服务
## 观察者模式
服务被直接的注入到两个单元之内后,如何实现:拦截器发送的通知能被组件及时获取呢?我想如果当前三者的关系如下,肯定难不倒你:
![image-20210409180925060](https://img.kancloud.cn/c7/d2/c7d26fcdf360ee0580e03ae4f2ffdcb5_922x368.png)
此时只需要在拦截器调用AuthService的相关方法,然后在AuthService中再调用IndexComponent的相关方法即可。但当前三者的关系却如下图:
![image-20210409175617703](https://img.kancloud.cn/3e/02/3e0284bea41997f106638c4e6feff8c4_1748x362.png)
这时候就需要自定义一个观察者了,我们将上面各个单元简单换个名字,相信你此时应该明了接下来需要做什么了。
![image-20210409181215128](https://img.kancloud.cn/a3/c8/a3c8fee9f323ebe1828c151d6783c99d_1690x356.png)
没错,我们需要将AuthService打造成一个大V,然后使用粉丝一关注这个大V。此时公关公司便可以通过大V向粉丝发送数据了。
### 打造大V
打造在V的方式主要有两种,我们在此使用较简单的。
```typescript
+++ b/first-app/src/app/service/auth.service.ts
@@ -1,10 +1,17 @@
import {Injectable} from '@angular/core';
+import {Subject} from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class AuthService {
private key;
+ /**
+ * Subject是个大V.
+ * 本大V只需要发送一个未认证的通知,并不需要传递具体数据,所以泛型为void
+ */
+ public unAuthSubject = new Subject<void>(); 👈
+
constructor() {
console.log('AuthService构造函数被调用');
this.key = Math.random();
```
- `Subject`是个数据源,该数据源可供订阅,同时也提供了发送新数据的方法。
在了大V以后,开始订阅大V。
### 订阅大V
在`Index`组件中订阅大V,当接收到未认证的通知时,如果当前未显示登录界面,则显示登录界面。
```typescript
+++ b/first-app/src/app/index/index.component.ts
@@ -20,6 +20,13 @@ export class IndexComponent implements OnInit {
if (window.sessionStorage.getItem('login') !== null) {
this.login = true;
}
+ this.authService.unAuthSubject
+ .subscribe(() => {
+ console.log('接收到未认证的通知');
+ if (this.login) {
+ this.login = false;
+ }
+ });
}
```
### 通知大V
此时在拦截器发现401时,及时的通知在V,则可以在`Index`组件中获取到一个通知:
```typescript
+++ b/first-app/src/app/un-auth.interceptor.ts
@@ -21,6 +21,7 @@ export class UnAuthInterceptor implements HttpInterceptor {
.pipe(catchError(error => {
if (error.status === 401) {
console.log('发生了401错误, 通知应用显示登录界面', error);
+ this.authService.unAuthSubject.next();
}
// 使用throwError()继续向上抛出异常
return throwError(error);
```
此时在模拟半小时后被后台主动注销的情况下刷新页面,则可以在控制台查看到如下信息:
![image-20210409182827213](https://img.kancloud.cn/4b/4a/4b4ac527f99bbce48d182ddd088287cb_1668x184.png)
同时应用跳转到登录界面:
![image-20210305121257833](https://img.kancloud.cn/0a/7b/0a7b7f1208bab18e46c08a1e5692296d_1416x542.png)
至此,一个单例的AuthService便可以的起到了`UnAuthInterceptor`与`IndexComponent`的信息桥梁的作用。以后有类似的需求时,当然也可以通过此模式来实现了。
| 名称 | 链接 |
| ------------------ | ------------------------------------------------------------ |
| 创建可注入的服务类 | [https://angular.cn/guide/dependency-injection#create-an-injectable-service-class](https://angular.cn/guide/dependency-injection#create-an-injectable-service-class) |
| RxJS Subject | [https://cn.rx.js.org/manual/overview.html#h15](https://cn.rx.js.org/manual/overview.html#h15) |
| 本节源码 | [https://github.com/mengyunzhi/angular11-guild/archive/step6.7.2.zip](https://github.com/mengyunzhi/angular11-guild/archive/step6.7.2.zip) |
- 序言
- 第一章 Hello World
- 1.1 环境安装
- 1.2 Hello Angular
- 1.3 Hello World!
- 第二章 教师管理
- 2.1 教师列表
- 2.1.1 初始化原型
- 2.1.2 组件生命周期之初始化
- 2.1.3 ngFor
- 2.1.4 ngIf、ngTemplate
- 2.1.5 引用 Bootstrap
- 2.2 请求后台数据
- 2.2.1 HttpClient
- 2.2.2 请求数据
- 2.2.3 模块与依赖注入
- 2.2.4 异步与回调函数
- 2.2.5 集成测试
- 2.2.6 本章小节
- 2.3 新增教师
- 2.3.1 组件初始化
- 2.3.2 [(ngModel)]
- 2.3.3 对接后台
- 2.3.4 路由
- 2.4 编辑教师
- 2.4.1 组件初始化
- 2.4.2 获取路由参数
- 2.4.3 插值与模板表达式
- 2.4.4 初识泛型
- 2.4.5 更新教师
- 2.4.6 测试中的路由
- 2.5 删除教师
- 2.6 收尾工作
- 2.6.1 RouterLink
- 2.6.2 fontawesome图标库
- 2.6.3 firefox
- 2.7 总结
- 第三章 用户登录
- 3.1 初识单元测试
- 3.2 http概述
- 3.3 Basic access authentication
- 3.4 着陆组件
- 3.5 @Output
- 3.6 TypeScript 类
- 3.7 浏览器缓存
- 3.8 总结
- 第四章 个人中心
- 4.1 原型
- 4.2 管道
- 4.3 对接后台
- 4.4 x-auth-token认证
- 4.5 拦截器
- 4.6 小结
- 第五章 系统菜单
- 5.1 延迟及测试
- 5.2 手动创建组件
- 5.3 隐藏测试信息
- 5.4 规划路由
- 5.5 定义菜单
- 5.6 注销
- 5.7 小结
- 第六章 班级管理
- 6.1 新增班级
- 6.1.1 组件初始化
- 6.1.2 MockApi 新建班级
- 6.1.3 ApiInterceptor
- 6.1.4 数据验证
- 6.1.5 教师选择列表
- 6.1.6 MockApi 教师列表
- 6.1.7 代码重构
- 6.1.8 小结
- 6.2 教师列表组件
- 6.2.1 初始化
- 6.2.2 响应式表单
- 6.2.3 getTestScheduler()
- 6.2.4 应用组件
- 6.2.5 小结
- 6.3 班级列表
- 6.3.1 原型设计
- 6.3.2 初始化分页
- 6.3.3 MockApi
- 6.3.4 静态分页
- 6.3.5 动态分页
- 6.3.6 @Input()
- 6.4 编辑班级
- 6.4.1 测试模块
- 6.4.2 响应式表单验证
- 6.4.3 @Input()
- 6.4.4 FormGroup
- 6.4.5 自定义FormControl
- 6.4.6 代码重构
- 6.4.7 小结
- 6.5 删除班级
- 6.6 集成测试
- 6.6.1 惰性加载
- 6.6.2 API拦截器
- 6.6.3 路由与跳转
- 6.6.4 ngStyle
- 6.7 初识Service
- 6.7.1 catchError
- 6.7.2 单例服务
- 6.7.3 单元测试
- 6.8 小结
- 第七章 学生管理
- 7.1 班级列表组件
- 7.2 新增学生
- 7.2.1 exports
- 7.2.2 自定义验证器
- 7.2.3 异步验证器
- 7.2.4 再识DI
- 7.2.5 属性型指令
- 7.2.6 完成功能
- 7.2.7 小结
- 7.3 单元测试进阶
- 7.4 学生列表
- 7.4.1 JSON对象与对象
- 7.4.2 单元测试
- 7.4.3 分页模块
- 7.4.4 子组件测试
- 7.4.5 重构分页
- 7.5 删除学生
- 7.5.1 第三方dialog
- 7.5.2 批量删除
- 7.5.3 面向对象
- 7.6 集成测试
- 7.7 编辑学生
- 7.7.1 初始化
- 7.7.2 自定义provider
- 7.7.3 更新学生
- 7.7.4 集成测试
- 7.7.5 可订阅的路由参数
- 7.7.6 小结
- 7.8 总结
- 第八章 其它
- 8.1 打包构建
- 8.2 发布部署
- 第九章 总结