基於「發佈 — 訂閱」模式的跨頁面通訊

前言

做爲一個大四的老油條,對於即將畢業的小編來說,畢業設計是逃不掉的,最近在着手開始寫畢設的後臺管理系統,這個後臺管理系統的前端用的 Vue + iframe 實現tab標籤頁,但經過這種方式實現管理系統頁面會遇到一個問題,例如:經過商品列表頁打開一個修改商品信息tab頁,修改完後想要讓商品列表頁自動刷新對應的商品信息。javascript

對於這個問題可能會想到,子頁面調用父頁面的方法而後經過父頁面調用對應的子頁面來進行更新內容,但這種方式會增長頁面之間的耦合度並且也很蠢,咱們不妨把這個機制理解爲 跨頁面通訊 ,而前端跨頁面通訊的方式有不少種,這裏小編採用「發佈 — 訂閱」的設計模式以及跨頁面通訊的兩種最簡單的方式來實現。html

更多的跨頁面通訊的方式可查看這片文章:跨頁面通訊的各類姿式前端

「發佈 — 訂閱」模式(Publisher && Subscriber)

發佈訂閱模式是指訂閱者(Subscriber)經過一個主題(Theme)和自定義事件(Event)進行消息訂閱,當發佈者(Publisher)經過發佈主題的方式通知各個訂閱該主題的Subscriber執行綁定的回調事件。vue

優勢:java

  • 下降各個模塊之間的耦合度,模塊之間是鬆散耦合的。
  • 更加靈活,多個訂閱者訂閱同一個主題,但各個訂閱者之間並不知道對方的存在,互不影響,只要發佈者發佈了主題以後,訂閱者就各自執行本身的回調。

基於發佈訂閱模式常見的案例有:windows

  • 大媽到銷售中心找銷售說「有優惠活動就告訴我」,這時候新出一個優惠活動,銷售告訴大媽「有優惠活動啦」過來買吧。
  • 點擊事件:window.addEventListener("click", event)
  • Vue組件間經過eventBus進行通訊。

其中,在Vue裏組件之間經過eventBus進行通訊是典型的「發佈 - 訂閱」模式的例子,用法以下:設計模式

首先建立 bus.js,建立新的Vue實例並導出。api

import Vue from 'vux'
export default new Vue()
複製代碼

接着在組件A和組件B中引入bus.js:import Bus from '@/utils/bus,組件A在 mounted 鉤子中調用 Bus 的註冊訂閱方法 $on 傳入訂閱主題和回調方法,組件B中在點擊事件中發佈主題,讓訂閱該主題的組件執行回調方法。瀏覽器

// 組件A
mounted () {
    Bus.$on('SayHollow', text => {
        console.log(text)
    })
}
複製代碼
// 組件B
methods: {
    clickEvent () {
        Bus.$emit('SayHollow', '啊俊俊')
    }
}
複製代碼

那麼接下來小編將參照 Vue 的 eventBus 來實現基於「發佈 - 訂閱」模式的跨頁面通訊,注意這裏的跨頁面通訊並非Vue中的跨頁面通訊,而是跨瀏覽器tab頁面通訊。緩存

基於localStorage實現跨頁面通訊

localStorage是前端經常使用的本地存儲,它用於長久保存整個網站的數據,保存的數據沒有過時時間,直到手動去刪除,但localStorage有一個StorageEvent事件可能不太瞭解。

在同源頁面下,localStorage的內容發生改變後,會觸發 storage 事件並執行相應的回調函數,因此咱們能夠根據這個特性實現同源頁面下跨頁面通訊。

同源頁面:遵循同源策略,即兩個頁面的協議、域名、端口、主機要徹底相同,則這兩個頁面爲同源頁面。

// http://localhost:8080/A.html
window.addEventListener('storage', e => {
    // e.key 改變的key值 - msg
    // e.oldValue 改變前的值
    // e.newValue 改變後的值 - 哈哈哈
    if (e.key === 'msg') { ... }
})

// http://localhost:8080/B.html
localStorage.setItem('msg', '哈哈哈')
複製代碼

觸發listener條件

  • 遵循同源策略。
  • 只有值發生改變的時候才觸發,設置相同的值不會觸發listener事件,localStorage只能緩存String字符串,若是緩存的值是'a',而後設置同一個key的值是'a',那麼新設置的值將不會觸發listener事件,解決方法就是在設置的時候拼上一個隨機數或時間戳。
  • 執行 localStorage.setItem 操做的頁面沒法觸發listener事件。
  • Safari瀏覽器在隱身模式下沒法使用localStorage存儲。

代碼實現

// CrossPageMsg.js
class Listener {
  constructor (theme, fn) {
    this.theme = theme
    this.fn = fn
    this.open_status = true
    this.handle = this.handle.bind(this)
  }
  handle (e) {
    // 若是改變的storage的key不是CrossPageMsg就忽略
    if (e.key !== 'CrossPageMsg') return
    let info = JSON.parse(e.newValue)
    if (info.theme === this.theme && this.open_status) {
      this.fn(...info.args)
    }
  }
  change (fn) {
    this.fn = fn
  }
  open () {
    this.open_status = true
  }
  close () {
    this.open_status = false
  }
  off () {
    window.removeEventListener('storage', this.handle)
  }
}

export default {
  // 訂閱函數
  '$on': (theme, fn) => {
    const listener = new Listener(theme, fn)
    window.addEventListener('storage', listener.handle)
    return listener
  },
  // 發佈函數
  '$emit': (theme, ...args) => {
    if (typeof theme !== 'string') return
    localStorage.setItem('CrossPageMsg', JSON.stringify({
      theme,
      args,
      random: Math.random() * 10
    }))
  },
  // 關閉訂閱
  '$off': (listener) => {
    if (listener instanceof Listener) {
      listener.off()
    }
  }
}
複製代碼

以上是核心代碼的實現,使用方式很簡單,在 main.js 引入該文件並設置 Vue.prototype.$Cross = Cross 便可在組件中使用,如下是使用方式。

// main.js
import Vue from 'vue'
import Cross from 'CrossPageMsg.js'

Vue.prototype.$Cross = Cross
...
複製代碼

// PageA.vue - 訂閱者A(Subscriber)
<template>
  <div>
    <div>我是訂閱者A, 訂閱的主題是GetStudent</div>
    <button @click="CreateListener">建立訂閱</button>
    <button @click="OffListener">移除訂閱</button>
    <button @click="listener.close()">關閉訂閱</button>
    <button @click="listener.open()">開啓訂閱</button>
    <button @click="ChangeEvent">開啓訂閱</button>
    <div v-for="(item, index) in stu_list" :key="index">{{item.name}}, {{item.age}}</div>
  </div>
</template>

<script>
  export default {
    data () {
      return {
        listener: '',
        stu_list: []
      }
    },
    methods: {
      // 註冊訂閱
      CreateListener () {
        this.listener = this.$Cross.$on('GetStudent', (name, age) => {
          console.log('我是訂閱者A,我訂閱的主題是GetStudent')
          console.log('我收到的信息是', name, age)
          this.stu_list.push({ name, age })
        })
      },
      // 移除訂閱
      OffListener () {
        // 調用listener自身off方法移除訂閱
        this.listener.off()
        // 調用$Cross.$off,傳入listener移除訂閱
        // this.$Cross.$off(this.listener)
      },
      // 修改listener回調
      ChangeEvent () {
        this.listener.change((name, age) => {
          console.log(`你好 ${name}`)
        })
      }
    },
    mounted () {
      this.CreateListener()
    }
  }
</script>
複製代碼

// Send.vue - 發佈者(Publisher)
<template>
  <div>
    <div>發送 [GetStudent] 主題,信息:小明,16歲</div>
    <button @click="Send1">發送</button>
    <div>發送 [GetStudent] 主題,信息:小張,18歲</div>
    <button @click="Send2">發送</button>
    <div>發送 [GetGrade] 主題,信息:一年級</div>
    <button @click="Send3">發送</button>
  </div>
</template>

<script>
  export default {
    methods: {
      Send1 () {
        this.$Cross.$emit('GetStudent', '小明', '16歲')
      },
      Send2 () {
        this.$Cross.$emit('GetStudent', '小張', '18歲')
      },
      Send3 () {
        this.$Cross.$emit('GetGrade', '一年級')
      }
    }
  }
</script>
複製代碼

50行代碼都不到就完成了,CrossPageMsg.js 中的代碼是整個功能的核心代碼。

  • $on 傳入 theme 和回調方法註冊 listener,註冊後的 listener 還有其餘的api(下文會介紹)。
  • $emit 用來發布主題消息,傳入的第一個參數是 theme,其餘參數則依次傳入回調方法中。
  • $off 負責移除 listener,要是使用過Vue中的eventBus的小夥伴應該不陌生。

基於Broadcast Channel實現跨頁面通訊

可能不少小夥伴都不知道 Broadcast Channel 是什麼?在 MDN 上面的解釋是這樣子的:

The BroadcastChannel interface represents a named channel that any browsing context of a given origin can subscribe to. It allows communication between different documents (in different windows, tabs, frames or iframes) of the same origin. Messages are broadcasted via a message event fired at all BroadcastChannel objects listening to the channel.

意思就是「廣播頻道」,它能夠在同源頁面下建立一個廣播消息頻道,當不一樣頁面監聽該頻道後,某個頁面向該頻道發出消息後會被監聽該頻道的頁面所接收並進行回調。

// A頁面監聽廣播
// 第一步 建立實例
const bc = new BroadcastChannel('myBroadcastChannel')
// 第二部 經過onmessage設置回調事件
bc.onmessage = e => {
    console.log(e.data)
}

// B頁面發送廣播
const bc = new BroadcastChannel('myBroadcastChannel')
bc.postMessage('hollow word')

// 關閉廣播
bc.close()
複製代碼

觸發onmessage條件

  • 遵循同源策略。
  • 發送和接收的通道一致,即便用 new BroadcastChannel 建立實例時傳入參數一致。
  • 執行 postMessage 操做的實例沒法觸發自身的 onmessage 事件。

兼容性問題

雖說Broadcast Channel的api很是簡單,在跨頁面通訊上有着出色的表現,但對於萬惡的 IE瀏覽器 來說,兼容性就不那麼樂觀了,這裏能夠看到在 Can I Use 上的兼容性,目前最新版本的IE都不兼容。

雖然兼容性不太友好,但也實現如下吧~~

代碼實現

// Broadcast.js
class Listener {
  constructor (theme, fn) {
    this.theme = theme
    this.open_status = true
    this.bc = new BroadcastChannel(theme)
    this.change(fn)
  }
  change (fn) {
    this.bc.onmessage = e => {
      if (this.open_status) {
        fn(...e.data.args)
      }
    }
  }
  open () {
    this.open_status = true
  }
  close () {
    this.open_status = false
  }
  off () {
    this.bc.close()
  }
}

export default {
  // 訂閱者
  '$on': (theme, fn) => {
    return new Listener(theme, fn)
  },
  // 發佈者
  '$emit': (theme, ...args) => {
    const bc = new BroadcastChannel(theme)
    bc.postMessage({ theme, args })
    bc.close()
  },
  // 關閉訂閱
  '$off': (listener) => {
    if (listener instanceof Listener) {
      listener.close()
    }
  }
}
複製代碼

以上是核心代碼,使用方式和localStorage方式的一毛同樣,這裏就不把代碼寫出來了。

API

簡單的整理下API,第一次寫,寫得很差莫怪~~

Cross

方法名 說明 傳參 返回
$on 註冊跨頁面訂閱事件 themecallback Listener
$emit 傳入 themeparams 發送跨頁面消息 theme, ...params -
$off 註銷 Listener Listener -

Listener 對象

方法名 說明 傳參
change 修改listener的回調事件 Function
open 開啓listener回調事件 -
close 關閉listener回調事件,關閉後可調用 listener.open() 從新開啓 -
off 註銷listener,和 close 的區別是註銷後調用 open 也沒法執行回調 -

總結一下

  • 發佈訂閱模式:訂閱者(Subscribe)經過一個主題和自定義事件進行消息訂閱,當發佈者(Publisher)經過發佈主題的方式通知各個訂閱該主題的Subscriber執行綁定的回調事件。
  • 使用 VueeventBus 進行跨組件通訊。
  • 同源頁面下使用 localStoragestorage 事件進行跨頁面通訊。
  • 同源頁面下使用 BroadcastChannel 廣播消息頻道進行跨頁面通訊(若是要兼容萬惡根源IE瀏覽器的話不建議使用)。
  • 結合 VueeventBus 和兩種跨頁面通訊方式實現基於「發佈 — 訂閱」模式的跨頁面通訊。
相關文章
相關標籤/搜索