常见的浏览器不同标签页间通信的几种方式

383 阅读8分钟

引言

在日常Web开发中,我们需要在不同浏览器标签页之间进行数据传递和时间触发,比如不同标签页面同时切换,另一个标签页将拿到得数据传递回当前页面,或通知原来得页面进行刷新等操作。那么为解决上述问题,我学习并整理了几种浏览器跨标签页通信的解决方案,仅供大家学习和参考!!!

常见方案如下:

  • BroadCast Channel
  • Service Worker
  • Window.onstorage
  • window.postMessage
  • Shared Worker
  • IndexedDB
  • Cookie
  • Websoket

BroadCast Channel

BroadCast Channel,顾名思义会常见一个同源页面都可以共享的广播频道,当某个页面中发送消息时可以被同源的其他页面监听到,从而实现跨标签也的广播通信。 Broadcast Channel API允许同源(同一站点)的浏览器上下文(包括窗口,标签,框架或iframe)之间的简单通信。

image.png

下边我们对Broadcast Channel进行简单的一个封装:

/**
 * 简单封装BroadcastChannel的用法
 */
export 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: {
    LOGOUT: {name: 'logout',comment: '用户登出系统'},
    LOGIN: { name: 'login', comment: '用户登录成功' }
  }
};

Service Worker

Service Worker ,一个服务器与浏览器之间的中间人角色,可以实现消息推动、地理围栏、离线应用等功能,如果网站中注册了service worker那么它可以拦截当前网站所有的请求,进行判断(需要编写相应的判断程序),如果需要向服务器发起请求的就转给服务器,如果可以直接使用缓存的就直接返回缓存不再转给服务器,从而大大提高浏览体验。

// page1.vue
navgator.serviceWorker.register('./serviceWorker.js'
	.then(()=>{
		console.log('serviceWorker 注册成功')
	})
const buttonClick = ()=>{
	navigator.serviceWorker.controller.postMessage('小猪Passion');
}
// page2.vue
navgator.serviceWorker.register('./serviceWorker.js'
	.then(()=>{
		console.log('serviceWorker 注册成功')
	})
navigator.serviceWorker.onmessage = ({ data }) => {
	console.log(data)
}
// serviceWorker.js
self.addEventListener("message",async event=>{
    const clients = await self.clients.matchAll();
    clients.forEach(function(client){
        client.postMessage(event.data)
    });
});

LocalStorage Window.onstorage

如何利用window.onstorage进行通信呢?我们都知道触发window.onstorage必须满足条件:就是对已有storage进行更新。localStorage 里面存储的数据没有过期时间设置,而存储在 sessionStorage 里面的数据在页面会话结束时会被清除,故我们采用localStorage 结合 window.onstorage 去完成。

// page1.vue
localStorage.setItem('message', '小猪Passion');
// page2.vue
window.onstorage = (e) => {
	if(e.key === 'message'){
		console.log(e.newValue)
	}
};

注:

  1. 同一页面不生效:同一页面中修改了local Storage值,window.onstorage时间不会在当前页面触发;
  2. 初次设置值不生效:onstorage的触发条件是值得变化,而新增并不被是为值得变化。

window.postMessage

window.postMessage,显而易见也是为了实现不同页面间的相互通信。但此方法可以在不同源的情况下,任意页面之间进行通信,它提供了一种受控机制来规避跨域的限制。该方法也有一定的安全隐患,如果在没有任何限制的情况下,不同源的页面可能会对你进行xss攻击。不过,只要正确的使用,这种方法就很安全。

// 发起通讯
window.opener.postMessage(message,targetOrigin);

// 监听通信
window.addEventListener("message", (e) => { console.log(e); });

注意:

  1. 如果你不希望从其他网站接收message,请不要为message事件添加任何事件侦听器;
  2. 如果你确实希望从其他网站接收message,请始终使用originsource属性验证发件人的身份;
  3. 当你使用postMessage将数据发送到其他窗口时,始终指定精确的目标origin,而不是'*'。

Shared Worker

Shared Worker,是一种在多个浏览器标签页之间共享的 JavaScript 线程,因此也可用来实现跨标签页通信。它可以在同源页面中进行连接,访问窗口、iframe或其他worker

// use Shared Worker
// 创建一个 Shared Worker
const worker = new SharedWorker('worker.js');
// 监听来自 Shared Worker 的消息
worker.port.onmessage = function(event) {
  console.log('Received message from worker:', event.data);
};
// 向 Shared Worker 发送消息
worker.port.postMessage('小猪Passion!!!');

// worker.js
// 监听来自主页面的消息
self.onconnect = function(event) {
  const port = event.ports[0];
  
  // 监听来自主页面的消息
  port.onmessage = function(event) {
    console.log('Received message from main page:', event.data);
    
    // 向主页面发送消息
    port.postMessage('Hello from shared worker!');
  };
};

IndexedDB

IndexedDB 是一种底层 API,用于在客户端存储大量的结构化数据(也包括文件/二进制大型对象(blobs))。该 API 使用索引实现对数据的高性能搜索。虽然 Web Storage 在存储较少量的数据很有用,但对于存储更大量的结构化数据来说力不从心。而 IndexedDB 提供了这种场景的解决方案。
IndexedDB,是一种浏览器提供的本地数据库,可以存在多个不同的标签页中故可以共享数据。

使用场景:

  1. 数据可视化等界面,大量数据请求会消耗性能;
  2. 聊天工具等,大量数据需要存储在本地
// 初始化 IndexedDB 写入操作
const dbName = 'crossTabDB';
const storeName = 'messages';
function openDB() {
  return new Promise((resolve, reject) => {
    const request = indexedDB.open(dbName, 1);

    request.onupgradeneeded = (event) => {
      const db = event.target.result;
      if (!db.objectStoreNames.contains(storeName)) {
        db.createObjectStore(storeName, { keyPath: 'id', autoIncrement: true });
      }
    };

    request.onsuccess = (event) => {
      resolve(event.target.result);
    };

    request.onerror = (event) => {
      reject(event.target.error);
    };
  });
}

async function writeMessage(message) {
  const db = await openDB();
  const tx = db.transaction(storeName, 'readwrite');
  const store = tx.objectStore(storeName);

  store.add({ message, timestamp: Date.now() });
  
  tx.oncomplete = () => {
    console.log('Message written:', message);
  };

  tx.onerror = (event) => {
    console.error('Error writing message:', event.target.error);
  };
}


// 利用 setInterval 轮询读取消息

let lastTimestamp = 0;
async function readMessages() {
  const db = await openDB();
  const tx = db.transaction(storeName, 'readonly');
  const store = tx.objectStore(storeName);
  const request = store.getAll();
  request.onsuccess = () => {
    const allMessages = request.result;
    const newMessages = allMessages.filter(msg => msg.timestamp > lastTimestamp);
    if (newMessages.length > 0) {
      console.log('New Messages:', newMessages);
      lastTimestamp = Math.max(...newMessages.map(msg => msg.timestamp));
    }
  };
  request.onerror = (event) => {
    console.error('Error reading messages:', event.target.error);
  };
}
// 设置轮询,每隔 1 秒检查新消息
setInterval(readMessages, 1000);

Cookie

同理,Cookie也可以在用于多个不同标签页共享数据。

// 写入消息
function writeMessageToCookie(message, expireSeconds = 5) {
  const expireDate = new Date();
  expireDate.setTime(expireDate.getTime() + expireSeconds * 1000);
  document.cookie = `crossTabMessage=${encodeURIComponent(
    message
  )}; expires=${expireDate.toUTCString()}; path=/`;
  console.log('Message written to cookie:', message);
}

// 读取Cookie
function readMessageFromCookie(name) {
  const cookieArr = document.cookie.split(';');
  for (let cookie of cookieArr) {
    const [key, value] = cookie.split('=').map((item) => item.trim());
    if (key === name) {
      return decodeURIComponent(value);
    }
  }
  return null;
}

// 轮询读取Cookie 并 处理消息
let lastMessage = null;
function checkForNewMessage() {
  const message = readMessageFromCookie('crossTabMessage');
  if (message && message !== lastMessage) {
    console.log('New Message:', message);
    lastMessage = message;
  }
}
// 设置轮询,每隔 1 秒检查新消息
setInterval(checkForNewMessage, 1000);

注意:

  1. Cookie 通信只能在同一域名下共享;
  2. Cookie 存储大小有限,通常为几KB;
  3. 安全性问题,Cookie中的数据可以通过其他途径更改

Websocket

WebSocket 是一种在单个TCP连接上进行全双工通信的协议。WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。故也可以用来进行跨标签页通信,示例代码如下:

// page1 发送消息
// 创建一个 WebSocket 连接
const socket = new WebSocket('ws://example.com');
// 监听连接成功的事件
socket.onopen = function() {
 // 发送消息到服务器
  socket.send('小猪Passion');
};

// page2 接收消息
// 创建一个 WebSocket 连接
const socket = new WebSocket('ws://example.com');
// 监听来自服务器的消息
socket.onmessage = function(event) {
  console.log('Received message:', event.data);
};

// 服务端
// 创建一个 WebSocket 服务器
const WebSocketServer = require('ws').Server
const wss = new WebSocketServer({ port: 8080 })
// 监听来自客户端的连接
wss.on('connection', function (socket) {
  // 监听来自客户端的消息
  socket.on('message', function (message) {
    console.log('Received message:', message)

    // 向所有客户端发送消息
    wss.clients.forEach(function (client) {
      client.send(message)
    })
  })
})


小结

  • BroadcastChannel,叫做“广播频道”,官方文档说,该API是用于同源不同页面之间完成通信的功能。与window.postMessage的区别:BroadcastChannel只能用于同源的页面之间进行通信,而window.postMessage却可以用于任何的页面之间基于BroadcastChannel的同源策略,它无法完成跨域的数据传输,跨域的情况,我们还是使用window.postMessage来处理
  • Service Worker是一种在浏览器后台运行的脚本,可以拦截和处理网络请求。通过在Service Worker中监听和处理消息事件,可以实现跨标签页通信。
  •  window.onstorage监听:通过在不同的标签页中监听LocalStorage的变化,可以实现跨标签页通信。当一个标签页修改LocalStorage的值时,其他标签页可以通过监听storage事件来获取最新值。
  • 文章的后四种方式,采用轮询+的方式来实现跨标签页面通信,但是实际使用场景较少较为复杂,并不推荐。

上述时小弟的一点点理解,希望能一次积累进步,若存在错误请指出,谢谢大家!!!


学习参考:

  1. 跨标签页通信的8种方式(下)
  2. 浏览器跨页面通信 Broadcast Channel 与 window.postMessage 详解
  3. 前端本地存储数据库IndexedDB完整教程

文章荐读:

  1. 🔥🔥🔥Vite6 +TypeScript+Vue3+Tailwind+ESlint+Prettier+Husky搭建企业级前端项目
  2. 📸📸📸前端屏幕录制解决方案探索———WebRTC,html2canvas和rrweb
  3. 🧠🧠🧠由一个BUG引发的对JavaScript运行机制Event Loop的探索