小程序页面通讯、数据刷新、事件总线 、event bus 终极解决方案之 iny-bus

背景介绍

在各类小程序中,咱们常常会遇到 这种状况
有一个 列表,点击列表中的一项进入详情,详情有个按钮,删除了这一项,这个时候当用户返回到列表页时,
发现列表中的这一项依然存在,这种状况,就是一个 `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 文档javascript

简单使用

iny-bus 使用及其简单,在须要的页面 onLoad 中添加事件监听, 在须要触发事件的地方派发事件,使监
听该事件的每一个页面执行处理函数,达到通讯和刷新数据的目的,在小程序中的使用能够参考如下代码
复制代码
// 小程序
  import bus from 'iny-bus'

  // 添加事件监听
  // 在 onLoad 中注册, 避免在 onShow 中使用
  onLoad () {
    this.eventId = bus.on('事件名', (a, b, c, d) => {
      // 支持多参数
      console.log(a, b, c, d)

      this.setData({
        a,
        b,
        c
      }
      // 调用页面请求函数,刷新数据
      this.refreshPageData()
    })

    // 添加只须要执行一次的 事件监听

    this.eventIdOnce = bus.once('事件名', () => {
      // do some thing
    })
  }

  // 移除事件监听,该函数有两个参数,第二个事件id不传,会移除整个事件监听,传入ID,会移除该
  页面的事件监听,避免多余资源浪费, 在添加事件监/// 听后,页面卸载(onUnload)时建议移除

  onUnload () {
    bus.remove('事件名', this.eventId)
  }

  // 派发事件,触发事件监听处更新视图
  // 支持多参传递
  onClick () {
    bus.emit('事件名', a, b, c)
  }

复制代码

更详细的使用和例子能够参考 Github iny-bus 小程序代码java

iny-bus 具体实现

  1. iny-bus 咱们是使用 typescript 编写,同时要发布到 npm 上供你们使用,那咱们就须要搭建开发环境,选择编辑打包工具,编写发布脚本,具体的细节这里不讲,只列举如下使用到的工具和库
  • 基本打包工具,这里使用很是优秀的开源库 typescript-library-starter,具体细节不展开ios

  • 测试工具 使用 facebook 的 jestgit

  • build ci 使用 [travis-ci](www.travis-ci.org/)github

  • 测试覆盖率上传使用 codecovtypescript

  • 具体的其余细节你们能够看源码中的 package.json,这里就一一展开讲了,咱们来看具体实现npm

  1. 具体实现
  • 首先,咱们来设计咱们的事件中心,iny-bus 做为事件中心,咱们就须要一个容器来储存咱们的事件,同时咱们不但愿,使用者能够直接访咱们的容器,因此咱们就须要私有化,例如这样
class EventBus {

    private events: any[] = []

  }

复制代码
  • 而后,咱们的事件中心但愿拥有那些能力呢,好比说事件监听 on,监听了就须要派发 emit, 也就须要移除 remove,移除就须要查找,咱们也须要一次性事件,好比说 once,大概是这样子
interface EventBus {

    // 监听,咱们须要知道一个事件名字,也须要一个 派发时的执行函数,同时,咱们返回一个
    // id 给使用者,方便使用者移除 事件监听
    on(name: string, execute: Function): string

    // once 和 on在使用建立和使用时,没什么区别,惟一的区别就在 执行一次后移除,因此在
    // 建立时 和 on 没有任何区别
    once(name: string, execute: Function): string

    // remove, 前面提到了咱们须要删除事件监听,那咱们就须要 事件名称,为了多个页面能够监
    // 听同一个事件,因此咱们不能一次性把该事件监听所有移除
    // 那么咱们就用到 建立 事件时的 id 了, 同时,咱们返回 咱们的事件中心,能够链式调用
    remove(name: string, eventId?: string): EventBus

    // emit 咱们须要告诉系统,咱们须要派发的事件名和所携带的参数,同时返回 事件实例
    emit(name: string, ...args: any[]): EventBus

    // find 函数返回一个联合类型,有可能存在 该事件,也有可能返回 null
    find(name: string): Event | null

  }

复制代码
  • 上面咱们大概设计好咱们的事件中心了,这个时候,咱们须要明确,咱们的每个事件所拥有的能力和属性
// 每个东西,都有一个名字,方便记忆和寻找,咱们的事件
  // 也须要一个 name,同时,咱们的每个事件,都有可能被监听 n 次,那么咱们就须要
  // 每一个事件来有一个容器,存放每一个事件的执行者

  interface Event {

    // 名称
    name: string

    // 执行者容器
    executes: Execute[]
  }

  // 咱们也须要肯定每一个执行者的类型,为了能精确的找到执行者,因此须要一个 id,这也是 用来
  // 删除的id, 这里的 eventType 是来标示是不是一次性执行者, 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 新增事件监听 * @param name 事件名 * @param execute 回调函数 * @returns { string } eventId 事件ID,用户取消该事件监听 */


    on(name: string, execute: Function): string {
      
      // 由于 on 和 once 在新建上没什么区别,因此这里咱们统一使用 addEvent, 但为了区分 on 和 once,咱们传入了 EventType
      return this.addEvent(name, EventTypeEnum.NORMAL_EVENT, execute)
    }

    /** * one 只容许添加一次事件监听 * @param name 事件名 * @param execute 回调函数 * @returns { string } eventId 事件ID,用户取消该事件监听 */

    once(name: string, execute: Function): string {
      // 同理 on
      return this.addEvent(name, EventTypeEnum.ONCE_EVENT, execute)
    }

  }


复制代码
  • 实现 addEvent 方法
class EventBus {

    /** * 添加事件的方法 * @param name * @param execute */

    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,
            eventType,
            execute
          }
        ]
      })

      return eventId
    }

  }

复制代码
  • 实现 find 方法
class EventBus {
    /** * 查找事件的方法 * @param name */

    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
    }
  }

复制代码
  • 实现 remove 方法
class EventBus {
    /** * remove 移除事件监听 * @param name 事件名 * @param eventId 移除单个事件监听需传入 * @returns { EventBus } EventBus 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
    }
  }

复制代码
  • 实现 emit 方法
class EventBus {
    /** * emit 派发事件 * @param name 事件名 * @param args 其他参数 * @returns { EventBus } EventBus 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
    }
  }

复制代码
  • 做为一个事件中心,为了不使用者错误使用,建立多个实例,咱们可使用 工厂模式,建立一个全局实例供使用者使用,同时提供使用者一个方法,建立新的实例
// 不直接 new EventBus, 而是经过 一个工厂函数来建立实例, 参考 axios 源码
  function createInstance (): EventBusInstance {

    const bus = new EventBus()

    return bus as EventBusInstance
  }

  const bus = createInstance()

  // 扩展 create 方法,用于 使用者 建立新的 bus 实例
  bus.create = function create () {
    return createInstance()
  }
复制代码

总结

iny-bus 的核心代码,其实就这么多,总的来讲,很是少,可是能解决咱们在小程序中遇到的大量 通讯 和 数据刷新问题,是采用 各大平台小程序 原生开发时,页面通讯的不二之选,同时,100% 的测试覆盖率,确保了 iny-bus 在使用中的稳定性和安全性,固然,每一个库都是从简单走向复杂,功能慢慢完善,若是 你们在使用或者源码中发现了bug或者能够优化的点,欢迎你们提 pr 或者直接联系我json

最后,若是 iny-bus 给你提供了帮助或者让你有任何收获,请给 做者 点个赞,感谢你们 点赞axios

相关文章
相关标签/搜索