使用BroadcastChannel建立较为可靠的连接
如需安装所有解决方案,请参考全量解决方案安装
使用BroadcastChannel,同源的不同标签页之间可以相互通信,有的时候,一些操作需要在一个标签页面执行,然后广播给其他标签页。
比如: 和服务器建立WebSocket通信,只能一个标签页和服务器建立通信,这个时候就需要多个标签页之间协调出一个主标签页建立连接,将消息广播给其他标签页。
此方案基于BroadcastChannel:
- 相对可靠的。
- 动态更新tab标签页列表。
- 主节点选举。
- 事件监听。
实现流程如下
- 标签初始化:标签页面打开,连接BroadcastChannel时,先广播消息(id为当前时间+随机数)。
- 已存在标签回复:已经存在的标签页面广播通道收到广播消息,进行回复。
- ID 收集:收到回复后,将该标签页添加到id列表(
friendChannelIdSet
)中。 - ID列表确认:一定时间(300ms)后,根据收到的最新的id列表(
friendChannelIdSet
),可以知道都有那些标签页。 - 主节点选举:根据id列表信息(
friendChannelIdSet
),可以知道自己是否可以为主节点。 - 主节点职责:
- 定时发送主节点心跳广播信息。
- 并将id列表信息保存为旧节点信息列表(
_oldFrendChannelIdList
)。 - 清空id列表信息(
friendChannelIdSet
)。 - 根据旧列表重新评估主状态;必要恢复成普通节点。
- 心跳监听:
- 回复主节点心跳。
- 将id列表信息保存为旧节点信息列表(
_oldFrendChannelIdList
)。 - 清空当前 ID 列表。
- 将主节点添加到id列表中。
- 处理回复:所有监听此广播通道的标签页,收到主节点广播信息的回复信息后,将其对应id添加到id列表信息中(
friendChannelIdSet
) - 申请为主节点:其他监听此广播通道的非主节点标签页,每次收到主节点心跳广播信息,会设置一个超时时间,如果超时了没收到主节点信息,则认为主节点不在了:
- 广播申请为主节点。
- 一定时间内,如果收到其他标签页的拒绝信息,则不成为主节点,如果没收到拒绝信息,则成为主节点。
- 选举协商:收到其他标签页申请为主节点的消息后,根据自己的id,回复其是否可以为主节点。
单独安装此javascript类
shell
npm install @havue/bc-connect --save
shell
yarn add @havue/bc-connect
shell
pnpm install @havue/bc-connect
使用
引入
ts
import { BroadcastChannelManager } from 'havue'
// or
import { BroadcastChannelManager } from '@havue/solutions'
// or
import { BroadcastChannelManager } from '@havue/bc-connect'
示例
可复制当前页面地址,通过其他标签页打开,进行通信。
其他标签页实例id 列表
当前标签页实例 id:
当前标签页实例 类型:
点我看代码
vue
<template>
<div class="brdCnl-box">
<div class="left-box">
<div class="title">其他标签页实例id 列表</div>
<div class="friend-item" v-for="friend in friendList" :key="friend">{{ friend }}</div>
</div>
<div class="right-box">
<div class="current-info">当前标签页实例 id: {{ currentBcId }}</div>
<div class="current-info">当前标签页实例 类型: {{ currentBcNodeType }}</div>
<div class="options-box">
<div class="form-item"><span class="label">发送消息:</span><input v-model="message" /></div>
<button class="borad-btn" @click="handleBroadMessage">广播</button>
<div class="form-item"><span class="label">目标实例id:</span><input v-model="targetId" /></div>
<button class="sigle-btn" @click="handleBroadOneMessage">单发</button>
</div>
<div class="message-box">
<div class="message-item" v-for="message in recieveMessageList" :key="message.id">
<div class="id">id:{{ message.id }}</div>
<div class="data">data: {{ message.data }}</div>
</div>
</div>
</div>
</div>
</template>
ts
<script setup lang="ts">
import type { BcConnectSendMessageType } from '@havue/bc-connect'
import { ref, onMounted, onBeforeUnmount } from 'vue'
// import { BroadcastChannelManager, BcConnectEventTypeEnum } from '@havue/bc-connect'
import { BroadcastChannelManager, BcConnectEventTypeEnum } from '@havue/solutions'
let bcManager: BroadcastChannelManager | null = null
const currentBcId = ref()
const currentBcNodeType = ref()
const friendList = ref<Array<number>>([])
const message = ref()
const targetId = ref()
const recieveMessageList = ref<Array<BcConnectSendMessageType>>([])
function handleBroadMessage() {
if (!message.value) {
alert('请输入消息')
return
}
bcManager?.send('test-message', message.value)
}
function handleBroadOneMessage() {
if (!message.value) {
alert('请输入消息')
return
}
if (!targetId.value) {
alert('请输入目标id')
return
}
bcManager?.sendToTarget('test-message', Number(targetId.value), message.value)
}
onMounted(() => {
if (!bcManager) {
bcManager = new BroadcastChannelManager('bc_test')
}
currentBcId.value = bcManager.id
friendList.value = bcManager.friendList
bcManager.connect()
bcManager.on('test-message', (info) => {
recieveMessageList.value.push(info)
})
bcManager.on(BcConnectEventTypeEnum.Friend_List_Update, (info) => {
friendList.value = info.data || []
})
bcManager.on(BcConnectEventTypeEnum.Node_Type_Change, (info) => {
currentBcNodeType.value = info.data
})
})
onBeforeUnmount(() => {
bcManager?.close()
bcManager?.destroy()
bcManager = null
})
</script>
scss
<style lang="scss" scoped>
.brdCnl-box {
box-sizing: border-box;
display: flex;
width: 100%;
height: 600px;
color: black;
background-color: #4d797c;
.left-box {
width: 30%;
overflow: auto;
.title {
font-size: 18px;
font-weight: bold;
}
.friend-item {
padding: 6px 10px;
margin: 5px;
background-color: rgb(100 148 237);
}
}
.right-box {
display: flex;
flex: 1;
flex-direction: column;
padding: 5px 10px;
background-color: #4e7c4d;
.current-info {
padding: 3px 5px;
background-color: #a8b9be;
}
.message-box {
flex: 1;
margin-top: 10px;
overflow: auto;
background-color: antiquewhite;
.message-item {
padding: 5px;
margin: 5px;
background-color: #8cbba9;
.id {
font-weight: bold;
}
.message {
color: #44b15c;
}
}
}
.form-item {
display: flex;
align-items: center;
margin: 5px 20px;
.label {
display: inline-block;
width: 100px;
text-align: right;
}
input {
flex: 1;
padding: 5px;
font-size: 16px;
color: #fff;
background-color: black;
}
}
button {
width: 100%;
color: black;
background-color: #a8b9be;
}
}
}
</style>
源码
ts
// #region typedefine
/** 事件数据类型 */
export type BcConnectSendMessageType = {
/** 事件类型 */
type: string
/** 数据 */
data: any
/** 发送事件的实例id */
id: number
/** 消息是发送给目标实例id的 */
targetId?: number
}
/** 事件类型 */
export enum BcConnectEventTypeEnum {
/** 初始广播 */
Broadcast = 'Hello world',
/** 回复初始广播 */
Broadcast_Reply = 'I can hear you',
/** 主节点心跳 */
Main_Node_Hearbeat = '苍天还在,别立黄天',
/** 回复主节点心跳 */
Res_Main_Node_Hearbeat = '苍天在上,受我一拜',
/** 长时间未收到主节点心跳,我想当主节点,你们同意吗 */
Req_Be_Main_Node = '苍天已死,黄天当立',
/** 排资论辈,我应是主节点,不同意 */
Res_Be_Main_Node = '我是黄天,尔等退下',
/** 当前BC节点类型更改 */
Node_Type_Change = 'node type change',
/** 其他标签页BC节点id列表更新 */
Friend_List_Update = 'friend list update'
}
/** 当前webview BroadcastChannel节点类型 */
export enum BcConnectNodeTypeEnum {
Main = 'main',
Normal = 'normal'
}
// #endregion typedefine
// 消息超时时间
const MessageTimeout = 300
/**
* 使用BroadcastChannel与其他标签页进行通信
*/
export class BroadcastChannelManager {
/** 通道名称 */
private _bcName: string
/** BroadcastChannel实例 */
private _broadcastChannel: BroadcastChannel | undefined = undefined
/** 事件map */
private _eventMap: Map<string, Array<(_: BcConnectSendMessageType) => void>>
/** 主节点发送心跳的interval */
private _mainNodeMsgInterval: number | null = null
/** 认为主节点掉线的timeout */
private _mainNodeMsgTimeoutTimer: number | null = null
/** 更新友方列表的timeout */
private _updateFriendListTimer: number | null = null
/** 当前实例id */
public id: number = Date.now() + Math.random()
/** 记录的友方id数组 */
private _oldFrendChannelIdList: Array<number> = []
/** 正在更新的右方id数组 */
private _friendChannelIdSet: Set<number> = new Set()
/** 当前节点类型 */
private _nodeType: BcConnectNodeTypeEnum | undefined = undefined
/** 是否开启调试模式,会在控制台打印相关信息 */
private _debug: boolean = false
constructor(name: string, debug: boolean = false) {
this._debug = debug
this._bcName = name
this._eventMap = new Map()
this._debug && console.log('BC:id:', this.id)
}
get nodeType() {
return this._nodeType
}
get friendList() {
return [...this._oldFrendChannelIdList]
}
public connect() {
this._debug && console.log('BC:bc connect')
this.close()
this._broadcastChannel = new BroadcastChannel(this._bcName)
this._bindBroadcastChannelEvent()
this._updateFriendList()
}
public close() {
this._debug && console.log('BC:bc close')
this._broadcastChannel && this._broadcastChannel.close()
this._broadcastChannel = undefined
this._updateFriendListTimer && clearTimeout(this._updateFriendListTimer)
this._updateFriendListTimer = null
this._mainNodeMsgInterval && clearInterval(this._mainNodeMsgInterval)
this._mainNodeMsgInterval = null
this._mainNodeMsgTimeoutTimer && clearTimeout(this._mainNodeMsgTimeoutTimer)
this._mainNodeMsgTimeoutTimer = null
this._oldFrendChannelIdList = []
this._friendChannelIdSet.clear()
this._nodeType = undefined
}
/**
* 切换节点类型
* @param {BcConnectNodeTypeEnum} type
* @returns
*/
private _setNodeType(type: BcConnectNodeTypeEnum) {
if (this._nodeType === type) {
return
}
this._nodeType = type
this.emit(BcConnectEventTypeEnum.Node_Type_Change, {
type: BcConnectEventTypeEnum.Node_Type_Change,
data: type,
id: this.id
})
}
/** 更新友方列表 */
private _updateFriendList() {
// 广播告知己方存在
this.send(BcConnectEventTypeEnum.Broadcast)
this._updateFriendListTimer && clearTimeout(this._updateFriendListTimer)
this._updateFriendListTimer = setTimeout(() => {
this._oldFrendChannelIdList = this._getNewFriendList()
this._debug && console.log('BC:connect:updateFriendChannelIdList:', this._oldFrendChannelIdList)
this.emit(BcConnectEventTypeEnum.Friend_List_Update, {
type: BcConnectEventTypeEnum.Friend_List_Update,
data: [...this._oldFrendChannelIdList],
id: this.id
})
this._updataNodeType()
}, MessageTimeout)
}
/** 绑定事件 */
private _bindBroadcastChannelEvent() {
this._broadcastChannel &&
(this._broadcastChannel.onmessage = (event) => {
const { type, targetId } = event.data
if (targetId && targetId !== this.id) {
return
}
this.emit(type, event.data)
})
// 收到世界呼唤
this.on(BcConnectEventTypeEnum.Broadcast, (data) => {
const { id } = data
if (!this._friendChannelIdSet.has(id)) {
this._friendChannelIdSet.add(id)
}
this.sendToTarget(BcConnectEventTypeEnum.Broadcast_Reply, id)
})
// 收到友方存在
this.on(BcConnectEventTypeEnum.Broadcast_Reply, (data) => {
const { id } = data
this._addFriend(id)
})
// 收到其他申请为主节点
this.on(BcConnectEventTypeEnum.Req_Be_Main_Node, (data) => {
const { id } = data
if (id > this.id) {
this.sendToTarget(BcConnectEventTypeEnum.Res_Be_Main_Node, id)
}
})
// 收到其他节点回复主节点心跳
this.on(BcConnectEventTypeEnum.Res_Main_Node_Hearbeat, (data) => {
this._addFriend(data.id)
})
this._bindNodeEvent()
}
/** 监听节点类型切换事件 */
private _bindNodeEvent() {
const onMainNodeHearbeat = (data: BcConnectSendMessageType) => {
this._timeoutToBeMainNode()
this._catchOldFriend()
this._addFriend(data.id)
this.send(BcConnectEventTypeEnum.Res_Main_Node_Hearbeat)
}
this.on(BcConnectEventTypeEnum.Node_Type_Change, (info) => {
const { data } = info
this._mainNodeMsgInterval && clearInterval(this._mainNodeMsgInterval)
this._debug && console.log('BC:代理类型切换:', info.data)
if (data === BcConnectNodeTypeEnum.Main) {
// 定时发送主节点心跳
this._mainNodeMsgInterval = setInterval(() => {
this._catchOldFriend()
this.send(BcConnectEventTypeEnum.Main_Node_Hearbeat)
}, MessageTimeout)
} else if (data === BcConnectNodeTypeEnum.Normal) {
this._timeoutToBeMainNode()
}
// 收到主节点心跳, 重新更新友方列表
this.on(BcConnectEventTypeEnum.Main_Node_Hearbeat, onMainNodeHearbeat)
})
}
/** 获取最新的友方列表 */
private _getNewFriendList() {
return [...this._friendChannelIdSet].sort((a, b) => a - b)
}
/**
* 更新当前节点类型
*/
private _updataNodeType() {
this._mainNodeMsgInterval && clearInterval(this._mainNodeMsgInterval)
if (this._oldFrendChannelIdList.length === 0 || Math.min(...this._oldFrendChannelIdList) > this.id) {
if (this._nodeType === BcConnectNodeTypeEnum.Main) {
return
}
this._setNodeType(BcConnectNodeTypeEnum.Main)
} else {
if (this._nodeType === BcConnectNodeTypeEnum.Normal) {
return
}
this._setNodeType(BcConnectNodeTypeEnum.Normal)
}
}
private _timeoutToBeMainNode() {
this._mainNodeMsgTimeoutTimer && clearTimeout(this._mainNodeMsgTimeoutTimer)
// 超时未收到心跳,认为主节点掉线,申请为主节点
this._mainNodeMsgTimeoutTimer = setTimeout(() => {
this._req_beMainNode()
}, MessageTimeout * 3)
}
/**
* 保持记录的活跃的友方id列表
* 清空正在记录的友方id列表
*/
private _catchOldFriend() {
const newFriendList = this._getNewFriendList()
if (this._oldFrendChannelIdList.join() !== newFriendList.join()) {
this._debug && console.log('BC:updateFriendChannelIdList:', newFriendList)
this.emit(BcConnectEventTypeEnum.Friend_List_Update, {
type: BcConnectEventTypeEnum.Friend_List_Update,
data: [...newFriendList],
id: this.id
})
this._oldFrendChannelIdList = [...newFriendList]
}
if (this._nodeType === BcConnectNodeTypeEnum.Main && Math.min(...this._oldFrendChannelIdList) < this.id) {
// 有更小的id,不再为主节点
this._setNodeType(BcConnectNodeTypeEnum.Normal)
}
this._friendChannelIdSet.clear()
}
/**
* 申请成为主节点
*/
private _req_beMainNode() {
this._debug && console.log('BC:req_beMainNode')
// 向所有id友方节点发送申请
this.send(BcConnectEventTypeEnum.Req_Be_Main_Node)
// 如果长时间未回复,认为自己可以当主节点
const timer = setTimeout(() => {
this._setNodeType(BcConnectNodeTypeEnum.Main)
}, MessageTimeout)
// 收到拒绝回复,清空timeout
const handleRes_beMainNode = () => {
clearTimeout(timer)
this.off(BcConnectEventTypeEnum.Res_Be_Main_Node, handleRes_beMainNode)
}
this.on(BcConnectEventTypeEnum.Res_Be_Main_Node, handleRes_beMainNode)
}
/**
* 添加友方
* @param id 节点id
*/
public _addFriend(id: number) {
if (!this._friendChannelIdSet.has(id)) {
this._friendChannelIdSet.add(id)
}
}
/**
* 广播消息
* @param type 消息类型
* @param data 数据
*/
public send(type: string, data?: any) {
this._broadcastChannel?.postMessage({
type,
data,
id: this.id
})
}
/**
* 给特定id的节点发送消息
* @param type 消息类型
* @param targetId 目标节点id
* @param data 数据
*/
public sendToTarget(type: string, targetId: number, data?: any) {
this._broadcastChannel?.postMessage({
type,
data,
id: this.id,
targetId
})
}
/**
* 注册事件
* @param { string } event 事件类型
* @param callback 回调
* @returns void
*/
public on(event: string, callback: (_: BcConnectSendMessageType) => void) {
const callbacks = this._eventMap.get(event)
if (!callbacks) {
this._eventMap.set(event, [callback])
return
}
if (callbacks.includes(callback)) {
return
}
callbacks.push(callback)
}
/**
* 注销事件
* @param { string } event 事件类型
* @param callback 事件回调
* @returns
*/
public off(event: string, callback?: (_: BcConnectSendMessageType) => void) {
const callbacks = this._eventMap.get(event)
if (!callbacks) {
return
}
if (!callback) {
callbacks.length = 0
return
}
const index = callbacks.indexOf(callback)
callbacks.splice(index, 1)
}
/**
* 触发事件
* @param { string } event 事件类型
* @param data 数据
*/
public emit(event: string, data: BcConnectSendMessageType) {
const callbacks = this._eventMap.get(event) || []
callbacks.forEach((cb) => {
cb(data)
})
}
/**
* 销毁
*/
public destroy() {
this._bcName = ''
this._eventMap.clear()
this._broadcastChannel?.close()
this._broadcastChannel = undefined
this._oldFrendChannelIdList = []
this._friendChannelIdSet.clear()
this.id = -1
this._nodeType = undefined
this._mainNodeMsgInterval && clearInterval(this._mainNodeMsgInterval)
this._mainNodeMsgInterval = null
this._mainNodeMsgTimeoutTimer && clearInterval(this._mainNodeMsgTimeoutTimer)
this._mainNodeMsgTimeoutTimer = null
this._debug && console.log('BC:destroy')
}
}
BroadcastChannelManager类介绍
构造函数
属性 | 说明 | 参数类型 |
---|---|---|
constructor | 构造函数, 参数为通道名称 | (name: string) => void |
实例属性
属性 | 说明 | (参数)类型 |
---|---|---|
id | 实例id | number |
nodeType | 当前节点类型 | NodeTypeEnum | null |
friendList | 存储的其他标签页通道实例id列表 | number[] |
connect | 建立通信,监听通道消息 | () => void |
close | 断开通信,不再监听通道消息 | () => void |
destroy | 销毁实例 | () => void |
send | 广播消息,其他实例会触发type 事件 | (type: string, data?:any) => void |
sendToTarget | 给特定id的实例广播消息 | (type: string, targetId: number, data?:any) => void |
on | 监听特定事件 | (event: string, callback: (_: SendMessageType) => void) => void |
off | 取消监听事件, 不传callback 会清空事件 | (event: string, callback?: (_: SendMessageType) => void) => void |
emit | 触发事件 | (event: string, data: SendMessageType) => void |
类型定义
ts
/** 事件数据类型 */
export type BcConnectSendMessageType = {
/** 事件类型 */
type: string
/** 数据 */
data: any
/** 发送事件的实例id */
id: number
/** 消息是发送给目标实例id的 */
targetId?: number
}
/** 事件类型 */
export enum BcConnectEventTypeEnum {
/** 初始广播 */
Broadcast = 'Hello world',
/** 回复初始广播 */
Broadcast_Reply = 'I can hear you',
/** 主节点心跳 */
Main_Node_Hearbeat = '苍天还在,别立黄天',
/** 回复主节点心跳 */
Res_Main_Node_Hearbeat = '苍天在上,受我一拜',
/** 长时间未收到主节点心跳,我想当主节点,你们同意吗 */
Req_Be_Main_Node = '苍天已死,黄天当立',
/** 排资论辈,我应是主节点,不同意 */
Res_Be_Main_Node = '我是黄天,尔等退下',
/** 当前BC节点类型更改 */
Node_Type_Change = 'node type change',
/** 其他标签页BC节点id列表更新 */
Friend_List_Update = 'friend list update'
}
/** 当前webview BroadcastChannel节点类型 */
export enum BcConnectNodeTypeEnum {
Main = 'main',
Normal = 'normal'
}