源 origin = 协议 scheme + 主机名/域名 host + 端口 port
BroadcastChannel:用于同源不同浏览上下文之间进行一对多的定向消息广播。new 一个 bc 实例,设置频道,通过 bc.postMessage 广播消息,其他标签通过监听同频道实例的 message 事件来获取
LocalStorage:通过监听 storage 事件来实现,其他页签改动了 ls 会触发
postMessage:用于不同浏览上下文之间进行一对一的定向消息传递,可以是同源或跨域
MessageChannel:MessageChannel API 可以看作是 postMessage API 的一种更灵活的实现方式,它允许创建独立的双向通信通道,而不需要依赖于 window 或 worker 对象。new 一个 mc 实例,实例具有两个 port,可以通过 port.postMessage 一对一发消息,通过 port.onmessage 监听其他 port 发送的消息。React 调度器就是使用这个 API 使内部的更新与渲染任务参与到浏览器的事件循环的
Shared Worker:传入worker 脚本的 url 以及 name 标识,相同的标识会共享同一个 SharedWorker 后台线程。通过 onconect 事件参数 e.ports[0] 获取页面连接的 port,与 MessageChannel 类似,通过 sw.port.postMessage 广播消息,页面通过 sw.port.onmessage 事件监听消息
1 BroadcastChannel
Broadcast Channel 是一个较新的 Web API,用于在不同的浏览器窗口、标签页或框架之间实现跨窗口通信。它基于发布-订阅模式,允许一个窗口发送消息,并由其他窗口接收。
前提:同源,频道名字一致
- tab1 使用实例的 postMessage 发送消息
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document1</title>
</head>
<body>
<button>点我发送消息</button>
</body>
<script>
const channel1 = new BroadcastChannel('111')
const btn = document.querySelector('button')
btn.onclick = () => {
channel1.postMessage({data: 'message from tab1'})
console.log('发送成功')
}
</script>
</html>
- tab2 监听实例的 message 事件,接收消息
const c1 = new BroadcastChannel('111')
c1.addEventListener('message', (e) => {
console.log(e.data)
})
- 注意在合适的时间调用
channel.close()
断开频道连接
2 LocalStorage
前提:同源
- tab1 修改 LocalStorage
const btn = document.querySelector('button')
btn.onclick = () => {
localStorage.setItem('text', 'message from tab1')
console.log('修改成功')
}
- tab2 监听
storage
事件,即监听 LocalStorage 变动
window.addEventListener('storage', (event) => {
let { key, oldValue, newValue } = event
console.log('receive message from ls', key, oldValue, newValue)
});
相似的还有 SessionStorage 与 IndexDB
3 postMessage
前提:主窗口和弹出的新窗口之间(同源)
也可以用于不同页签
- tab1 打开新标签页后,通过调用新标签页实例的 postMessage 方法向新标签页发送消息
const btn1 = document.querySelector('#open')
const btn2 = document.querySelector('#message')
let newTab = null
btn1.onclick = () => {
// window.open(url, target上下文名称, windowFeatures窗口特性列表)
newTab = window.open('2.html', "123", "height=600,width=600,top=20")
}
btn2.onclick = () => {
const data = { value: 'message from tab1 ' }
newTab?.postMessage(data, "*")
}
- tab2 监听 message 事件即可
window.addEventListener('message', (e) => {
// 注意屏蔽插件或其他程序的 message 信息
// 屏蔽 react-devtools-content-script 的消息
if(e?.data?.source === 'react-devtools-content-script') {
return
}
console.log(e.data)
})
4 Shared Worker
SharedWorker API 是 HTML5 中提供的一种多线程解决方案,它可以在多个浏览器 TAB 页面之间共享一个后台线程,从而实现跨页面通信。
初始化 shared worker
实例化 Worker ,传入worker 脚本的 url 以及 name 标识,相同的标识会共享同一个 SharedWorker
// html
const sw = new SharedWorker('./sharedWorker.js', 'testWorker')
Shared Worker 实例存在一个 port
属性,相当于当前连接的 tab 页面,用于与共享线程通信
页面监听消息
页面中使用 port.onmessage
监听共享线程传递过来的消息
// html
sw.port.onmessage = (e) => alert(e.data)
页面发送消息
页面中使用 port.postMessage()
向共享线程发送消息
// html
sw.port.postMessage({ tag: 'close' })
worker 处理与分发消息
使用 onconnect
或 self.onconnect
监听页面的连接,通过事件参数 e.port[0]
获取与连接事件关联的第一个 MessagePort 对象,即当前连接的页面
使用 port.onmessage
接收并处理页面传递过来的消息;使用 port.postMessage
向页面发消息
// sharedWorker.js
onconnect = e => {
const port = e.ports[0]
// 可以将 port 缓存起来,用于后续通信
!portsPool.includes(port) && portsPool.push(port)
port.onmessage = (e) => {
console.log('message from tab', e.data)
}
port.postMessage(xxx)
}
调试 shared worker
因为 SharedWorker
的作用域中没有 window
对象,所以 console
、alert
等方法都是无法使用的
如果我们需要调试 SharedWorker
,可以在浏览器地址栏中输入 chrome://inspect/#workers
,这样就可以看到当前页面中的SharedWorker
。
关闭 shared worker
页面断开链接,通知 worker 关闭;页面关闭时,中断连接
// html
sw.port.postMessage({ tag: 'close' });
sw.port.close();
// 或者
window.onbeforeunload = () => {
sw.port.postMessage({ tag: 'close' });
sw.port.close();
};
worker 删除内部缓存即可
// sharedWorker.js
const index = portsPool.findIndex(item => item === port);
if (~index) {
portsPool.splice(index, 1);
}
当所有创建 SharedWorker
的页面关闭之后,那么 SharedWorker
的生命就走到了尽头,否则它就会一直常驻。
实战:广播与指定页面发送消息
由于 port 没有标识,各标签页之间也无法直接实现精准通信
所以只能通过广播的方式,各标签页通过的 message 的某个字段来区分是否是发给自己的消息
<!-- 1.html -->
<body>
<div>广播消息</div>
<hr />
<div id="broadcast_info"></div>
<button id="broadcast_btn">广播消息</button><br />
<button id="send">向tab2发送消息</button>
<script>
const broadcastBtn = document.querySelector('#broadcast_btn')
const sendBtn = document.querySelector('#send')
const broadcastInfo = document.querySelector('#broadcast_info')
// 初始化
const sw = new SharedWorker('./sharedWorker.js', 'test worker')
sw.port.onmessage = (e) => {
if (e.data.tag === 'broadcast') {
broadcastInfo.innerHTML = e.data.info
}
}
broadcastBtn.addEventListener('click', e => {
sw.port.postMessage({ tag: 'broadcast', info: '来自 tab1 的广播' })
})
sendBtn.addEventListener('click', e => {
sw.port.postMessage({ tag: 'tab2', info: 'tab1 发送给 tab2 的消息' })
})
window.onbeforeunload = () => {
// 取消该port在共享线程中的存储[广播用的]
sw.port.postMessage({ tag: 'close' });
// 关闭与共享线程的连接
sw.port.close();
};
</script>
</body>
<!-- 2.html -->
<body>
<div>广播消息</div>
<hr />
<div id="broadcast_info"></div>
<div>来自其他 tab 的消息</div>
<div id="message"></div>
<button id="broadcast_btn">广播消息</button><br />
<script>
const broadcastBtn = document.querySelector('#broadcast_btn')
const broadcastInfo = document.querySelector('#broadcast_info')
const messageInfo = document.querySelector('#message')
// 初始化
const sw = new SharedWorker('./sharedWorker.js', 'test worker')
sw.port.onmessage = (e) => {
if (e.data.tag === 'broadcast') {
broadcastInfo.innerHTML = e.data.info
}
if(e.data.tag === 'tab2') {
messageInfo.innerHTML = e.data.info
}
}
broadcastBtn.addEventListener('click', e => {
sw.port.postMessage({ tag: 'broadcast', info: '来自 tab2 的广播' })
})
window.onbeforeunload = () => {
// 取消该port在共享线程中的存储[广播用的]
sw.port.postMessage({ tag: 'close' });
// 关闭与共享线程的连接
sw.port.close();
};
</script>
</body>
// sharedWorker.js
const portsPool = []
// { tag: 'tab1' | 'tab2' | 'broadcast' | 'close', info: 'xxx' }
onconnect = e => {
const port = e.ports[0]
!portsPool.includes(port) && portsPool.push(port)
port.onmessage = (e) => {
if (e.data.tag === 'close') {
const index = portsPool.findIndex(item => item === port);
if (~index) {
portsPool.splice(index, 1);
}
} else {
portsPool.forEach(port => {
port.postMessage(e.data)
})
}
}
}
5 MessageChannel
const channel = new MessageChannel();
const output = document.querySelector(".output");
const iframe = document.querySelector("iframe");
iframe.addEventListener("load", onLoad);
function onLoad() {
// 监听 port1 的 message 事件
channel.port1.onmessage = onMessage;
// 通过 postMessage 向 port2 发送消息
iframe.contentWindow.postMessage("Hello from the main page!", "*", [
channel.port2,
]);
}
// Handle messages received on port1
function onMessage(e) {
output.innerHTML = e.data;
}
6 非同源
借助服务,例如 WebSocket