zoukankan      html  css  js  c++  java
  • 浅析浏览器跨页面通信的方式:localStorage+StorageEvent事件、BroadCast Channel广播通信、Service Worker消息中转、postMessage、直接引用-window.open + window.opener、WebSocket服务端推送、利用iframe桥实现非同源页面通信

      在浏览器中,我们可以同时打开多个Tab页,每个Tab页可以粗略理解为一个“独立”的运行环境,即使是全局对象也不会在多个Tab间共享。然而有些时候,我们希望能在这些“独立”的Tab页面之间同步页面的数据、信息或状态。

      正如下面这个例子:我在列表页点击“收藏”后,对应的详情页按钮会自动更新为“已收藏”状态;类似的,在详情页点击“收藏”后,列表页中按钮也会更新。这就是我们所说的前端跨页面通信。那么你知道哪些跨页面通信的方式呢?

      浏览器的同源策略在下述的一些跨页面通信方法中依然存在限制。因此,我们先来看看,在满足同源策略的情况下,都有哪些技术可以用来实现跨页面通信。

    一、同源页面间的跨页面通信1:localStorage

    1、实现原理:一个窗口更新 localStorage,另一个窗口监听 window 对象的 storage 事件来实现通信。

      注意:两个页面要同源(URL的协议、域名和端口相同)。要访问一个localStorage对象,页面必须来自同一个域名(子域名无效),使用同一种协议,在同一个端口上,相当于globalStorage[localhost.host]。

    2、具体实现代码:

    // 一个窗口的设值代码
    localStorage.setItem('aaa', 10)
    
    // 其他窗口监听storage事件
    window.addEventListener("storage", function (e) {
      console.log(e)
      console.log(e.newValue)
    })

    3、localStorage 可以实现同一浏览器多个标签页之间通信的原理

      localStorage是Storage对象的实例。对Storage对象进行任何修改,都会在文档上触发storage事件。当通过属性或者setItem()方法保存数据,使用delete操作符或removeItem()删除数据,或者调用clear()方法时,都会发生该事件。这个事件的event对象有以下属性:

    • domain:发生变化的存储空间的域名;
    • key:设置或者删除的键名;
    • newValue:如果是设置值,则为新值;如果是删除值,则是null;
    • oldValue:键被更改之前的值;

      注意:IE8和Firefox只实现了domin属性。

    4、只有值变化才会触发 StorageEvent 事件

      注意这里有一个细节:我们在mydata上添加了一个取当前毫秒时间戳的.st属性。这是因为,storage事件只有在值真正改变时才会触发。举个例子:

    window.localStorage.setItem('test', '123');
    window.localStorage.setItem('test', '123');

      由于第二次的值'123'与第一次的值相同,所以以上的代码只会在第一次setItem时触发storage事件。因此我们通过设置st来保证每次调用时一定会触发storage事件。

    mydata.st = +(new Date);
    window.localStorage.setItem('ctc-msg', JSON.stringify(mydata));

    二、同源页面间的跨页面通信2:BroadCast Channel

      BroadCast Channel 可以帮我们创建一个用于广播的通信频道。当所有页面都监听同一频道的消息时,其中某一个页面通过它发送的消息就会被其他所有页面收到。它的API和用法都非常简单。

    // 下面的方式就可以创建一个标识为 A-Broad 的频道:
    const bc = new BroadcastChannel('A-Broad');

      各个页面可以通过onmessage来监听被广播的消息:

    bc.onmessage = function (e) {
        const data = e.data;
        const text = '[receive] ' + data.msg + ' —— tab ' + data.from;
        console.log('[BroadcastChannel] receive message:', text);
    };

      要发送消息时只需要调用实例上的 postMessage 方法即可:

    bc.postMessage(mydata);

    三、同源页面间的跨页面通信3:Service Worker

      Service Worker 是一个可以长期运行在后台的 Worker,能够实现与页面的双向通信。多页面共享间的 Service Worker 可以共享,将 Service Worker 作为消息的处理中心(中央站)即可实现广播效果。

    1、注册:首先,需要在页面注册 Service Worker

    // 页面逻辑
    navigator.serviceWorker.register('../util.sw.js').then(function () {
        console.log('Service Worker 注册成功');
    });

      其中../util.sw.js是对应的 Service Worker 脚本。

    2、消息中转站逻辑

      Service Worker 本身并不自动具备“广播通信”的功能,需要我们添加些代码,将其改造成消息中转站:

    // ../util.sw.js Service Worker 逻辑
    self.addEventListener('message', function (e) {
        console.log('service worker receive message', e.data);
        e.waitUntil(
            self.clients.matchAll().then(function (clients) {
                if (!clients || clients.length === 0) {
                    return;
                }
                clients.forEach(function (client) {
                    client.postMessage(e.data);
                });
            })
        );
    });

      我们在 Service Worker 中监听了message事件,获取页面(从 Service Worker 的角度叫 client)发送的信息。然后通过self.clients.matchAll()获取当前注册了该 Service Worker 的所有页面,通过调用每个client(即页面)的postMessage方法,向页面发送消息。这样就把从一处(某个Tab页面)收到的消息通知给了其他页面。

    3、监听消息

      处理完 Service Worker,我们需要在页面监听 Service Worker 发送来的消息:

    /* 页面逻辑 */
    navigator.serviceWorker.addEventListener('message', function (e) {
        const data = e.data;
        const text = '[receive] ' + data.msg + ' —— tab ' + data.from;
        console.log('[Service Worker] receive message:', text);
    });

    4、发送消息:最后,当需要同步消息时,可以调用 Service Worker 的postMessage方法

    /* 页面逻辑 */
    navigator.serviceWorker.controller.postMessage(mydata);

      上面我们看到了三种实现跨页面通信的方式,不论是建立广播频道的 Broadcast Channel,还是使用 Service Worker 的消息中转站,抑或是storage事件,其都是“广播模式”:一个页面将消息通知给一个“中央站”,再由“中央站”通知给各个页面。

    在上面的例子中,这个“中央站”可以是一个 BroadCast Channel 实例、一个 Service Worker 或是 LocalStorage。

    四、同源页面间的跨页面通信5:postMessage

      这个就不用多说了,具体昨天总结的这篇博客:《浅析 postMessage 方法介绍、如何接收数据(监听message事件及其属性介绍)、使用postMessage的安全注意事项、具体使用方式(父子页面如何互发消息、接收消息)

    五、同源页面间的跨页面通信4:直接引用 - window.open + window.opener

      当我们使用window.open打开页面时,方法会返回一个被打开页面window的引用。而在未显示指定noopener时,被打开的页面可以通过window.opener获取到打开它的页面的引用 —— 通过这种方式我们就将这些页面建立起了联系(一种树形结构)。

    (一)消息发送方

    1、首先,我们把window.open打开的页面的window对象收集起来:

    let childWins = [];
    document.getElementById('btn').addEventListener('click', function () {
        const win = window.open('./some/sample');
        childWins.push(win);
    });

    2、然后,当我们需要发送消息的时候,作为消息的发起方,一个页面需要同时通知它打开的页面与打开它的页面:

    // 过滤掉已经关闭的窗口
    childWins = childWins.filter(w => !w.closed);
    if (childWins.length > 0) {
        mydata.fromOpenner = false;
        childWins.forEach(w => w.postMessage(mydata));
    }
    if (window.opener && !window.opener.closed) {
        mydata.fromOpenner = true;
        window.opener.postMessage(mydata);
    }

      注意,我这里先用.closed属性过滤掉已经被关闭的 Tab 窗口。这样,作为消息发送方的任务就完成了。下面看看,作为消息接收方,它需要做什么。

    (二)消息接收方

      此时,一个收到消息的页面就不能那么自私了,除了展示收到的消息,它还需要将消息再传递给它所“知道的人”(打开与被它打开的页面):

    window.addEventListener('message', function (e) {
        const data = e.data;
        const text = '[receive] ' + data.msg + ' —— tab ' + data.from;
        console.log('[Cross-document Messaging] receive message:', text);
        // 避免消息回传
        if (window.opener && !window.opener.closed && data.fromOpenner) {
            window.opener.postMessage(data);
        }
        // 过滤掉已经关闭的窗口
        childWins = childWins.filter(w => !w.closed);
        // 避免消息回传
        if (childWins && !data.fromOpenner) {
            childWins.forEach(w => w.postMessage(data));
        }
    });

      这样,每个节点(页面)都肩负起了传递消息的责任,也就是所谓的“口口相传”,而消息就在这个树状结构中流转了起来

      显然,“口口相传”的模式存在一个问题:如果页面不是通过在另一个页面内的window.open打开的(例如直接在地址栏输入,或从其他网站链接过来),这个联系就被打破了。通过父子窗口的引用关系(以’window.open’或’target=_blank’方式打开子窗口),子窗口很容易感知到父窗口作用域值的变化,但是当子窗口刷新后,父子窗口之间的引用关系会消失,此时子窗口也不能接收到父窗口的消息。

      其实还有一种做法是通过 WebSocket 这类的“服务器推”技术来进行同步,这好比将我们的“中央站”从前端移到了后端。

    六、WebSocket

    1、实现原理:所有的 WebSocket 都监听同一个服务器地址,利用send发送消息,利用onmessage获取消息的变化。

    2、优点:不仅能跨窗口,还能跨浏览器,兼容性最佳。

    3、缺点:只是需要消耗点服务器资源。

    七、非同源页面之间的通信:iframe桥

      上面我们介绍了前端跨页面通信的方法,但它们大都受到同源策略的限制。然而有时候,我们有两个不同域名的产品线,也希望它们下面的所有页面之间能无障碍地通信。那该怎么办呢?

      要实现该功能,可以使用一个用户不可见的 iframe 作为“桥”。由于 iframe 与父页面间可以通过指定origin来忽略同源限制,因此可以在每个页面中嵌入一个 iframe (例如:http://sample.com/bridge.html),而这些 iframe 由于使用的是一个 url,因此属于同源页面,其通信方式可以复用上面第一部分提到的各种方式。

    1、页面与 iframe 通信非常简单,首先需要在页面中监听 iframe 发来的消息,做相应的业务处理:

    /* 业务页面代码 */
    window.addEventListener('message', function (e) {
        // …… do something
    });

    2、然后,当页面要与其他的同源或非同源页面通信时,会先给 iframe 发送消息

    /* 业务页面代码 */
    window.frames[0].window.postMessage(mydata, '*');

      其中为了简便此处将postMessage的第二个参数设为了'*',你也可以设为 iframe 的 URL。

    3、iframe 收到消息后,会使用某种跨页面消息通信技术在所有 iframe 间同步消息,例如下面使用的 Broadcast Channel:

    /* iframe 内代码 */
    const bc = new BroadcastChannel('A-Broad');
    // 收到来自页面的消息后,在 iframe 间进行广播
    window.addEventListener('message', function (e) {
        bc.postMessage(e.data);
    });    

    4、其他 iframe 收到通知后,则会将该消息同步给所属的父页面:

    /* iframe 内代码 */
    // 对于收到的(iframe)广播消息,通知给所属的业务页面
    bc.onmessage = function (e) {
        window.parent.postMessage(e.data, '*');
    };

      下图就是使用 iframe 作为“桥”的非同源页面间通信模式图。

      其中“同源跨域通信方案”可以使用之前提到的某种技术。

    八、总结:

    1、对于同源页面,常见的方式包括:

    • 广播模式:Broadcast Channe / Service Worker / LocalStorage + StorageEvent
    • 共享存储模式:Shared Worker / IndexedDB / cookie
    • 口口相传模式:window.open + window.opener
    • 基于服务端:Websocket / Comet / SSE 等

    2、而对于非同源页面,则可以通过嵌入同源 iframe 作为“桥”,将非同源页面通信转换为同源页面通信。

    参考文章:https://juejin.cn/post/6844903811232825357
  • 相关阅读:
    [模板]LCA
    洛谷 P1103 书本整理(动规)
    [模板]KMP字符串匹配
    [模板]优先队列(堆)
    Java面试题10(如何取到set集合的第一个元素)
    Java集合操作类Collections的一些常用方法
    本机不装Oracle,使用plsql连接远程Oracle的方法
    ORACLE配置tnsnames.ora文件实例
    JS正则表达式验证数字
    diea破解
  • 原文地址:https://www.cnblogs.com/goloving/p/15386826.html
Copyright © 2011-2022 走看看