在前面的章节中,我们学习父子组件传值的两种方法。当前的需求是将请求发生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) |