小程序頁面通訊、數據刷新、事件總線 、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

相關文章
相關標籤/搜索