前言
作为一个大四的老油条,对于即将毕业的小编来讲,毕业设计是逃不掉的,最近在着手开始写毕设的后台管理系统,这个后台管理系统的前端用的 Vue
+ iframe
实现tab标签页,但通过这种方式实现管理系统页面会遇到一个问题,例如:通过商品列表页打开一个修改商品信息tab页,修改完后想要让商品列表页自动刷新对应的商品信息。
对于这个问题可能会想到,子页面调用父页面的方法然后通过父页面调用对应的子页面来进行更新内容,但这种方式会增加页面之间的耦合度而且也很蠢,我们不妨把这个机制理解为 跨页面通信 ,而前端跨页面通信的方式有很多种,这里小编采用“发布 — 订阅”的设计模式以及跨页面通信的两种最简单的方式来实现。
更多的跨页面通信的方式可查看这片文章:跨页面通信的各种姿势
“发布 — 订阅”模式(Publisher && Subscriber)
发布订阅模式是指订阅者(Subscriber)通过一个主题(Theme)和自定义事件(Event)进行消息订阅,当发布者(Publisher)通过发布主题的方式通知各个订阅该主题的Subscriber执行绑定的回调事件。
优点:
- 降低各个模块之间的耦合度,模块之间是松散耦合的。
- 更加灵活,多个订阅者订阅同一个主题,但各个订阅者之间并不知道对方的存在,互不影响,只要发布者发布了主题之后,订阅者就各自执行自己的回调。
基于发布订阅模式常见的案例有:
- 大妈到销售中心找销售说“有优惠活动就告诉我”,这时候新出一个优惠活动,销售告诉大妈“有优惠活动啦”过来买吧。
- 点击事件:
window.addEventListener("click", event)
。 - Vue组件间通过eventBus进行通信。
其中,在Vue里组件之间通过eventBus进行通信是典型的“发布 - 订阅”模式的例子,用法如下:
首先创建 bus.js
,创建新的Vue实例并导出。
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 | 注册跨页面订阅事件 | theme ,callback |
Listener |
$emit | 传入 theme 和 params 发送跨页面消息 |
theme , ...params |
- |
$off | 注销 Listener |
Listener |
- |
Listener 对象
方法名 | 说明 | 传参 |
---|---|---|
change | 修改listener的回调事件 | Function |
open | 开启listener回调事件 | - |
close | 关闭listener回调事件,关闭后可调用 listener.open() 重新开启 |
- |
off | 注销listener,和 close 的区别是注销后调用 open 也无法执行回调 |
- |
总结一下
- 发布订阅模式:订阅者(Subscribe)通过一个主题和自定义事件进行消息订阅,当发布者(Publisher)通过发布主题的方式通知各个订阅该主题的Subscriber执行绑定的回调事件。
- 使用
Vue
的eventBus
进行跨组件通信。 - 同源页面下使用
localStorage
的storage
事件进行跨页面通信。 - 同源页面下使用
BroadcastChannel
广播消息频道进行跨页面通信(如果要兼容万恶根源IE浏览器的话不建议使用)。 - 结合
Vue
的eventBus
和两种跨页面通信方式实现基于“发布 — 订阅”模式的跨页面通信。