• 同源多页面实时通信之BroadcastChannel实现及简单封装


    背景

    日常开发做项目时,如果采用非Vue脚手架时,肯定会碰到这样的场景:比如在浏览器中新开两个tab页面,A页面发送消息后,B页面实时监听并触发某些动作。类似的需求有很多,比如实时共享状态等等。这样的实时通信场景的解决方案我相信大家有很多想法,比如localStoragepostMessageWebSocketsharedWorker等等。
    今天带来另一种方式:BroadcastChannel广播通信。如果着急,可直接跳转到“项目中简单封装章节查看封装代码”

    介绍及API使用

    是什么

    BroadcastChannel 接口代理了一个命名频道,可以让指定 origin 下的任意 browsing context 来订阅它。它允许同源的不同浏览器窗口,Tab 页,frame 或者 iframe 下的不同文档之间相互通信。通过触发一个 message 事件,消息可以广播到所有监听了该频道的 BroadcastChannel 对象。

    简单来说,就是需要在同源的情况下,实现浏览器多窗口实时进行通信,且该通信是广播进行的。

    对象实例化

    // 通道名称,用以区分不同的通道。对于相同的来源下的所有浏览上下文,一个名称只对应一个通道。是string类型,用来标识当前的BroadcastChannel
    const channel = new BroadcastChannel(channelName)
    
    • 1
    • 2

    发送消息

    可以使用postMessage() 发送一条消息,给所有同源下监听了该频道的所有浏览器。消息message事件的形式发送给每一个绑定到该频道的广播频道。

     
    const channel = new BroadcastChannel("test")
     
    // 发送消息通知,参数是任何对象
    bc.postMessage('hello xiaozong');
    
    • 1
    • 2
    • 3
    • 4
    • 5

    监听消息

    发送事件后,如何使用进行消息的监听呢?当频道收到一条消息时,会在关联的BroadcastChannel对象上触发 message 事件,监听方式有两种,具体如下:

    const channel = new BroadcastChannel("test")
    // 消息监听 方式一
    channel.onmessage = ({data}) => {
    	// 这里写具体的业务逻辑
    }
     
    // 消息监听 方式二
    channel.addEventListener('message', ({data}) => {
    	// 这里写具体的业务逻辑
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    错误处理

    const channel = new BroadcastChannel('test');
     
    // 方式一
    channel.addEventListener('messageerror', ({data}) => {
      console.error(data);
    })
     
    // 方式二
    channel.onmessageerror = ({data}) => {
      console.log(data);
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    关闭通道

    通过调用 close() 方法,可以马上断开其与对应频道的关联,并让其被垃圾回收。这是必要的步骤,因为浏览器没有其他方式知道频道不再被需要。不断开可能会导致一直处于监听状态,消耗资源,会导致不能被内存回收。

    // 连接到指定频道
    const channel = new BroadcastChannel('test');
    
    // 当完成后,断开与频道的连接
    channel.close();
    
    • 1
    • 2
    • 3
    • 4
    • 5

    项目中简单封装

    channel.js文件,引入到使用的页面中

    /**
     * 简单封装BroadcastChannel的用法
     */
    const Channel = {
        /**
         * BroadcastChannel对象
         */
        channel: null,
    
        /**
         * 实例化BroadcastChannel对象,赋值给channel变量
         * @param {*} channelName 通道名称,用以区分不同的通道
         * @returns 
         */
        getChannel: (channelName) => {
            Channel.channel = new BroadcastChannel(channelName)
            return Channel.channel
        },
    
        /**
         * 发送消息
         * @param {*} object 消息体
         */
        send: (object) => {
            Channel.channel.postMessage(object)
        },
    
        /**
         * 发送消息,重载方法,可直接调用,省略对象实例化操作
         * @param {*} channelName 通道名称,用以区分不同的通道
         * @param {*} object 消息体
         */
        send: (channelName, object) => {
            if (Channel.channel == null) {
                Channel.channel = Channel.getChannel(channelName)
            }
            Channel.channel.postMessage(object)
        },
    
        /**
         * 监听消息
         * @param {*} callback 回调函数
         */
        listen: (callback) => {
            Channel.channel.onmessage = ({ data }) => {
                callback(data)
            }
        },
    
        /**
         * 监听消息,重载方法,可直接调用,省略对象实例化操作
         * @param {*} channelName 通道名称,用以区分不同的通道
         * @param {*} callback 回调函数
         */
        listen: (channelName, callback) => {
            if (Channel.channel == null) {
                Channel.channel = Channel.getChannel(channelName)
            }
            Channel.channel.onmessage = ({ data }) => {
                callback(data)
            }
        },
    
        /**
         * 通道关闭
         */
        close: () => {
            Channel.channel.close()
        },
    
        /**
         * 通道关闭,重载方法,可直接调用,省略对象实例化操作
         * @param {*} channelName 通道名称,用以区分不同的通道
         */
        close: (channelName) => {
            if (Channel.channel == null) {
                Channel.channel = Channel.getChannel(channelName)
            }
            Channel.channel.close()
        },
    
        /**
         * 通道枚举,定义业务中需要用到的所有通道名称枚举,可根据业务需求无限扩容
         */
        channelEnum: {
            TEST: { name: 'test', coment: '测试通道' },
            REAL_EVENT: { name: 'real_event', coment: '实时事项通道' },
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89

    发送端send.html

    使用方式测试如下:

    DOCTYPE html>
    <html lang="en">
    
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>标签页通信-发送端title>
        <script src="./channel.js">script>
    head>
    
    <body>
        标签页通信-发送端
    body>
    <script>
        Channel.send(Channel.channelEnum.TEST.name, 'hello xiaozong')
    
    script>
    
    html>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    监听端

    使用方式测试如下:

    DOCTYPE html>
    <html lang="en">
    
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>标签页通信-接收端title>
        <script src="./channel.js">script>
    head>
    
    <body>
        标签页通信-接收端
    body>
    <script>
        Channel.listen(Channel.channelEnum.TEST.name, (data) => {
            document.write(`

    ${data}

    `
    ) })
    script> html>
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    运行测试一下吧

    就只贴一下图吧 ,比较简单
    在这里插入图片描述
    在这里插入图片描述

    浏览器兼容程度

    在这里插入图片描述

    总结

    昨晚十二点左右心血来潮,太晚了也只进行了简单的封装,基本满足常规场景的使用,如果实际项目开发中有用到的话,可以优化后使用。

    重要更新

    今天发现这样封装一个页面只支持创建一个channel实例,这完全不行,于是进行了修改,现在附在下面,很抱歉。

    /**
     * 简单封装BroadcastChannel的用法
     */
    const Channel = {
        /**
         * BroadcastChannel对象Map
         */
        channelMap: new Map(),
    
        /**
         * 发送消息,重载方法,可直接调用,省略对象实例化操作
         * @param {*} channelName 通道名称,用以区分不同的通道
         * @param {*} object 消息体
         */
        send: (channelName, object) => {
            if (!Channel.channelMap.has(channelName)) {
                let channel = new BroadcastChannel(channelName);
                Channel.channelMap.set(channelName, channel);
            }
            Channel.channelMap.get(channelName).postMessage(object);
        },
    
        /**
         * 监听消息,重载方法,可直接调用,省略对象实例化操作
         * @param {*} channelName 通道名称,用以区分不同的通道
         * @param {*} callback 回调函数
         */
        listen: (channelName, callback) => {
            if (!Channel.channelMap.has(channelName)) {
                let channel = new BroadcastChannel(channelName);
                Channel.channelMap.set(channelName, channel);
            }
            Channel.channelMap.get(channelName).onmessage = ({ data }) => {
                callback(data);
            };
        },
    
        /**
         * 通道关闭
         * @param {*} channelName 通道名称,用以区分不同的通道
         */
        close: (channelName) => {
            if (Channel.channelMap.has(channelName)) {
                Channel.channelMap.get(channelName).close();
                Channel.channelMap.delete(channelName);
            }
        },
    
        /**
         * 通道枚举,定义业务中需要用到的所有通道名称枚举,可根据业务需求无限扩容
         */
        channelEnum: {
            REMOVE_TAB: { name: 'removeTab', comment: 'tabs标签移除时,用以通知被关闭页面,进行诸如实例、注册事件等的销毁工作' },
            GLOBAL_LOADING: { name: 'globalLoading', comment: '全局加载动画' },
        },
    };
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
  • 相关阅读:
    原型和原型链
    墙布的使用窍门和保养清洁方法 - 江南爱窗帘十大品牌
    java(类加载)
    Logrus 集成 color 库实现自定义日志颜色输出字符原理
    基于RNN和Transformer的词级语言建模 代码分析 PositionalEncoding
    正点原子 核心板IMX6ULL IIC RTC驱动 PCF8563
    解决Spring Boot启动异常:未配置数据源的问题
    定位new运算符
    Halcon Camera-calibration 相关算子(一)
    快来跟我一起抢先看看未来世界的出行,体验未来城市吧~
  • 原文地址:https://blog.csdn.net/shaofengzong/article/details/133820784