实现浏览器内多个标签页之间的通信(服务器不同站点通信)

657 阅读5分钟

原理

  • 实现多个标签页之间的通信,本质上都是通过中介者模式来实现的。因为标签页之间没有办法直接通信,因此我们可以找一个中介者,让标签页和中介者进行通信,然后让这个中介者来进行消息的转发。

方法

  • WebSocket 协议: WebSocket 协议是全双工协议,可以实现服务器推送,所以服务器就可以用来当做这个中介者。标签页通过向服务器发送数据,然后由服务器还可向其他标签页推送转发。

    注意:WebSocket协议不同于SSE,SSE的方式是单向通信,只能实现服务器端向客户端推送信息,如果客户端需要发送信息就是属于下一个http请求,HTTP/2也实现了服务器推送,但也不同于SSE,http2下服务器主动推送的是静态资源。

    // 客户端的 API
    // 1.构造函数,执行语句之后,客户端就会与服务器进行连接。
    var ws = new WebSocket("wss://echo.websocket.org");
    
    
    /*
        2.webSocket.readyState
        readyState属性返回实例对象的当前状态,共有四种。
        CONNECTING:值为0,表示正在连接。
        OPEN:值为1,表示连接成功,可以通信了。
        CLOSING:值为2,表示连接正在关闭。
        CLOSED:值为3,表示连接已经关闭,或者打开连接失败。
    */
    // 3.实例对象的onopen属性,用于指定连接成功后的回调函数。
    /*
        如果要指定多个回调函数,可以使用addEventListener方法。
        ws.addEventListener('open', function (event) {
          ws.send('Hello Server!');
        });
    */
    ws.onopen = function(evt) { 
      console.log("Connection open ..."); 
      ws.send("Hello WebSockets!");
    };
    // 4.实例对象的onmessage属性,用于指定收到服务器数据后的回调函数。
    /*
        如果要指定多个回调函数,可以使用addEventListener方法。
        ws.addEventListener("message", function(event) {
          var data = event.data;
          // 处理数据
        });
    */
    ws.onmessage = function(evt) {
      console.log( "Received Message: " + evt.data);
      ws.close();
    };
    
    // 5.实例对象的onclose属性,用于指定连接关闭后的回调函数。
    ws.onclose = function(evt) {
      console.log("Connection closed.");
    };      
    
  • localStorage:在一个标签页对 localStorage 的变化事件进行监听,当另一个标签页修改数据的时候,通过这个监听事件来获取到数据。这个时候 localStorage 对象就是充当的中介者的角色。

    window.onstorage = (e) => {console.log(e)}
    // 或者这样
    window.addEventListener('storage', (e) => console.log(e))
    

    注意:localstorage是浏览器多个标签共用的存储空间,可以用来实现多标签之间的通信,而session是会话级的存储空间,每个标签页都是单独的。且localstorage的onstorage以及storage事件,针对非当前页面对localStorage进行修改时才会触发,当前页面修改localStorage不会触发监听函数,并且修改要使缓存发生改变时,才会触发。

  • Web Worker:Worker可以通过Worker()构造函数来构建,Worker接收一个参数,该参数为线程脚本的地址,但是必须遵守同源策略,比如在下面的代码中我们使用同目录下的workers.js文件创建一个Worker对象,初始化完成之后,浏览器就会用后台线程运行我们的workers.js文件中的代码。

    // 初始化worker
    let worker = new Worker('workers.js');
    

    创建新的后台线程之后,用到postMessage方法实现和后台线程的通信。 在主线程中,我们可以通过 worker.postMessage 向后台线程传递数据,postMessage接受2个参数,第一个为我们要传递的消息对象,第二个参数为一个数组,用来转让对象的所有权。

    button.addEventListener('click', function () {
        // 向worker传递消息
        worker.postMessage('start');
    });
    

    上述代码中,我们在点击按钮之后主线程中向子线程传递了一个内容为'strat'的字符串对象。此时如果想在子线程中处理这些传递进来的数据的话,我们需要在子线程中通过监听onmessage来捕获这些数据,onmessage接收一个MessageEvent 对象,这里我们先关注 MessageEvent 的 data 属性,data就是我们通过主线程传递进来的数据,其他的MessageEvent属性(如origin、source等)大家可以自行了解。

    // workers.js
    onmessage = function (messageEvent) {
        switch (messageEvent.data) {
            case 'start':
                let result = fabonacci(42);
                postMessage(result);
        }
    }
    

    上述代码中我们通过onmessage监听父线程传递进来的消息,如果消息内容是'start'就执行计算,计算完成之后我们再次通过postMessage把结果传递回去,这里我们需要注意的是,在主线程中我们需要通过指定worker来调用postMessage (worker.postMessage),而在子线程中我们则不需要指定worker,直接调用postMessage方法就可以了。

    回到我们的主线程的代码,在主线程中,我们可以通过worker.onmessage监听子线程传递过来的数据。

    // 监听workder返回的消息
    worker.onmessage = function (messageEvent) {
        alert('计算结束,结果为' + messageEvent.data + ',用时' + (new Date() - time) + 'ms'); 
    }
    
  • ShareWorker:shareWorker 会在页面存在的生命周期内创建一个唯一的共享线程,即开启多个页面也只会使用同一个线程。这个时候共享线程就可以充当中介者的角色。标签页间通过共享一个线程,然后通过这个共享的线程来实现数据的交换。须保证这些标签页都是同源的(相同的协议,主机和端口号)

    创建shaerdWorker:
    let worker = new SharedWorker('sharedworkers.js');
    

    在shareWorker中,如果我们想要拿到Message对象的话,需要通过 shareWorker.port 获得,其中的postMessage和onmessage的用法同workder,如下:。

    // 传递strat指令
    worker.port.postMessage('start');
    // 接收子线程的数据
    worker.port.onmessage = function (val) {
        timeDom.innerHTML = val.data
    }
    

    这里需要注意的是,如果你是通过addEventListener绑定message事件的话(而不是.onmessage)这时候需要手动的调用 port 的 .start()方法。

    worker.port.start();
    worker.port.addEventListener('message', function(e) {
        // ... 
    });
    

    同样的在子线程中我们的使用也有些小的区别,我们可以通过对onconnect事件获取到新的shareWorker的连接,然后在connect的参数中的ports字段我们可以拿到MessageEvent,这时候我们就可以使用 .onmessage 和 .postMessage 来处理和传递我们的数据了。

    // sharedworkers.js
    onconnect = function (e) {
        // 通过 e.ports 拿到 port
        var port = e.ports[0];
    
        // port.onmessage 监听父线程的消息
        port.onmessage = function () {
            // port.postMessage 向父线程传递消息
            port.postMessage(a++)
        }
    }
    
  • 还有一种方式是使用 postMessage 方法,如果我们能够获得对应标签页的引用,我们就可以使用 postMessage 方法,进行通信。

参考

www.zhuwenlong.com/blog/articl…

juejin.cn/post/684490…

www.ruanyifeng.com/blog/2017/0…