小程序实现一个事件中心
# 前言
在开发小程序的过程中遇到了这么一个场景,业务中会有许多操作需要用户当前是已登录的状态才可以进行,例如某些页面的跳转,亦或者下单等需要用户信息的操作。目前我们的做法是跳转到登录页完成登录后再返回到当前页面并保持静止。这也是市面上大多数产品的处理方式,为了更丝滑的体验,我想实现一个登录完成之后自动去执行用户上一个触发登录的操作,以此来免去用户登陆完成之后需要重复执行那个操作的尴尬。由此诞生了本篇文章
# 思路
既然是要自动触发上一次的操作,那就必须得知道用户上次点了什么,是什么行为触发的跳转到登录页。而我们需要做的其实就是保存下这个事件并且在返回这个页面之后
自动去执行
这个事件
import {getCurrentPage} from "@/plugin/nav";
import {localToken} from "@/utils/local";
class SelfExecutingEventCenter{
constructor() {
this.eventBus = new Map()
}
// 添加事件
addEvent(event){
const {route} = getCurrentPage()
this.eventBus.set(route,event)
}
// 触发事件
triggerEvent(){
const {route} = getCurrentPage()
const isLogin = !!localToken.get()
if(!isLogin){
this.delEvent(route)
return
}
this.eventBus.has(route)&&this.eventBus.get(route)()
this.delEvent(route)
}
delEvent(key){
this.eventBus.delete(key)
}
}
export default SelfExecutingEventCenter
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
代码其实很简单,就是维护了一个事件中心,提供一个添加事件的api
和一个触发事件的api
,这里我们用添加事件的页面的path
作为key
来存储事件,以防止映射关系错乱。
在触发事件的api
中取出当前的路由path
,判断事件中心是否有待执行的事件 有的话就去执行这个事件,执行完毕删除即可。
# 使用
下面来看看如何使用
先初始化这个事件中心并挂载到全局
// main.js
import Vue from 'vue'
import SelfExecutingEventCenter from '@/url/SelfExecutingEventCenter'
Vue.propoty.$SelfExecutingEventCenter = new SelfExecutingEventCenter()
2
3
4
5
6
7
首先触发事件我们需要找到一个只要进入当前页面就会执行的钩子,在小程序中 onShow
满足这个条件。
<!--page.vue-->
onShow(){
this.$SelfExecutingEventCenter.triggerEvent()
}
2
3
4
5
6
这样每次触发页面的onshow
我们都会去实践中心检索是否有待执行的函数,有的话就执行这个函数。
接下来就是收集待执行的函数
handleGoPay() {
if (!localToken.get()) {
redirectToLogin()
return
}
uni.navigateTo({
url: `/pagesA/payment/index`,
})
}
2
3
4
5
6
7
8
9
上述函数中 需要判断登录态才能跳转到payment
页面,所以我们可以在这里进行事件的收集
handleGoPay() {
if (!localToken.get()) {
redirectToLogin()
// 收集待执行的事件
this.$SelfExecutingEventCenter.addEvent(()=>this.handleGoPay())
return
}
uni.navigateTo({
url: `/pagesA/payment/index`,
})
}
2
3
4
5
6
7
8
9
10
11
这里传入箭头函数是因为有些事件会需要传入参数,通过箭头函数的形式我们可以在箭头函数内闭包引用待执行函数所需的参数,这样省去了我们对参数的透传处理。
以上就是一个完整的使用案例。
# 场景二:跨页面通信
在开发过程中不仅需要掌握组件之间的通信,跨页面的通信通常也是一个很重要的点。一般来说跨页面的通信我们会采用全局的缓存
,或者采用vuex
等全局状态管理。但有些场景下我们可能只是想通知下一个页面或者上一个页面干某些事情,这个时候如果要通过vuex
或者缓存去实现我们通常需要创建一个变量,在通知方存下 ➡ 执行方取出 ➡ 判断是否执行 ➡ 然后删除这个变量
。
这种方式用起来属实不太方便,要是忘记删除这个开关变量了还会导致一些难以排查的问题,受vue eventBus
的启发,这里我也自行实现了一个发布订阅模式的全局事件总线。
根据上面一个案例我们可以写出一个精简版
class MessageChannel {
constructor() {
this.subscribers = {}
}
/**
* 订阅函数
* @param topic
* @param subscriber
*/
on(topic,subscriber){
if (this.subscribers[topic]){
this.subscribers[topic].push(subscriber)
}else{
this.subscribers[topic] = [subscriber]
}
}
/**
* 移除订阅事件
* @param 一个参数都不传入则移除所有的订阅事件
* @param topic 只传入事件名则移除这个事件名下的所有事件
* @param subscriber 同时传入事件名和对应事件 则移除这个对应事件,这个事件必须和订阅时传入的事件相同
*/
off(topic,subscriber){
if(!topic&&!subscriber){
this.subscribers = {}
}else if(topic&&!subscriber){
this.subscribers[topic] = []
}else if (topic&&subscriber){
const events = this.subscribers[topic]||[]
this.subscribers[topic] = events.filter(sub => sub !== subscriber);
}
}
emit(topic,data){
const events = this.subscribers[topic]||[]
// 发布所有事件
events.forEach((event,index)=>{
if(event&&typeof event === "function"){
event(data)
}
})
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
简易版的我们实现了发布事件 订阅事件以及移除事件 三个功能,为了使他的功能更加完善,我们可以再加入一个只订阅一次的方法,执行完之后自动销毁。代码改造如下
/**
* 订阅一次
* @param topic
* @param subscriber
*/
once(topic,subscriber){
if (this.subscribers[topic]){
this.subscribers[topic].push({
e:subscriber,
once:true
})
}else{
this.subscribers[topic] = [{
e:subscriber,
once:true
}]
}
}
emit(topic,data){
const events = this.subscribers[topic]||[]
let onceIdx = null
// 发布所有事件
events.forEach((event,index)=>{
if(event&&typeof event === "function"){
event(data)
}else if(event&&event.once){
event.e(data)
onceIdx = index
}
})
// 如果是只订阅一次的 在出发完后就删除这个事件
if(onceIdx!==null){
events.splice(onceIdx,1)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
上述代码中我们新增了一个只订阅一次的api
,并且在发布函数里对这个api
进行了兼容,如果是只订阅一次的 发布完消息就把他删除
现在这个类看似就已经完善了,但是在使用过程中遇到了一个问题。
- 问题:
- 如果是
A
页面跳转到B
页面,再从B
页面回到A
页面的时候需要A
页面执行某些操作。那我们就应该在B
页面发布消息,A
页面订阅消息。但是这和我们之前写的逻辑就相悖了,按我们之前的设计:应该是先订阅消息 然后发布的时候回去触发这些订阅消息的回调函数。但此处我们的场景是先发布消息再订阅消息。这里就引入了一个新概念:已过期的消息
- 如果是
提示
什么是已过期的消息呢?就是事件中心先发布了这条消息,但是订阅者没有及时订阅,但他又希望再他订阅的时候能获取到这条旧消息。这也就对应了我们上面的使用场景,B
页面发布了消息,A
页面后来才订阅,但是也需要拿到B
页面发布的这条消息。
那怎么去实现这个功能呢?
其实也很简单,此处我们需要改造一下事件池的数据结构: 原先我们的数据结构如下
this.subscribers = {
'key':[event,event,event...],
...
}
2
3
4
由于需要能拿到过期的消息,所以我们需要在每次发布完消息后把这个消息的值保存下来
this.subscribers = {
'key':{
value:'',
events:[event,event,event...]
},
...
}
2
3
4
5
6
7
经改造之后的结构我们就可以缓存以过期的值啦,接下来再去改造一下之前的发布函数和订阅函数
class MessageChannel {
constructor() {
this.subscribers = {}
}
/**
* 订阅函数
* @param topic
* @param subscriber
* @param isOld 是否接受之前发布的消息
*/
on(topic,subscriber,isOld = true){
if (this.subscribers[topic]){
this.subscribers[topic].events.push(subscriber)
// 如果存在过期的消息,则自动触发一次回调
if(this.subscribers[topic].value){
subscriber(this.subscribers[topic].value)
}
}else{
this.subscribers[topic] = {
value:'',
events:[subscriber]
}
}
}
/**
* 订阅一次
* @param topic
* @param subscriber
*/
once(topic,subscriber){
if (this.subscribers[topic]){
// 如果存在过期的消息,则自动触发一次回调,因为是订阅一次 所以就不用存入事件池了
if(this.subscribers[topic].value){
subscriber(this.subscribers[topic].value)
}else{
this.subscribers[topic].events.push({
e:subscriber,
once:true
})
}
}else{
this.subscribers[topic] = {
value:'',
events:[{
e:subscriber,
once:true
}]
}
}
}
/**
* 移除订阅事件
* @param 一个参数都不传入则移除所有的订阅事件
* @param topic 只传入事件名则移除这个事件名下的所有事件
* @param subscriber 同时传入事件名和对应事件 则移除这个对应事件,这个事件必须和订阅时传入的事件相同
*/
off(topic,subscriber){
if(!topic&&!subscriber){
this.subscribers = {}
}else if(topic&&!subscriber){
this.subscribers[topic].events = []
}else if (topic&&subscriber){
const events = this.subscribers[topic].events||[]
this.subscribers[topic].events = events.filter(sub => sub !== subscriber);
}
}
emit(topic,data){
// 存储本次事件的值
this.subscribers[topic].value = data
const events = this.subscribers[topic].events||[]
let onceIdx = null
// 发布所有事件
events.forEach((event,index)=>{
if(event&&typeof event === "function"){
event(data)
}else if(event&&event.once){
event.e(data)
onceIdx = index
}
})
// 如果是只订阅一次的 在出发完后就删除这个事件
if(onceIdx!==null){
events.splice(onceIdx,1)
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
这次改动主要是两个地方:
1.发布的时候将这个值保存在事件池对应的key
的value
上
2.订阅的时候先判断当前订阅的key
是否有已发布的值,如果有的话就自执行一次订阅函数,并且将之前缓存的值传递出去。
# 结语
设计模式在日常开发中无处不在,掌握他们可以让我们在面对一些问题的时候处理的更得心应手,本文就是通过发布订阅模式解决了日常开发中跨页面通信的问题,第一个问题的解决方案其实也是发布订阅模式的思想。