浏览器跨页面通信方案
作者: GZHDEV 2021/1/6
前言: 前几天搞了web worker的相关知识点, 今天来看看一个另一个比较有趣的东西, 跨页面通信的实现方案. 这篇文章的篇幅挺长的, 所以方法也是比较齐全的, 如果你想了解浏览器跨页面通信的方案, 读完这篇足够了.❤️
能想到的方案:
- BroadcastChannel
- postMessage
- localStorage
- SharedWorker
- SSE
- websocket
- service worker
- MQTT
- cookie
- cookieStore
- indexedDB
1. Broadcast Channel
第一个要介绍的broadcast channel, 这个api可能知道它的人并不会特别多, 但是在跨页面通信的时候却十分的简单, 就几个简单的方法. 但是broadcast channel只支持同源下的跨页面通信, 多说无益, 来看看呗
<!-- page1.html -->
<h1>page1</h1>
<script>
const bc = new BroadcastChannel('first-channel');
bc.onmessage = e => {
console.log('page1: ', e);
}
setTimeout(() => {
bc.postMessage('hello every one, this message from page1.html');
bc.close();
}, 2000);
</script>
<!-- page2.html -->
<h1>page2</h1>
<script>
const bc = new BroadcastChannel('first-channel');
bc.onmessage = e => {
console.log('page2: ', e);
bc.postMessage('this is message from page2.html');
}
</script>
具体步骤:
- 通过BroadcastChannel构造函数实例化一个实例, 传入消息通道名称
- 创建一个消息通信通道, 通过两个简单的接受方法进行通信
- postMessge发送消息, onmessage接收消息
特别简单对不对~ 那继续
2. postMessage
接下来介绍下postMessge, 这货兼容性超级好, 还简单, 能满足需求就用它用它
看看这兼容性, 稳得不行, 看看语法↓
otherWindow.postMessage(message, targetOrigin, [transfer]);
优缺点: 必须是自己打开的窗口, 并且能获取到窗口的句柄. 但是可以跨域通信.
鄙人也不是很善言辞, 直接就上demo看看吧
<!-- page1.html -->
<h1>page1</h1>
<button id="btn">send</button>
<script>
const child = window.open('page2.html');
const btn = document.querySelector('#btn');
btn.addEventListener('click', () => {
// 跨域需要填写origin
// child.postMessage('hello', 'http://test2.local.com:8080');
// child.postMessage('hello', '*')
child.postMessage('hello');
console.log('send ok!');
});
</script>
<!-- page2.html -->
<h1>page2</h1>
<script>
window.addEventListener('message', e => {
console.log('page2: ', e);
// 不能直接获取, 但是可以e.source.postMessage回复消息到主窗口
// console.log('source: ', e.source);
})
window.addEventListener('storage', e => {
console.log('storage: ', e);
})
</script>
postMessage可以安全的实现跨页面通信(支持跨域通信)
安全问题
如果不希望获取来自其他网站或者浏览器插件的信息, 不要在页面上监听messge事件就好啦.
3. localStorage
localstorage, 你没看错就是本地存储的localstorage, 他还有个兄弟叫sessionStorage, 区别是什么? 区别是localStorage你不手动的去清除, 就一直存在你的电脑硬盘里面, 而sessionStorage的生命周期只有会话期间存在, 当你把页面关闭只有就自动的清除掉了.
而利用localstorage的storage
事件可以进行跨越通信, 当localstorage变化的时候就会触发storage
事件, 但是得注意一点, localstorage的storage
事件只有跨页面才能监听到, 本页面是无效的, 想实现本页面监听localstorage的话也挺简单, 自己重写下localstorage的api, 手动dispatch事件就好了, 说那么多废话, 不如直接来看看demo
<!-- page1.html -->
<h1>page1</h1>
<button class="btn">set</button>
<script>
const btn = document.querySelector('.btn');
// 修改localstorage的页面想监听storage变化得重写setItem
window.addEventListener('storage', e => {
console.log('storage: ', e);
})
// 重写setItem方法
localStorage.setItem = (key, value) => {
// 自定义事件
const event = new CustomEvent('storage', {
detail: {
oldData: { [key]: localStorage.getItem(key) },
newData: { [key]: value },
localData: { ...localStorage },
}
});
localStorage[key] = value;
// 完成原生setItem的功能后, 手动dispatch事件
window.dispatchEvent(event);
}
btn.addEventListener('click', () => {
localStorage.setItem('hello', 'guozhenghong');
})
</script>
<!-- page2.html -->
<h1>page2</h1>
<script>
window.addEventListener('storage', e => {
console.log(e);
})
</script>
注意事项
- 触发写入操作的页面下的storage监听器不会被触发
- storage事件只有在发生改变的时候才会触发,即重复设置相同值不会触发storage监听
- safari隐身模式下无法设置localStorage值
4.SharedWorker
这第四个要介绍的跨页面通信方案, 就是这个SharedWorker了, 实现跨页面通信的原理是这个API会公用一个worker, 通过同一个port去通信, 注意了, 这货也不支持跨域通信, 想了解更多SharedWorker的细节可以去MDN官网查看, 或者我前面讲worker的文章了解. 上demo
<!-- page1.html -->
<h1>page1</h1>
<input type="text" class="value-box">
<h2 class="content"></h2>
<button class="set-message">setMessage</button>
<button class="get-message">getMessage</button>
<script>
const $ = el => document.querySelector(el);
const sw = new SharedWorker('worker.js');
$('.set-message').addEventListener('click', () => {
sw.port.postMessage($('.value-box').value);
console.log('set ok!');
});
// 采用DOM3级别的addEventListener进行监听,必须port.start激活
$('.get-message').addEventListener('click', () => {
sw.port.postMessage('get');
console.log('get ok!');
});
sw.port.addEventListener('message', ({data}) => {
$('.content').textContent = `value: ${data}`;
console.log(data);
});
sw.port.start();
</script>
<!-- page2.html -->
<h1>page2</h1>
<input type="text" class="value-box">
<h2 class="content"></h2>
<button class="set-message">setMessage</button>
<button class="get-message">getMessage</button>
<script>
const $ = el => document.querySelector(el);
const sw = new SharedWorker('worker.js');
$('.set-message').addEventListener('click', () => {
sw.port.postMessage($('.value-box').value);
console.log('set ok!');
});
$('.get-message').addEventListener('click', () => {
sw.port.postMessage('get')
console.log('get ok!');
});
sw.port.addEventListener('message', ({data}) => {
console.log(data);
$('.content').textContent = `value: ${data}`;
});
sw.port.start();
</script>
// worker.js
let store = null;
onconnect = (e) => {
const port = e.ports[0];
handleMessage(port);
}
// 通过定义一个全局变量store来缓存来自页面的数据
const handleMessage = (port) => {
port.onmessage = (e) => {
switch(e.data) {
case 'get':
port.postMessage(store);
break;
default:
store = e.data;
break;
}
}
}
上面的demo实现的功能是, 两个页面之间信息的发送和接受, 公用一个worker.js来处理逻辑.
5. Websocket
众所周知的, http请求是由客户端请求, 服务端进行响应, http请求是无状态, 走http协议, 服务器以前是没有能力主动往客户端推送数据的, 等下要说的SSE这货可以, 先由客户端发起一次请求, 但是现在要说的websocket就很强了, 它是建立在TCP连接上的应用程序协议, 和HTTP并没有太大关系, 但是在浏览器中, 需要走http进行握手连接和断开连接.
websocket使浏览器有了双向通信的能力, websocket也是html5规范的一部分, 支持HTML5的浏览器已经集成了websocket的API, 但是这里要展示的demo用的是经过二次封装的socket.io客户端, 服务端采用的是nodeJS,
// index.js
const fs = require("fs");
const server = require("http").createServer((req, res) => {
if (/html$/i.test(req.url.toLowerCase())) {
fs.createReadStream('./public' + req.url).pipe(res);
}
});
const io = require('socket.io')(server);
io.on('connection', client => {
console.log('client connect to server!');
// 使用io发送的是广播信息, 所有连接的客户端都能监听到
client.on('broadcast-channel', data => {
io.emit('broadcast-channel', data);
});
client.on('disconnect', e => {
console.log('disconnect: ', e);
});
});
server.listen(3000);
<!-- page1.html -->
<h1>page1</h1>
<input type="text" class="value-provider">
<h1>received ↓: <p class="content-box" style="color: rebeccapurple;"></p></h1>
<button class="send-button">send message</button>
<!-- 这是socket.io基于express暴露的js文件 -->
<script src="/socket.io/socket.io.js"></script>
<script>
const contentBox = document.querySelector('.content-box');
const sendButton = document.querySelector('.send-button');
const valueProvider = document.querySelector('.value-provider');
// 建立socket连接
const socket = io('ws://localhost:3000');
socket.on('connect', function() {
console.log('connect to server.');
// 监听broadcast-channel事件, 接收服务端发来的数据
this.on('broadcast-channel', data => contentBox.textContent = data);
// emit往服务端发送数据
sendButton.addEventListener('click', () => socket.emit('broadcast-channel', valueProvider.value));
});
</script>
<!-- page2.html -->
<h1>page2</h1>
<input type="text" class="value-provider">
<h1>received ↓: <p class="content-box" style="color: rebeccapurple;"></p></h1>
<button class="send-button">send message</button>
<script src="/socket.io/socket.io.js"></script>
<script>
const contentBox = document.querySelector('.content-box');
const sendButton = document.querySelector('.send-button');
const valueProvider = document.querySelector('.value-provider');
const socket = io('ws://localhost:3000');
socket.on('connect', function() {
console.log('connect to server.');
this.on('broadcast-channel', data => contentBox.textContent = data);
sendButton.addEventListener('click', () => this.emit('broadcast-channel', valueProvider.value));
});
</script>
好了, 本文的主要内容是跨页面通信, websocket的知识点非常多, 这里就不展开聊了, 后面有空在单独的写写对websocket的理解, websocket的应用很广, 比如实时弹幕, 即时通讯聊天等等,都可以用websocket来实现.
6. Server-sent events(SSE)
日常进行前后端数据交互过程中, 大部分情况都是客户端主动的对服务端发起请求, 服务端才会返回数据, 而服务端随时随地主动的往客户端推送消息的技术, 在web里除了websocket
之外, 这里提到的Server-Sent Events(SSE)也是, 而且和websocket不同的事
websocket只是连接和断开是时, 走http进行握手和挥手
Server-Sent Events是完全走的http, 但是严格来说http是不支持服务器主动推送数据的, 但是Server-Sent Events算是一种变通的做法, 在客户端发起请求后, 服务端返回的信息告诉客户端接下来的是流信息(Stream), 客户端收到就不关闭连接, 一直在等服务器的下一次流信息.
好了知道大致流程我们还是看看demo,
// index.js
const express = require('express');
const path = require('path');
const app = express();
app.use(express.static(path.join(__dirname, './public')));
let clients = {};
// 通过/register进行注册
app.get('/register', (req, res) => {
const {id = 'un'} = req.query;
clients[id] || (clients[id] = res);
// SSE必须返回text/event-stream的content-type
res.setHeader('Content-Type', 'text/event-stream');
// 返回的数据格式, data开头
res.write('data: register success!\n\n');
req.on('aborted', () => clients = []);
});
// 需要通知的信息
app.get('/notice', (req, res) => {
const {message} = req.query;
// 通知register的客户端
for (let ki in clients) {
clients[ki].write(`data: ${message}\n\n`);
}
res.status(200).end();
})
app.listen(9999, console.log('http://localhost:9999'));
<!-- register.html -->
<h1>sse demo</h1>
<h2></h2>
<script>
const es = new EventSource('/register?id=one', { withCredentials: true });
const messageBox = document.querySelector('h2');
es.onmessage = e => {
// register后,监听sse发来的数据
console.log('onmessage: ', e);
messageBox.textContent = `message: ${e.data}`;
}
es.addEventListener('close', () => {
es.close()
}, false)
</script>
<!-- notice.html -->
<script src="./axios.min.js"></script>
<script>
const sendButton = document.querySelector('.sendButton');
const input = document.querySelector('.inputBox');
const sendMessage = async (message) => {
const res = await axios.get('/notice', {
params: {
message,
}
});
console.log('res: ', res);
}
sendButton.addEventListener('click', () => {
console.time('send');
sendMessage(input.value || 'this is message from page2.html');
console.timeEnd('send');
})
</script>
好了SSE这东西是HTML5规范的一部分, 在IE系列老旧的浏览器中是不支持的, 使用的时候得要根据业务场景具体分析哦.
7. ServiceWorker
Service workers 本质上充当 Web 应用程序、浏览器与网络(可用时)之间的代理服务器。这个 API 旨在创建有效的离线体验,它会拦截网络请求并根据网络是否可用采取来适当的动作、更新来自服务器的的资源。它还提供入口以推送通知和访问后台同步 API。
依赖
Service Worker 作为现代浏览器的高级特性,依赖于 fetch 、promise 、CacheStorage、Cache API、等浏览器的基础能力, Cache 提供了 Request / Response 对象对的存储机制。CacheStorage 则提供了存储 Cache 对象的机制。
功能和特性:
- 一个独立的 worker 线程,独立于当前网页进程,有自己独立的 worker context。
- 一旦被 install,就永远存在,除非被手动 unregister
- 用到的时候可以直接唤醒,不用的时候自动睡眠
- 可编程拦截代理请求和返回,缓存文件,缓存的文件可以被网页进程取到(包括网络离线状态)
- 离线内容开发者可控
- 能向客户端推送消息
- 不能直接操作 DOM
- 必须在 HTTPS 环境下才能工作
- 异步实现,内部大都是通过 Promise 实现
service worker是pwa内容中的一部分, 这里主要讨论如何跨页面通信, 关于service worker的内容后面写pwa相关的知识点再详谈, 现在我们看看用service worker通信的小demo
<!-- page1.html -->
<h1>page1</h1>
<button>send</button>
<script>
// 注册service
navigator.serviceWorker.register('sw.js').then(() => {
console.log('register success!');
});
document.querySelector('button').addEventListener('click', () => {
// 通过controller往worker发送信息
navigator.serviceWorker.controller.postMessage('hello');
});
</script>
<!-- page2.html -->
<h1>page2</h1>
<script>
navigator.serviceWorker.register('sw.js').then(() => {
console.log('register success!');
});
navigator.serviceWorker.addEventListener('message', ({data}) => {
console.log('message: ', data);
});
</script>
// sw.js
self.addEventListener('message', async event => {
// 获取所有注册sw.js的clients
const clients = await self.clients.matchAll();
clients.forEach(client => client.postMessage('sw message: xx'));
})
除了上面的方式外, 还有一种一般骚的方法, 在service worker里跨页面通信--MessageChannel
这是一个平常使用频率不是很高的API, 实例化对象有两个通道, port1
和port2
, 每个通道都可以监听message
事件, 都可以通过postMessage
发送消息.
port1发送的消息可以在port2进行message监听
port2发送的消息可以在port1进行message监听
在service worker里借助MessageChannel可以这样用
const mc = new MessageChannel();
// 监听消息
mc.port1.onmessage = function(e) {
console.log(e.data);
}
navigator.serviceWorker.controller.postMessage('hello', [mc.port2]);
self.addEventListener('message', e => {
// 通过e.ports获取mc.part2通道,发送信息
e.ports[0].postMessage('xx');
});
以上就是基于service worker进行跨页面通信的方法, 但是值得注意的一点是, service woker进行跨页面通信是不支持跨域的.
8. MQTT消息队列
MQTT(Message Queuing Telemetry Transport,消息队列遥测传输协议),是一种基于发布/订阅(publish/subscribe)模式的"轻量级"通讯协议,是 TCP/IP 的再封装,由 IBM 在 1999 年发布.MQTT 最大优点在于低开销少流量的实现网络通信, 物联网上MQTT协议用的就比较多, 是作为一名有追求的工程师, 我们得学.
这里就不自己搭建broker了, 直接使用在线的免费的消息队列看看吧, 有兴趣的可以自己用
activemq, rocketmq...这些消息队列去搭建自己的broker, 废话少说看demo
MQTT协议中有以下几个主要的概念需要先了解:
- broker 就是消息队列代理服务器
- topic 主题, subscribe和publish时指定
- subscribe 进行主题的订阅
- publish 进行主题内容的发布
首先我们下载mqtt.js这个包
npm install mqtt
# 或者使用yarn进行安装
yarn add mqtt
然后就是巴拉巴拉的简单demo代码了
<!-- page1.html -->
<h1>page1</h1>
<input type="text" class="value-provider">
<h1>content: <p class="content"></p></h1>
<button class="send-button">send</button>
<script src="../node_modules/mqtt/dist/mqtt.js"></script>
<script>
const $ = el => document.querySelector(el);
const publishTopic = 'from-page1-message';
const subscribeTopic = 'from-page2-message';
// broker地址, 在浏览器里mqtt目前只能通过websocket进行连接了
const brokerAddress = 'ws://broker.emqx.io:8083/mqtt';
// 建立brocker连接
const client = mqtt.connect(brokerAddress);
client.on('connect', () => {
console.log('connected to broker!');
// 订阅主题, 通过publish可以发布主题
client.subscribe(subscribeTopic, (err) => {
if (!err) {
console.log('successfully connected to mqtt server');
$('.send-button').addEventListener('click', () => client.publish(publishTopic, $('.value-provider').value));
}
});
});
// 监听到message的前提是, 已经订阅了主题
client.on('message', (topic, message, packet) => {
console.log('received message: ', message.toString(), packet);
$('.content').textContent = message.toString();
});
</script>
<!-- page2.html -->
<h1>page2</h1>
<input type="text" class="value-provider">
<h1>content: <p class="content"></p></h1>
<button class="send-button">send</button>
<script src="../node_modules/mqtt/dist/mqtt.js"></script>
<script>
const $ = el => document.querySelector(el);
const publishTopic = 'from-page2-message';
const subscribeTopic = 'from-page1-message';
const brokerAddress = 'ws://broker.emqx.io:8083/mqtt';
const client = mqtt.connect(brokerAddress);
client.on('connect', () => {
console.log('connected to broker!');
client.subscribe(subscribeTopic, (err) => {
if (!err) {
console.log('successfully connected to mqtt server');
$('.send-button').addEventListener('click', () => client.publish(publishTopic, $('.value-provider').value));
}
});
});
client.on('message', (topic, message, packet) => {
console.log('received message: ', message.toString(), packet);
$('.content').textContent = message.toString();
});
</script>
这里还可以推荐一款美观又好友的MQTT客户端软件, MQTTX
EMQ官网 www.emqx.io/cn/mqtt/pub…
9. IndexedDB
什么是IndexedDB? 看看的两个大写字母, DB耶, 就是DataBase, 网页自己的数据库, 它能用来干什么呢? 小傻瓜, 数据库当然是用来存储数据的啦, 你可能会说localstorage不是也可以用来做数据存储么? 还好这个IndexedDB干嘛, 因为localstorage对存储的数据量是有限制的, 几M到几十M之间, 不同浏览器的限制不一样, 但是IndexedDB的话就大了, 它的数据存储量差不多由你的硬盘大小来决定, IndexedDB 是一种底层 API, 用于在客户端存储大量的结构化数据(也包括文件/二进制大型对象(blobs)), 该 API 使用索引实现对数据的高性能搜索。虽然 Web Storage在存储较少量的数据很有用,但对于存储更大量的结构化数据来说力不从心。而 IndexedDB 提供了这种场景的解决方案。
indexedDB基本使用步骤
- 打开数据库
- 在数据库中创建一个对象仓库(object store)
- 启动一个事务,并发送一个请求来执行一些数据库操作,像增加或提取数据等
- 通过监听正确类型的 DOM 事件以等待操作完成
- 在操作结果上进行一些操作(可以在 request 对象中找到)
虽然这个方案可以实现跨页面通信, 但其实有点鸡肋的, indexDB的主要用途并不是在这里,
如果使用indexedDB实现跨页面通信呢, 大致思路就是一个同源的页面写入数据库, 另外一个同源的页面进行轮训读取indexedDB中的数据, 实现跨页面数据通信, 以及后面要说的cookie实现跨页面通信原理也是一样, 轮训, 又low又低效.
看是indexedDB还是比较牛的一个东西, 学些无妨, 看看具体怎么用吧
<!-- demo.html -->
<script>
const dbName = 'demo_db';
// 打开数据库, 没有就自动新建, 版本指定为2, 不指定为默认值1, 不要填浮点数
const request = indexedDB.open(dbName, 2);
// 监听事件
request.onerror = e => console.error('error: ', e);
request.onsuccess = e => console.log('success: ', e);
request.onupgradeneeded = e => {
console.log('upgradeneeded: ', e);
// 建立连接后获取 IDBOpenDBRequest 对象
const db = e.target.result;
// 创建objectStore存储空间
const objectStore = db.createObjectStore('firstCollection', {keyPath: 'id', autoIncrement: true});
/**
* 创建索引
* createIndex能够给当前的存储空间设置一个索引。它接受三个参数:
* 第一个参数,索引的名称。
* 第二个参数,指定根据存储数据的哪一个属性来构建索引。
* 第三个属性, options对象,其中属性unique的值为true表示不允许索引值相等。
*/
objectStore.createIndex('age', 'age', {unique: false});
objectStore.createIndex('name', 'name', {unique: true});
/**
* 创建数据库事务
* 事务支持三种模式:
* readOnly,只读。
* readwrite,读写。
* versionchange,数据库版本变化。
*/
objectStore.transaction.oncomplete = e => {
console.log('transaction complete: ', e);
const collectionTransaction = db.transaction(['firstCollection'], 'readwrite').objectStore('firstCollection');
// 新增数据
const awaitInsertData = [
{name: 'gzh', age: 18},
{name: 'xx', age: 19},
{name: 'hello', age: 3},
]
awaitInsertData.forEach(data => collectionTransaction.add(data));
// 查询数据(通过组件查询数据)
const queryByKey = collectionTransaction.get(2);
queryByKey.onsuccess = e => {
const queryResult = e.target.result;
console.log('queryResult(key): ', queryResult);
}
// 查询数据(通过索引查询数据)
const index = collectionTransaction.index('name');
console.log('index: ', index);
const queryByIndex = index.get('gzh');
queryByIndex.onsuccess = e => {
const queryResult = e.target.result;
console.log('queryResult(index): ', queryResult);
// 修改数据
queryResult.age = 19;
collectionTransaction.put(queryResult);
// 删除数据
collectionTransaction.delete(3);
}
}
}
</script>
demo代码都有点长了, 但是也合理, 毕竟indexedDB-----前端自己的数据库, 数据库这么强大的功能, 东西多点也正常.
10. Cookie & cookieStore
前面输过cookie和indexedDB一样跨页面通讯的实现是基于轮训实现的, 大多数情况比较鸡肋, 所以就不说了, 而且cookie在浏览器里的操作也是比较的怪异, 长久以来深受开发者们的鄙视和吐槽, 可是最近我关注到, chrome推出了一个叫cookieStore
的API来解决这个问题, 而且cookieStore
在cookie改变时change是跨页面个监听的, 这里也提一下, cookieStore也挺简洁易学的. 就是这个兼容性目前太差了, 但是相信在chrome的牵头下, 以后一定会火的, 成为新的cookie管理api指日可待.
注意点:
需要在https
下才能使用
talk is cheap, 看看我写的小demo
<!-- page1.html -->
<h1>page1</h1>
<button class="btn">set cookie</button>
<script>
const btn = document.querySelector('.btn');
cookieStore.addEventListener('change', e => {
console.log("page1: ", e);
});
btn.addEventListener('click', function() {
cookieStore.set({name: 'xx', value: 'xx'});
});
</script>
<!-- page2.html -->
<h1>page2</h1>
<script>
cookieStore.addEventListener('change', e => {
console.log("page2: ", e);
});
</script>
11. 总结
这篇字数比较多, 写的时间的挺久, 但是写的过程中学到了很多东西, 并且对很多旧的知识也加深了记忆和理解, 以后有空会多写技术文章, 在输出中总结和提升, 希望在技术上能走得更高更远.❤️第一次在掘金写发文章有点紧张!, 还有那个有兴趣一起学习可以关注我微信公众哈!