前言
你经常会遇到需要跨标签共享信息的情况,那么本文就跟大家一起回顾下web端有哪些方式可以实现这样的需求。
解决方案
websocket
var ws = new WebSocket("wss://echo.websocket.org");ws.onopen = function(evt) {console.log("Connection open ...");ws.send("Hello WebSockets!");};ws.onmessage = function(evt) {console.log( "Received Message: " + evt.data);ws.close();};ws.onclose = function(evt) {console.log("Connection closed.");};
参考资料:websocket教程(阮一峰)
localStorage 的监听
localstorge在一个标签页里被添加、修改或删除时,都会触发一个storage事件,通过在另一个标签页里监听storage事件,即可得到localstorge存储的值,实现不同标签页之间的通信。
$(function(){window.addEventListener("storage", function(event){console.log(event.key );console.log(event.oldValue);console.log(event.newValue);console.log(event.url); //当前发生改变的url});});
定时器监听cookie
使用cookie+setInterval,将要传递的信息存储在cookie中,每隔一定时间读取cookie信息,即可随时获取要传递的信息。
$(function(){setInterval(function(){var value=cookieUtil.get('name');console.log(value);}, 10000);});
BroadCast Channel — postMessage
适用于同源的跨页面通讯,可以帮我们创建一个用于广播的通信频道。当所有页面都监听同一频道的消息时,其中某一个页面通过它发送的消息就会被其他所有页面收到。它的API和用法都非常简单。
下面的方式就可以创建一个标识为AlienZHOU的频道:
const bc = new BroadcastChannel(‘AlienZHOU’);
各个页面可以通过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);
service worker
Service Worker 是一个可以长期运行在后台的 Worker,能够实现与页面的双向通信。多页面共享间的 Service Worker 可以共享,将 Service Worker 作为消息的处理中心(中央站)即可实现广播效果。
首先,需要在页面注册 Service Worker:
/ 页面逻辑 / navigator.serviceWorker.register(‘../util.sw.js’).then(function () { console.log(‘Service Worker 注册成功’); });
其中../util.sw.js是对应的 Service Worker 脚本。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页面)收到的消息通知给了其他页面。
处理完 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);});//最后,当需要同步消息时,可以调用 Service Worker 的postMessage方法:/* 页面逻辑 */navigator.serviceWorker.controller.postMessage(mydata);
indexDB
消息发送方将消息存至 IndexedDB 中;接收方(例如所有页面)则通过轮询去获取最新的信息。在这之前,我们先简单封装几个 IndexedDB 的工具方法。
打开数据库连接:
function openStore() {const storeName = 'ctc_aleinzhou';return new Promise(function (resolve, reject) {if (!('indexedDB' in window)) {return reject('don't support indexedDB');}const request = indexedDB.open('CTC_DB', 1);request.onerror = reject;request.onsuccess = e => resolve(e.target.result);request.onupgradeneeded = function (e) {const db = e.srcElement.result;if (e.oldVersion === 0 && !db.objectStoreNames.contains(storeName)) {const store = db.createObjectStore(storeName, {keyPath: 'tag'});store.createIndex(storeName + 'Index', 'tag', {unique: false});}}});}//存储数据function saveData(db, data) {return new Promise(function (resolve, reject) {const STORE_NAME = 'ctc_aleinzhou';const tx = db.transaction(STORE_NAME, 'readwrite');const store = tx.objectStore(STORE_NAME);const request = store.put({tag: 'ctc_data', data});request.onsuccess = () => resolve(db);request.onerror = reject;});}//查询/读取数据function query(db) {const STORE_NAME = 'ctc_aleinzhou';return new Promise(function (resolve, reject) {try {const tx = db.transaction(STORE_NAME, 'readonly');const store = tx.objectStore(STORE_NAME);const dbRequest = store.get('ctc_data');dbRequest.onsuccess = e => resolve(e.target.result);dbRequest.onerror = reject;}catch (err) {reject(err);}});}
剩下的工作就非常简单了。首先打开数据连接,并初始化数据:
openStore().then(db => saveData(db, null))//对于消息读取,可以在连接与初始化后轮询:openStore().then(db => saveData(db, null)).then(function (db) {setInterval(function () {query(db).then(function (res) {if (!res || !res.data) {return;}const data = res.data;const text = '[receive] ' + data.msg + ' —— tab ' + data.from;console.log('[Storage I] receive message:', text);});}, 1000);});//最后,要发送消息时,只需向 IndexedDB 存储数据即可:openStore().then(db => saveData(db, null)).then(function (db) {// …… 省略上面的轮询代码// 触发 saveData 的方法可以放在用户操作的事件监听内saveData(db, mydata);});
window.open + window.opener(同源页面)
当我们使用window.open打开页面时,方法会返回一个被打开页面window的引用。而在未显示指定noopener时,被打开的页面可以通过window.opener获取到打开它的页面的引用 —— 通过这种方式我们就将这些页面建立起了联系(一种树形结构)。
首先,我们把window.open打开的页面的window对象收集起来:
let childWins = [];document.getElementById('btn').addEventListener('click', function () {const win = window.open('./some/sample');childWins.push(win);});然后,当我们需要发送消息的时候,作为消息的发起方,一个页面需要同时通知它打开的页面与打开它的页面:// 过滤掉已经关闭的窗口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));}});
这样,每个节点(页面)都肩负起了传递消息的责任,也就是我说的“口口相传”,而消息就在这个树状结构中流转了起来。
iframe 非同源页
在我的解决跨域的专题文章中有详细介绍代码方案,其思路如下图:

小结
在跨页沟通的方面,其实各种方案技术上基本都可走通,我觉得最关键的还是自己的使用场景是如何的, 再去确定使用什么技术方案。
