弁言
在浏览器中,我们可以同时翻开多个Tab页,每一个Tab页可以大略理解为一个“自力”的运转环境,即使是全局对象也不会在多个Tab间同享。但是有些时刻,我们愿望能在这些“自力”的Tab页面之间同步页面的数据、信息或状况。
正以下面这个例子:我在列表页点击“珍藏”后,对应的详情页按钮会自动更新为“已珍藏”状况;相似的,在详情页点击“珍藏”后,列表页中按钮也会更新。
这就是我们所说的前端跨页面通讯。
你晓得哪些跨页面通讯的体式格局呢?假如不清楚,下面我就带人人来看看七种跨页面通讯的体式格局。
一、同源页面间的跨页面通讯
以下各种体式格局的
在线 Demo 可以戳这里 >>
浏览器的同源战略在下述的一些跨页面通讯要领中依旧存在限定。因而,我们先来看看,在满足同源战略的情况下,都有哪些手艺可以用来完成跨页面通讯。
1. BroadCast Channel
BroadCast Channel 可以帮我们竖立一个用于播送的通讯频道。当一切页面都监听统一频道的音讯时,个中某一个页面经由过程它发送的音讯就会被其他一切页面收到。它的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);
Broadcast Channel 的详细的运用体式格局可以看这篇
《【3分钟速览】前端播送式通讯:Broadcast Channel》。
2. Service Worker
Service Worker 是一个可以历久运转在背景的 Worker,可以完成与页面的双向通讯。多页面同享间的 Service Worker 可以同享,将 Service Worker 作为音讯的处置惩罚中间(中心站)即可完成播送结果。
Service Worker 也是 PWA 中的核心手艺之一,由于本文重点不在 PWA ,因而假如想进一步相识 Service Worker,可以浏览我之前的文章
【PWA进修与实践】(3) 让你的WebApp离线可用。
起首,须要在页面注册 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);
3. LocalStorage
LocalStorage 作为前端最经常使用的当地存储,人人应当已异常熟习了;但StorageEvent
这个与它相干的事宜有些同砚可能会比较生疏。
当 LocalStorage 变化时,会触发storage
事宜。运用这个特征,我们可以在发送音讯时,把音讯写入到某个 LocalStorage 中;然后在各个页面内,经由过程监听storage
事宜即可收到关照。
window.addEventListener('storage', function (e) {
if (e.key === 'ctc-msg') {
const data = JSON.parse(e.newValue);
const text = '[receive] ' + data.msg + ' —— tab ' + data.from;
console.log('[Storage I] receive message:', text);
}
});
在各个页面增加如上的代码,即可监听到 LocalStorage 的变化。当某个页面须要发送音讯时,只须要运用我们熟习的setItem
要领即可:
mydata.st = +(new Date);
window.localStorage.setItem('ctc-msg', JSON.stringify(mydata));
注重,这里有一个细节:我们在mydata上增加了一个取当前毫秒时候戳的.st
属性。这是由于,storage
事宜只要在值真正转变时才会触发。举个例子:
window.localStorage.setItem('test', '123');
window.localStorage.setItem('test', '123');
由于第二次的值'123'
与第一次的值雷同,所以以上的代码只会在第一次setItem
时触发storage
事宜。因而我们经由过程设置st
来保证每次挪用时一定会触发storage
事宜。
小憩一下
上面我们看到了三种完成跨页面通讯的体式格局,不论是竖立播送频道的 Broadcast Channel,照样运用 Service Worker 的音讯中转站,抑或是些 tricky 的storage
事宜,其都是“播送情势”:一个页面将音讯关照给一个“中心站”,再由“中心站”关照给各个页面。
在上面的例子中,这个“中心站”可所以一个 BroadCast Channel 实例、一个 Service Worker 或是 LocalStorage。
下面我们会看到别的两种跨页面通讯体式格局,我把它称为“同享存储+轮询情势”。
4. Shared Worker
Shared Worker 是 Worker 家属的另一个成员。一般的 Worker 之间是自力运转、数据互不相通;而多个 Tab 注册的 Shared Worker 则可以完成数据同享。
Shared Worker 在完成跨页面通讯时的题目在于,它没法主动关照一切页面,因而,我们会运用轮询的体式格局,来拉取最新的数据。思绪以下:
让 Shared Worker 支撑两种音讯。一种是 post,Shared Worker 收到后会将该数据保留下来;另一种是 get,Shared Worker 收到该音讯后会将保留的数据经由过程postMessage
传给注册它的页面。也就是让页面经由过程 get 来主动猎取(同步)最新音讯。详细完成以下:
起首,我们会在页面中启动一个 Shared Worker,启动体式格局异常简朴:
// 组织函数的第二个参数是 Shared Worker 称号,也可以留空
const sharedWorker = new SharedWorker('../util.shared.js', 'ctc');
然后,在该 Shared Worker 中支撑 get 与 post 情势的音讯:
/* ../util.shared.js: Shared Worker 代码 */
let data = null;
self.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', (e) => {
const data = e.data;
const text = '[receive] ' + data.msg + ' —— tab ' + data.from;
console.log('[Shared Worker] receive message:', text);
}, false);
sharedWorker.port.start();
末了,当要跨页面通讯时,只需给 Shared Worker postMessage
即可:
sharedWorker.port.postMessage(mydata);
注重,假如运用
addEventListener
来增加 Shared Worker 的音讯监听,须要显式挪用
MessagePort.start
要领,即上文中的
sharedWorker.port.start()
;假如运用
onmessage
绑定监听则不须要。
5. IndexedDB
除了可以运用 Shared Worker 来同享存储数据,还可以运用其他一些“全局性”(支撑跨页面)的存储计划。比方 IndexedDB 或 cookie。
鉴于人人对 cookie 已很熟习,加上作为“互联网最初期的存储计划之一”,cookie 已在现实运用中承受了远多于其设想之初的义务,我们下面会运用 IndexedDB 来完成。
其思绪很简朴:与 Shared Worker 计划相似,音讯发送方将音讯存至 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);
});
小憩一下
在“播送情势”外,我们又相识了“同享存储+长轮询”这类情势。或许你会以为长轮询没有监听情势文雅,但现实上,有些时刻运用“同享存储”的情势时,不一定要搭配长轮询。
比方,在多 Tab 场景下,我们可能会脱离 Tab A 到另一个 Tab B 中操纵;过了一会我们从 Tab B 切换回 Tab A 时,愿望将之前在 Tab B 中的操纵的信息同步返来。这时刻,实在只用在 Tab A 中监听visibilitychange
如许的事宜,来做一次信息同步即可。
下面,我会再引见一种通讯体式格局,我把它称为“口口相传”情势。
6. 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));
}
});
如许,每一个节点(页面)都负担起了通报音讯的义务,也就是我说的“口口相传”,而音讯就在这个树状构造中流转了起来。
小憩一下
明显,“口口相传”的情势存在一个题目:假如页面不是经由过程在另一个页面内的window.open
翻开的(比方直接在地址栏输入,或从其他网站链接过来),这个联络就被打破了。
除了上面这六个罕见要领,实在另有一种(第七种)做法是经由过程 WebSocket 这类的“服务器推”手艺来举行同步。这比如将我们的“中心站”从前端移到了后端。
关于 WebSocket 与其他“服务器推”手艺,不相识的同砚可以浏览这篇《各种“服务器推”手艺道理与实例(Polling/COMET/SSE/WebSocket)》
另外,我还针对以上各种体式格局写了一个 在线演示的 Demo >>
二、非同源页面之间的通讯
上面我们引见了七种前端跨页面通讯的要领,但它们多数遭到同源战略的限定。但是有时刻,我们有两个差别域名的产品线,也愿望它们下面的一切页面之间能无障碍地通讯。那该怎么办呢?
要完成该功用,可以运用一个用户不可见的 iframe 作为“桥”。由于 iframe 与父页面间可以经由过程指定origin
来疏忽同源限定,因而可以在每一个页面中嵌入一个 iframe (比方:http://sample.com/bridge.html
),而这些 iframe 由于运用的是一个 url,因而属于同源页面,其通讯体式格局可以复用上面第一部份提到的各种体式格局。
页面与 iframe 通讯异常简朴,起首须要在页面中监听 iframe 发来的音讯,做响应的营业处置惩罚:
/* 营业页面代码 */
window.addEventListener('message', function (e) {
// …… do something
});
然后,当页面要与其他的同源或非同源页面通讯时,会先给 iframe 发送音讯:
/* 营业页面代码 */
window.frames[0].window.postMessage(mydata, '*');
个中为了轻便此处将postMessage
的第二个参数设为了'*'
,你也可以设为 iframe 的 URL。iframe 收到音讯后,会运用某种跨页面音讯通讯手艺在一切 iframe 间同步音讯,例以下面运用的 Broadcast Channel:
/* iframe 内代码 */
const bc = new BroadcastChannel('AlienZHOU');
// 收到来自页面的音讯后,在 iframe 间举行播送
window.addEventListener('message', function (e) {
bc.postMessage(e.data);
});
其他 iframe 收到关照后,则会将该音讯同步给所属的页面:
/* iframe 内代码 */
// 关于收到的(iframe)播送音讯,关照给所属的营业页面
bc.onmessage = function (e) {
window.parent.postMessage(e.data, '*');
};
下图就是运用 iframe 作为“桥”的非同源页面间通讯情势图。
个中“同源跨域通讯计划”可以运用文章第一部份提到的某种手艺。
总结
今天和人人分享了一下跨页面通讯的各种体式格局。
关于同源页面,罕见的体式格局包含:
- 播送情势:Broadcast Channe / Service Worker / LocalStorage + StorageEvent
- 同享存储情势:Shared Worker / IndexedDB / cookie
- 口口相传情势:window.open + window.opener
- 基于服务端:Websocket / Comet / SSE 等
而关于非同源页面,则可以经由过程嵌入同源 iframe 作为“桥”,将非同源页面通讯转换为同源页面通讯。
本文在分享的同时,也是为了抛转引玉。假如你有什么其他主意,迎接一同议论,提出你的看法和主意~
对文章感兴趣的同砚迎接关注
我的博客 >> https://github.com/alienzhou/blog