引言
最近帮潘佳琦解决了一个诡异的问题,然后突然发现⾃己对观察者感到迷茫了。
需求是⼀个注销按钮,如果是技术机构登陆,就调用技术机构的注销⽅法,如果是器具用户登陆,就调⽤器具⽤户的注销方法。当然,最优的解决⽅案并不是我下⽂所列的,既然功能不同,那就应该是两个对象。看来我们的⾯向对象运用得还不不够灵活。
原问题解决
问题描述
注销代码如下,只表达思想,别去深究具体的语法:
logout(): void {
this.departmentService.isLogin$.subscribe((isDepartmentLogin) => {
if (isDepartmentLogin) {
this.departmentLogout();
}
});
this.systemService.isLogin$.subscribe((isTechnicalLogin) => {
if (isTechnicalLogin) {
this.technicalLogout();
}
});
}
看着好像没啥错误啊?订阅获取当前登录用户状态,如果departmentService.isLogin$
为true
,表示是器具用户登录,则调用器具用户的注销方法;如果是systemService.isLogin$
为true
,表示是技术机构登录,则调用技术机构的注销方法。
然而,诡异的事情发生了:
- 首次打开系统,登录,登录成功。
- 点击注销,也能注销成功。
- 但是再登录系统,就登不进去了。
也就是说,这个注销方法影响了后续的登录,当时就很懵圈,为什么呢?
后来多打了几条日志,才发现了问题所在:
原因分析
根据Spring
官方的Spring Security and Angular
所述,官方做法是用一个boolean
值来判断当前是否登录,从而进行视图的展示。
潘老师在新系统中也是根据官方的推荐进行实现的:
let isLogin$ = new BehaviorSubject();
login() {
......
this.isLogin$.next(true);
}
logout() {
......
this.isLogin$.next(false);
}
一个可观察的boolean
对象来判断当前用户是否登录,然后main
组件订阅这个值,根据是否登录并结合ngIf
来判断当前应该显示登录组件还是应用组件。
看这个图大家应该就明白了,问题出在了对subscribe
的理解上。
点击注销,发起订阅,当isLogin$
为true
的时候就注销,注销成功。
下次登录时,这个订阅还在呢!然后点击登录,执行登录逻辑,将isLogin$
设置为true
,观察者就通知了订阅者,又执行了一遍注销逻辑。肯定登不上去了。
执行订阅之后,应该获取返回值,再执行取消订阅。
迷茫
所以,这个问题出在本该订阅一次,但是subscribe
是只要订阅过了,在取消订阅之前,我一直是这个可观察对象的观察者。
想到这我就迷茫了,就是我一订阅,一直到我取消订阅之前,这个可观察对象都要维护着观察者的列表。
那我们的网络请求用的不也是Observable
吗?只是此Observable
是由HttpClient
构建好返回给我们的。那我们订阅了也没取消,那它是不是一直维护着这个关系,会不会有性能问题?难道我们之前的用法都错了吗?
这个问题一直困扰了我好多天,知道今天才在Angular
官网上看到相关的介绍,才解决了我的迷茫。
HttpClient.get()
方法正常情况下只会返回一个可观察对象,它或者发出数据,或者发出错误。有些人说它是“一次性完成”的可观察对象。
The HttpClient.get() method normally returns an observable that either emits the data or an error. Some folks describe it as a "one and done" observable.
或许英文文档描述得更准确,one and done
,HttpClient
返回的Observable
对象只执行一次,然后就销毁。也就是所说的执行一次,或者是next
或者是error
,然后执行complete
销毁这个对象。
Angular
应该早就想到了维护观察者带来的性能问题,才设计一次性的观察者对象,我倒是杞人忧天了。
拓展RxJS
一次订阅解决方案
需求不同,有时我们需要一直订阅,有时我们却想只订阅一次,否则就会发生一些很诡异的问题。
但是Observable
只提供了一个subscribe
方法,想订阅一次得手动取消。我想如果Observable
中再添加一个subscribeOnce
的方法,那开发起来会不会比现在更顺手?
Google
了一下,真的有啊!StackOverflow
上这老哥和我一样的想法,有没有类似subscribeOnce
这样式的方法?
回答得很棒,旧版本流式调用使用first
方法,新版本使用pipe
,里面再调用first
方法。
同时这里说了,如果first
的条件不符合时,会自动取消订阅。
Angular
中取消订阅
关于如何在Angular
中最优雅地实现取消订阅,请参考这个问题:Angular/RxJs When should I unsubscribe from Subscription - StackOverflow
八百多个赞,回答得非常好,可惜我看得是一脸懵逼,可能我开发经验不够。您感兴趣可以去看看原回答的实现方式。
如果您有任何的意见或建议,欢迎批评指正。
总结
世上最难的不是学一门技术,而是如何实践。
没有教科书,没有项目参考,全靠一份官方文档,每个人都创造着自己的最佳实践。
一路走来,从华软开始,也得四个月了,到现在也没设计出一款满意的前台架构。
优秀架构师的设计经验,肯定不会分享给你,而普通的Angular
书籍,不过是一个小的Demo
项目,完全没有考虑过项目很庞大的时候应该怎么组织架构。
最近发现了基于Angular
的前台微服务框架Mooa
,最近没时间以后再细看,希望它不要再让我失望。