背景介绍
在各种小程序中,我们经常会遇到 这种情况
有一个 列表,点击列表中的一项进入详情,详情有个按钮,删除了这一项,这个时候当用户返回到列表页时,发现列表中的这一项依然存在,这种情况,就是一个 `bug`,也就是数据不同步问题,这个时候测试小姐姐
肯定会找你,让你解决,这个时候,你也许会很快速的解决,但过一会儿,测试小姐姐又来找你说,我打开了
四五个页面更改了用户状态,但我一层一层返回到首页,发现有好几个页面数据没有刷新,也是一个 bug,
这个时候你就犯愁了,怎么解决,常规方法有下面几种
复制代码
解决方法
1. 将所有请求放到 生命周期 `onShow` 中,只要我们页面重新显示,就会重新请求,数据也会刷新
2. 通过用 `getCurrentPages` 获取页面栈,然后找到对应的 页面实例,调用实例方法,去刷新数据
3. 通过设置一个全局变量,例如 App.globalData.xxx,通过改变这个变量的值,然后在对应 onShow
中检查,如果值已改变,刷新数据
4. 在打开详情页时,使用 redirectTo 而不是 navigateTo,这样在打开新的页面时,会销毁当前页面,
返回时就不会回到这个里面,自然也不会有数据不同步问题
复制代码
存在的问题
1. 假如我们将 所有 请求放到 onShow 生命周期中,自然能解决所有数据刷新问题,但是 onShow
这个生命周期,有两个问题
第一个问题,它其实是在 onLoad 后面执行的,也就是说,假如请求耗时相同,从它发起请求到页面渲染,
会比 onLoad 慢
第二个问题,那就是页面隐藏、调用微信分享、锁频等等都会触发执行,请求放置于 `onShow` 中就会造成
大量不需要的请求,造成服务器压力,多余的资源浪费、也会造成用户体验不好的问题
2. 通过 `getCurrentPages` 获取页面栈,然后找到对应的 页面实例,调用实例方法,去刷新数据,这也
不失为一个办法,但是就如微信官方文档所说
> 不要尝试修改页面栈,会导致路由以及页面状态错误。
> 不要在 App.onLaunch 的时候调用 `getCurrentPages()`,此时 page 还没有生成。
同时、当需要通信的页面有两个、三个、多个呢,这里去使用 `getCurrentPages` 就会比较困难、繁琐
3. 通过设置全局变量的方法,当需要使用的地方比较少时,可以接受,当使用的地方多的时候,维护起来
就会很困难,代码过于臃肿,也会有很多问题
4. 使用 redirectTo 而不是 navigateTo,从用来体验来说,很糟糕,并且只存在一个页面,对于
tab 页面,它也无能为力,不推荐使用
复制代码
最佳实践
在 Vue 中, 可以通过 new Vue() 来实现一个 event bus作为事件总线,来达到事件通知的功能,在各大
框架中,也有自身的事件机制实现,那么我们完全可以通过同样的方法,实现一个事件中心,来管理我们的事件,
同时,解决我们的问题。iny-bus 就是这样一个及其轻量的事件库,使用 typescript 编写,100% 测试覆
盖率,能运行 js 的环境,就能使用
复制代码
传送门
源码
NPM
文档
简单使用
iny-bus 使用及其简单,在需要的页面 onLoad 中添加事件监听, 在需要触发事件的地方派发事件,使监
听该事件的每个页面执行处理函数,达到通信和刷新数据的目的,在小程序中的使用可以参考以下代码
复制代码
import bus from 'iny-bus'
onLoad () {
this.eventId = bus.on('事件名',(a,b,c,d) => {
console.log(a,d)
this.setData({
a,c
}
this.refreshPageData()
})
this.eventIdOnce = bus.once('事件名',() => {
})
}
页面的事件监听,避免多余资源浪费,在添加事件监
onUnload () {
bus.remove('事件名',this.eventId)
}
onClick () {
bus.emit('事件名',a,c)
}
复制代码
更详细的使用和例子可以参考 Github iny-bus 小程序代码
iny-bus 具体实现
- iny-bus 我们是使用 typescript 编写,同时要发布到 npm 上供大家使用,那我们就需要搭建开发环境,选择编辑打包工具,编写发布脚本,具体的细节这里不讲,只列举以下使用到的工具和库
-
基本打包工具,这里使用非常优秀的开源库 typescript-library-starter,具体细节不展开
-
测试工具 使用 facebook 的 jest
-
build ci 使用 [travis-ci](www.travis-ci.org/)
-
测试覆盖率上传使用 codecov
-
具体的其他细节大家可以看源码中的 package.json,这里就一一展开讲了,我们来看具体实现
- 具体实现
- 首先,我们来设计我们的事件中心,iny-bus 作为事件中心,我们就需要一个容器来储存我们的事件,同时我们不希望,使用者可以直接访我们的容器,所以我们就需要私有化,例如这样
class EventBus {
private events: any[] = []
}
复制代码
- 然后,我们的事件中心希望拥有那些能力呢,比如说事件监听 on,监听了就需要派发 emit,也就需要移除 remove,移除就需要查找,我们也需要一次性事件,比如说 once,大概是这样子
interface EventBus {
on(name: string,execute: Function): string
once(name: string,execute: Function): string
remove(name: string,eventId?: string): EventBus
emit(name: string,...args: any[]): EventBus
find(name: string): Event | null
}
复制代码
- 上面我们大概设计好我们的事件中心了,这个时候,我们需要明确,我们的每一个事件所拥有的能力和属性
interface Event {
name: string
executes: Execute[]
}
interface Execute {
id: string
eventType: EventType
execute: Function
}
复制代码
- 在上面,我们提到了 eventType,这是为了标示是否为 一次性执行者,在 typescript 中,没有比 枚举 更适合这种情况了
type EventType = 1 | 2
enum EventTypeEnum {
NORMAL_EVENT = 1,
ONCE_EVENT = 2
}
复制代码
- 基本的类型是定义完了,我们来写具体实现的代码,第一步,实现 on once 方法
class EventBus {
private events: Event[] = []
on(name: string,execute: Function): string {
return this.addEvent(name,EventTypeEnum.NORMAL_EVENT,execute)
}
once(name: string,execute: Function): string {
return this.addEvent(name,EventTypeEnum.ONCE_EVENT,execute)
}
}
复制代码
class EventBus {
private addEvent(name: string,eventType: EventType,execute: Function): string {
const eventId = createUid()
const events = this.events
const event = this.find(name)
if (event !== null) {
event.executes.push({ id: eventId,eventType,execute })
return eventId
}
events.push({
name,executes: [
{
id: eventId,execute
}
]
})
return eventId
}
}
复制代码
class EventBus {
find(name: string): Event | null {
const events = this.events
for (let i = 0; i < events.length; i++) {
if (name === events[i].name) {
return events[i]
}
}
return null
}
}
复制代码
class EventBus {
remove(name: string,eventId: string): EventBus {
const events = this.events
for (let i = 0; i < events.length; i++) {
if (events[i].name === name) {
if (eventId && events[i].executes.length > 0) {
const eventIndex = events[i].executes.findIndex(item => item.id === eventId)
if (eventIndex !== -1) {
events[i].executes.splice(eventIndex,1)
}
} else {
events.splice(i,1)
}
return this
}
}
return this
}
}
复制代码
class EventBus {
emit(name: string,...args: any[]): EventBus {
const events = this.events
for (let i = 0; i < events.length; i++) {
if (name === events[i].name) {
const funcs = events[i].executes
funcs.forEach((item,i) => {
item.execute(...args)
if (item.eventType === EventTypeEnum.ONCE_EVENT) {
funcs.splice(i,1)
}
})
return this
}
}
return this
}
}
复制代码
- 作为一个事件中心,为了避免使用者错误使用,创建多个实例,我们可以使用 工厂模式,创建一个全局实例供使用者使用,同时提供使用者一个方法,创建新的实例
function createInstance (): EventBusInstance {
const bus = new EventBus()
return bus as EventBusInstance
}
const bus = createInstance()
bus.create = function create () {
return createInstance()
}
复制代码
总结
iny-bus 的核心代码,其实就这么多,总的来说,非常少,但是能解决我们在小程序中遇到的大量 通信 和 数据刷新问题,是采用 各大平台小程序 原生开发时,页面通信的不二之选,同时,100% 的测试覆盖率,确保了 iny-bus 在使用中的稳定性和安全性,当然,每个库都是从简单走向复杂,功能慢慢完善,如果
大家在使用或者源码中发现了bug或者可以优化的点,欢迎大家提 pr 或者直接联系我
最后,如果 iny-bus 给你提供了帮助或者让你有任何收获,请给 作者 点个赞,感谢大家 点赞