持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第19天,点击查看活动详情
🎈大家好,我是
橙橙子,新人初来乍到,请多多关照~📝小小的前端一枚,分享一些日常的学习和项目实战总结~
😜如果本文对你有帮助的话,帮忙点点赞呀!ღ( ´・ᴗ・` )比心~
在上一篇介绍了3种前端跨页面通信的方式,本文介绍另外的跨页面通信方式,“共享存储+轮询模式”。
1. 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绑定监听则不需要。
2. IndexedDB
除了可以利用 Shared Worker 来共享存储数据,还可以使用其他一些“全局性”(支持跨页面)的存储方案。例如 IndexedDB或 cookie。
消息发送方将消息存至 IndexedDB 中;接收方则通过轮询去获取最新的信息。
- 打开数据库连接
function openStore() {
const storeName = 'idb_store';
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 = 'idb_store';
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 = 'idb_store';
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));
}
});
这样,每个页面都肩负起了传递消息的责任,而消息就在这个树状结构中流转了起来。