什么?你只会用localStorage通信?来看看js跨标签页通信

2,265 阅读4分钟

一、为什么要跨标签页通信

在web开发中,有时会有这样的情况,A页面中打开了B页面,B页面中操作了一些内容,再回到A页面时,需要看到更新后的内容。这种场景在电商、支付、社交等领域经常出现。

二、实现跨标签页通信的几种方式

2.1 localStorage

image.png

打开A页面,可以看到localStorage和sessionStorage中都存储了testA:

image.png image.png

B页面中,可以获取到localStorage,但是无法获取到sessionStorage:

image.png

2.2 BroadcastChannel

BroadcastChannel允许同源(相同的协议、host 以及端口)下浏览器不同窗口订阅它,postMessage方法用于发送消息,message事件用于接收消息。

A页面:

      const bc = new BroadcastChannel('test')

      bc.postMessage('不去上班行吗?')

B页面:

      const bc = new BroadcastChannel('test')

      bc.onmessage = (e) => {
        console.log(e)
      }

A、B两个页面都已经打开了,A页面可以发送消息给B页面,B页面也可以发送消息给A页面: 动画.gif

2.3 postMessage(跨源通信)

image.png

2.3.1 自己给自己发送消息

这种方式没有实际作用,只是体会下message事件,和其回调中返回的参数e

http://127.0.0.1:5501/testA.html:

      const data = {msg: '不去上班行不行?'};

      window.postMessage(data, 'http://127.0.0.1:5501'); // 第二个参数表示哪些窗口能收到事件,可以写'*',但是能确定的最好写确定的

      window.addEventListener('message', (e) => {
        console.log(e);
        console.log(e.data); // {msg: '不去上班行不行?'}
        console.log(e.origin); // http://127.0.0.1:5501
        console.log(e.source); // window
        console.log(e.source === this); // true
      });

message参数:

  1. data就是postMessage发送消息时第一个参数
  2. origin表示监听的消息是那个标签页发送的
  3. source表示发送消息的代理对象(window)

2.3.2 A打开B,B发送数据给A

http://127.0.0.1:5501中有A和B两个页面

testA:

      window.name = 'A页面';
      window.addEventListener('message', (e) => {
        console.log(e.data);
        console.log(e.origin);
        console.log(e.source);
        console.log(e.source === this);
      });

      window.open('http://127.0.0.1:5501/testB.html', 'B页面'); // 第二个参数是设置被打开页面的window.name

testB:

      const data = {msg: '我是B页面'};

      const opener = window.opener; // 如果当前窗口是由另一个窗口打开的,window.opener保留了那个窗口的引用。如果当前窗口不是由其他窗口打开的,则返回null
      opener && opener.postMessage(data, 'http://127.0.0.1:5501');

image.png

image.png

2.3.3 A打开B并且发送数据给B

testA.html:

      window.name = 'A页面';
      const data = {name: '我是A页面'};

      const open = window.open('http://127.0.0.1:5501/testB.html', 'B页面');
      // window.open打开B页面后,B页面并没有被立即加载出来,这里用定时器等待B页面加载出来后再发送消息
      setTimeout(() => {
        open.postMessage(data, 'http://127.0.0.1:5501');
      }, 1000);

testB.html:

      window.addEventListener('message', (e) => {
        console.log(e.data);
        console.log(e.origin);
        console.log(e.source);
      });

打开A时,会自动打开B,并且给B发送消息:

image.png

但是在A页面中是通过定时器去触发消息发送的,这种会有个延时,实际中可以当B页面打开的时候就发送一条消息给A,A接收到消息表明此时B已经加载好了,再发送消息给B

testA.html:

      window.name = 'A页面';
      const data = {name: '我是A页面'};

      const open = window.open('http://127.0.0.1:5501/testB.html', 'B页面');

      window.addEventListener('message', (e) => {
        console.log(e);
        if (e.data === 'B页面初始化') {
          open.postMessage(data, 'http://127.0.0.1:5501');
        }
      });

testB.html:

      const opener = window.opener;
      opener.postMessage('B页面初始化', 'http://127.0.0.1:5501');
      window.addEventListener('message', (e) => {
        console.log(e.data);
        console.log(e.origin);
        console.log(e.source);
      });

2.3.4 iframe跨域数据传递

testA.html

    <h1>A页面</h1>
    <div id="message"></div>
    <iframe
      src="http://127.0.0.1:5501/testB.html"
      frameborder="1"
      id="Bframe"
    ></iframe>
    
       
      window.name = 'A页面';
      const data = {msg: '不上班行不行'};

      window.onload = () => {
        // const frame = document.querySelector('#Bframe').contentWindow;
        const frame = window.frames[0];
        frame.postMessage(data, 'http://127.0.0.1:5501');
      };

      window.addEventListener('message', (e) => {
        console.log(e.data);
        console.log(e.origin);
        console.log(e.source);
        const div = document.querySelector('#message');
        div.innerHTML = 'A页面接收到:' + e.data.msg;
      });

testB.html

    <h1>B页面</h1>
    <div id="message"></div>
    
      window.name = 'B页面';
      window.addEventListener('message', (e) => {
        console.log(e.data);
        console.log(e.origin);
        console.log(e.source);
        const div = document.querySelector('#message');
        div.innerHTML = 'B页面接收到:' + e.data.msg;

        const data = {msg: '不上班你养我啊'};
        window.top.postMessage(data, 'http://127.0.0.1:5501');
      });

image.png

2.4 SharedWorker

  1. 和BroadcastChannel一样,是必须要在同源窗口下,可以进行数据交互;同样地,postMessage方法用于发送消息,message事件用于接收消息。
  2. 和BroadcastChannel相比,SharedWorker的优势在于使用一个js文件统一管理状态,如果交互逻辑比较复杂,这点优势会被放大。

2.4.1 效果:

动画.gif

sharedWorker.js

let num = 0;
const workerList = [];

self.addEventListener('connect', (e) => {
  const port = e.ports[0];
  port.addEventListener('message', (e) => {
    num += e.data === 'add' ? 1 : -1;
    workerList.forEach((port) => {
      // 遍历所有已连接的part,发送消息
      port.postMessage(num);
    });
  });
  port.start();
  workerList.push(port); // 存储已连接的part
  port.postMessage(num); // 初始化
});

testA.html

    <p>首页</p>
    count:
    <span id="count">0</span>
    <button id="add">add</button>

    <br />
    <br />
    <iframe src="./testB.html"></iframe>

    <script>
      if (!!window.SharedWorker) {
        const count = document.querySelector('#count');
        const add = document.querySelector('#add');

        const worker = new SharedWorker('./sharedWorker.js');
        worker.port.start();
        worker.port.addEventListener('message', (e) => {
          count.innerText = e.data;
          console.log('首页监听', e);
        });

        add.addEventListener('click', () => {
          worker.port.postMessage('add');
        });
      }
    </script>

testB.html

    <p>iframe页面</p>
    count:
    <span id="count">0</span>
    <button id="reduce">reduce</button>
    <script>
      if (!!window.SharedWorker) {
        const count = document.querySelector('#count');
        const reduce = document.querySelector('#reduce');

        const worker = new SharedWorker('./sharedWorker.js');
        worker.port.start();
        worker.port.addEventListener('message', (e) => {
          count.innerText = e.data;
          console.log('iframe监听', e);
        });

        reduce.addEventListener('click', () => {
          worker.port.postMessage('reduce');
        });
      }
    </script>

2.4.5 Service Worker

如果你觉得这篇文章对你有用,可以看看作者封装的库xtt-utils,里面封装了非常实用的js方法。如果你也是vue开发者,那更好了,除了常用的api,还有大量的基于element-ui组件库二次封装的使用方法和自定义指令等,帮你提升开发效率。不定期更新,欢迎交流~