前言

你经常会遇到需要跨标签共享信息的情况,那么本文就跟大家一起回顾下web端有哪些方式可以实现这样的需求。

解决方案

websocket

  1. var ws = new WebSocket("wss://echo.websocket.org");
  2. ws.onopen = function(evt) {
  3. console.log("Connection open ...");
  4. ws.send("Hello WebSockets!");
  5. };
  6. ws.onmessage = function(evt) {
  7. console.log( "Received Message: " + evt.data);
  8. ws.close();
  9. };
  10. ws.onclose = function(evt) {
  11. console.log("Connection closed.");
  12. };

参考资料:websocket教程(阮一峰)

localStorage 的监听

localstorge在一个标签页里被添加、修改或删除时,都会触发一个storage事件,通过在另一个标签页里监听storage事件,即可得到localstorge存储的值,实现不同标签页之间的通信。

  1. $(function(){
  2. window.addEventListener("storage", function(event){
  3. console.log(event.key );
  4. console.log(event.oldValue);
  5. console.log(event.newValue);
  6. console.log(event.url); //当前发生改变的url
  7. });
  8. });

定时器监听cookie

使用cookie+setInterval,将要传递的信息存储在cookie中,每隔一定时间读取cookie信息,即可随时获取要传递的信息。

  1. $(function(){
  2. setInterval(function(){
  3. var value=cookieUtil.get('name');
  4. console.log(value);
  5. }, 10000);
  6. });

BroadCast Channel — postMessage

适用于同源的跨页面通讯,可以帮我们创建一个用于广播的通信频道。当所有页面都监听同一频道的消息时,其中某一个页面通过它发送的消息就会被其他所有页面收到。它的API和用法都非常简单。
下面的方式就可以创建一个标识为AlienZHOU的频道:

const bc = new BroadcastChannel(‘AlienZHOU’);

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

  1. bc.onmessage = function (e) {
  2. const data = e.data;
  3. const text = '[receive] ' + data.msg + ' —— tab ' + data.from;
  4. console.log('[BroadcastChannel] receive message:', text);
  5. };
  6. //要发送消息时只需要调用实例上的postMessage方法即可:
  7. 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 本身并不自动具备“广播通信”的功能,需要我们添加些代码,将其改造成消息中转站:

  1. /* ../util.sw.js Service Worker 逻辑 */
  2. self.addEventListener('message', function (e) {
  3. console.log('service worker receive message', e.data);
  4. e.waitUntil(
  5. self.clients.matchAll().then(function (clients) {
  6. if (!clients || clients.length === 0) {
  7. return;
  8. }
  9. clients.forEach(function (client) {
  10. client.postMessage(e.data);
  11. });
  12. })
  13. );
  14. });

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

  1. /* 页面逻辑 */
  2. navigator.serviceWorker.addEventListener('message', function (e) {
  3. const data = e.data;
  4. const text = '[receive] ' + data.msg + ' —— tab ' + data.from;
  5. console.log('[Service Worker] receive message:', text);
  6. });
  7. //最后,当需要同步消息时,可以调用 Service Worker 的postMessage方法:
  8. /* 页面逻辑 */
  9. navigator.serviceWorker.controller.postMessage(mydata);

indexDB

消息发送方将消息存至 IndexedDB 中;接收方(例如所有页面)则通过轮询去获取最新的信息。在这之前,我们先简单封装几个 IndexedDB 的工具方法。
打开数据库连接:

  1. function openStore() {
  2. const storeName = 'ctc_aleinzhou';
  3. return new Promise(function (resolve, reject) {
  4. if (!('indexedDB' in window)) {
  5. return reject('don't support indexedDB');
  6. }
  7. const request = indexedDB.open('CTC_DB', 1);
  8. request.onerror = reject;
  9. request.onsuccess = e => resolve(e.target.result);
  10. request.onupgradeneeded = function (e) {
  11. const db = e.srcElement.result;
  12. if (e.oldVersion === 0 && !db.objectStoreNames.contains(storeName)) {
  13. const store = db.createObjectStore(storeName, {keyPath: 'tag'});
  14. store.createIndex(storeName + 'Index', 'tag', {unique: false});
  15. }
  16. }
  17. });
  18. }
  19. //存储数据
  20. function saveData(db, data) {
  21. return new Promise(function (resolve, reject) {
  22. const STORE_NAME = 'ctc_aleinzhou';
  23. const tx = db.transaction(STORE_NAME, 'readwrite');
  24. const store = tx.objectStore(STORE_NAME);
  25. const request = store.put({tag: 'ctc_data', data});
  26. request.onsuccess = () => resolve(db);
  27. request.onerror = reject;
  28. });
  29. }
  30. //查询/读取数据
  31. function query(db) {
  32. const STORE_NAME = 'ctc_aleinzhou';
  33. return new Promise(function (resolve, reject) {
  34. try {
  35. const tx = db.transaction(STORE_NAME, 'readonly');
  36. const store = tx.objectStore(STORE_NAME);
  37. const dbRequest = store.get('ctc_data');
  38. dbRequest.onsuccess = e => resolve(e.target.result);
  39. dbRequest.onerror = reject;
  40. }
  41. catch (err) {
  42. reject(err);
  43. }
  44. });
  45. }

剩下的工作就非常简单了。首先打开数据连接,并初始化数据:

  1. openStore().then(db => saveData(db, null))
  2. //对于消息读取,可以在连接与初始化后轮询:
  3. openStore().then(db => saveData(db, null)).then(function (db) {
  4. setInterval(function () {
  5. query(db).then(function (res) {
  6. if (!res || !res.data) {
  7. return;
  8. }
  9. const data = res.data;
  10. const text = '[receive] ' + data.msg + ' —— tab ' + data.from;
  11. console.log('[Storage I] receive message:', text);
  12. });
  13. }, 1000);
  14. });
  15. //最后,要发送消息时,只需向 IndexedDB 存储数据即可:
  16. openStore().then(db => saveData(db, null)).then(function (db) {
  17. // …… 省略上面的轮询代码
  18. // 触发 saveData 的方法可以放在用户操作的事件监听内
  19. saveData(db, mydata);
  20. });

window.open + window.opener(同源页面)


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

  1. let childWins = [];
  2. document.getElementById('btn').addEventListener('click', function () {
  3. const win = window.open('./some/sample');
  4. childWins.push(win);
  5. });
  6. 然后,当我们需要发送消息的时候,作为消息的发起方,一个页面需要同时通知它打开的页面与打开它的页面:
  7. // 过滤掉已经关闭的窗口
  8. childWins = childWins.filter(w => !w.closed);
  9. if (childWins.length > 0) {
  10. mydata.fromOpenner = false;
  11. childWins.forEach(w => w.postMessage(mydata));
  12. }
  13. if (window.opener && !window.opener.closed) {
  14. mydata.fromOpenner = true;
  15. window.opener.postMessage(mydata);
  16. }

注意,我这里先用.closed属性过滤掉已经被关闭的 Tab 窗口。这样,作为消息发送方的任务就完成了。下面看看,作为消息接收方,它需要做什么。
此时,一个收到消息的页面就不能那么自私了,除了展示收到的消息,它还需要将消息再传递给它所“知道的人”(打开与被它打开的页面):

需要注意的是,我这里通过判断消息来源,避免将消息回传给发送方,防止消息在两者间死循环的传递。(该方案会有些其他小问题,实际中可以进一步优化)

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

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

iframe 非同源页

在我的解决跨域的专题文章中有详细介绍代码方案,其思路如下图:

image.png

小结

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

参考文档