前端跨页面通信

跨页面通信主要分两大类

  • 同源页面间的跨页面通信
  • 非同源页面间的跨页面通信

同源页面间的跨页面通信

1.BroadCast Channel(广播式通信)

顾名思义,BroadCast Channel 会创建一个所有同源页面都可以共享的(广播)频道,故其中某个页面发送的消息可以被其他页面监听到

如何使用?
  • 创建一个用于广播的通信频道
const bc = new BroadcastChannel('broadCast')

该构造函数接受一个 DOMString 作为 name,用以标识这个 channel。在其他页面,可以通过传入相同的 name 来使用同一个广播通信。
这个 name 值可以通过实例的 .name 属性获得

console.log(bc.name)
// broadCast
  • 消息监听
    BroadCast Channel 创建完成后,就可以在页面监听广播的消息
bc.onmessage = function(e) {
    console.log('receive:', e.data);
};

对于错误也可以绑定监听

bc.onmessageerror = function(e) {
    console.warn('error:', e);
};

除了为 .onmessage 赋值这种方式,也可以使用 addEventListener 来添加 'message' 监听。

bc.addEventListener('message', function (e) {
     console.log('receive:', e.data);
})
  • 发送消息
    BroadCast Channel 实例也有一个对应的 postMessage 方法用于发送消息
bc.postMessage('hello')
  • 关闭广播
    上面的代码实现了多个页面间的广播通信,有时我们会希望取消当前页面的广播监听:
  1. 一种方式是取消或者修改相应的 'message' 事件监听
  2. 另一种更加简便的方式就是使用 BroadCast Channel 实例为我们提供的 close方法。
bc.close()

两者的区别在于,取消 'message' 监听只是让页面不对广播消息进行响应,BroadCast Channel 仍然存在;而调用 close 方法会切断与 BroadCast Channel 的连接,浏览器会尝试回收该对象,在关闭后调用 postMessage 会报错:Channel is closed,如果之后又在需要广播,需要重新创建一个相同 name 的 BroadCast Channel。

  • 兼容性
    BroadCast Channel 是一个非常好用的多页面消息同步 API, 但是兼容性却不是很乐观:
兼容性

2.LocalStorage

LocalStorage作为前端最常用的本地存储,大家应该非常熟悉了,但是 StorageEvent 这个和它相关的事件我们可能不太常用到。

如何使用?
  • 消息监听
    当 LocalStorage 发生变化是,会触发 storage 事件。利用这个特性。我们可以在发送消息时,把消息写入到某个 LocalStorage 中,然后在各个页面内,通过监听 storage事件即可收到通知。
window.addEventListener('storage', function(e) {
  if (e.key === 'localStorageData') {
    console.log('[Storage I] receive message:', e.newValue)
  }
})
  • 发送消息
    在需要监听的各个页面上添加以上代码,即可监听到 LocalStorage 的变化。当某个页面需要发送消息时,只需要使用我们熟悉的 setItem方法即可:
const data = {
    txt: 'hello',
    timeStamp: +(new Date)
}
window.localStorage.setItem('localStorageData', JSON.stringify(data));

这里有个细节就是我们在 data 上添加了一个时间戳,这是因为 storage 事件只有在值真正改变时才会触发,所以通过加时间戳来保证每次调用时一定会触发 storage 事件。

  • 兼容性
    LocalStorage的兼容性如下:
兼容性

3.Service Worker

Service Worker 是一个可以长期运行在后台的 Worker, 能够实现与页面的双向通信,将 Service Worker 作为消息的处理中心(中央站)即可实现广播效果。
Ps: Service Worker 也是 PWA的核心技术之一,但是本次分享重点不在 PWA,所以不做进一步展开。

如何使用?
  • 在页面注册 Service Worker (需先判断当前浏览器是否支持 Service Worker ,避免由于浏览器不兼容导致的 bug )
if('serviceWorker' in window.navigator) {
    navigator.serviceWorker.register('./sw.js').then(function (registration) {
        console.log('success',registration)
    }).catch(function (err) {
        console.log('fail',err)
    })
  }

其中 sw.js 是对应的 Service Worker 脚本。Service Worker 本身并不自动具备“广播通信”的功能,我们需要添加些代码,将其改造成消息中转站:

this.addEventListener('message', function (e) {
  e.waitUntil(
      this.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 方法,向页面发送消息,这样就把从一个页面收到的消息通知给了其他页面。

  • 消息接收
if('serviceWorker' in window.navigator) {
  navigator.serviceWorker.addEventListener('message',function (e) {
    console.log('[Service Worker] receive message:', e.data);
  })
}
  • 发送消息
    在向 Service Worker 发送消息时,我们需要在 serviceWorker 实例上调用 postMessage 方法,这里我们用到的是 navigator.serviceWorker.controller
if('serviceWorker' in window.navigator && navigator.serviceWorker.controller){
  navigator.serviceWorker.controller.postMessage('hello')
}
  • 兼容性
    如下图所示, IE 和 Opera Mini 完全不支持,主流浏览器中 Edge 17以下不支持, Safair 和 IOS Safair 刚刚开始支持,而火狐和 Chrome 支持良好,大家在使用的时候最好还是做一下判断。
兼容性
总结

上面我们看到的三种实现跨页面通信的方式,不管是建立广播频道的 Broadcast Channel,还是使用 Service Worker 的消息中转站,或者是监听 storage 事件,都是“广播模式”:一个页面将消息通知给一个中央站,再由“中央站”通知给各个页面。
下面介绍的另外两种跨页面通信方式,本质是“共享存储 + 轮询模式”。

4.Shared Worker

Shared Worker 在实现跨页面通信的问题在于,它无法主动通知所有页面,因此我们需要使用轮询的方式来拉取最新的数据。

如何使用?
  • 在页面启动 Shared Worker
// 构造函数的第二个参数是 Shared Worker 名称,也可以留空
const sharedWorker = new SharedWorker('../util.shared.js', 'ctc');
  • 完善 util.shared.js
let data = null;
this.addEventListener('connect', function (e) {
    const port = e.ports[0];
    port.addEventListener('message', function (event) {
        // get 指令则返回存储的消息数据
        if (event.data.get) {
            data && port.postMessage(data);
        }
        // 非 get 指令则存储该消息数据
        else {
            data = event.data;
        }
    });
    port.start();
});
  • 消息监听
    在需要监听的页面定时发送 get 指令的消息给 Shared Worker,轮询最新的消息数据,并在页面监听返回的的信息:
// 定时轮询,发送 get 指令的消息
setInterval(function () {
  sharedWorker.port.postMessage({get: true})
}, 1000)

// 监听 get 消息的返回数据
sharedWorker.port.addEventListener('message',function (e) {
    console.log(e.data)
}, false)
sharedWorker.port.start()
  • 发送消息
sharedWorker.port.postMessage('hello');

注意:如果使用 addEventListener来添加 Shared Worker 的消息监听,需要显示调用 MessagePort.start 方法,即上文中的 sharedWorker.port.start();如果使用 onmessage 绑定监听的话则不需要。

  • 兼容性
兼容性

5.IndexedDB

IndexedDB 与 Shared Worker 方案类似,消息发送方将消息存至 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) {
                console.log('IndexedDB] receive message:', res.data);
            }
        });
    }, 1000);
});
  • 发送消息(向 IndexedDB 存储数据)
openStore().then(db => saveData(db, null)).then(function (db) {
    saveData(db, 'hello');
});
  • 兼容性
兼容性
总结

除了“广播模式”,我们又了解了“共享存储+长轮询”这种模式的两种方法,可能我们会觉得长轮询没有广播模式优雅,但实际上我们在使用“共享存储”形式时,不一定要搭配轮询。
例如在多 Tab 场景下,我们可能会离开 Tab A 到另一个 Tab B 中操作,过一会又切换到 Tab B,如果需要将之前在 Tab B 中操作的信息同步回来,只需在 Tab A 中监听 visibilitychange 这样的时间,来做一次信息同步即可。


非同源页面间的跨页面通信

  • 首先模拟场景,假设现在有两个不同源的页面,iframePage.html 和 index.html
<!-- index.html -->
<iframe ref={node => (this.iframeWrapper = node)} src="xxx/xxx/iframePage.html"
  • 父页面向子页面发送消息
this.iframeWrapper.onload = function(){
  // iframe加载完立即发送一条消息
  this.iframeWrapper.contentWindow.postMessag('MessageFromIndex','*');
}

我们知道 postMessage 是挂载到window 对象上的,所以等 iframe 加载完毕后,调用 postMessage 给子页面发送消息。
postMessage 方法的第一个参数是要发送的数据,可以是任何原始类型的数据。第二个参数可以设置发送到哪个 url ,如果当前子页面的 url 和设置的不一致,则会发送失败,我们设置成*,则不限制。

  • 子页面消息监听
function receiveMessageFromIndex ( event ) {
 console.log( 'receiveMessageFromIndex', event )
}

// 监听 message 事件
window.addEventListener("message", receiveMessageFromIndex, false);
  • 子页面向父页面发送消息
parent.postMessage({msg: 'MessageFromIframePage'}, '*');
  • 父页面接收消息
function receiveMessageFromIframePage (event) {
    console.log('receiveMessageFromIframePage', event)
}

//监听message事件
window.addEventListener("message", receiveMessageFromIframePage, false);
  • 兼容性
兼容性

总结

对于同源页面,常见的方式包括:

  • 广播模式:Broadcast Channe / Service Worker / LocalStorage
  • 共享存储模式:Shared Worker / IndexedDB / cookie
    对于非同源页面,则可通过嵌入同源 iframe 作为 “桥”,将非同源页面通信转换为同源页面通信。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,014评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,796评论 3 386
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,484评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,830评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,946评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,114评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,182评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,927评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,369评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,678评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,832评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,533评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,166评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,885评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,128评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,659评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,738评论 2 351

推荐阅读更多精彩内容