前端跨标签页通信方案(下)
2025年11月19日 22:06
前情
平时开发很少有接触到有什么需求需要实现跨标签页通信,但最近因为一些变故,不得不重新开始找工作了,其中就有面试官问到一道题,跨标签页怎么实现数据通信,我当时只答出二种,面试完后特意重新查资料,因此有些文章
SharedWorker
共享工作线程可以在多个标签页之间共享数据和逻辑,通过postMessage通信
关键代码如下:
标签页1
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SharedWorker0</title>
</head>
<body>
<h1>SharedWorker0</h1>
<button id="communication">SharedWorker0.html 发送消息</button>
<script>
// 主线程
const worker = new SharedWorker('sw.js');
// 发送消息
document.getElementById('communication').addEventListener('click', () => {
worker.port.postMessage('Hello from Tab:SharedWorker0.html');
});
</script>
</body>
</html>
标签页2
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SharedWorker1</title>
</head>
<body>
<h1>SharedWorker1</h1>
<script>
// 主线程
const worker = new SharedWorker('sw.js');
// 接收消息
worker.port.onmessage = (e) => {
console.log('Received:SharedWorker1.html', e.data);
};
</script>
</body>
</html>
sw.js关键代码:
const connections = [];
self.onconnect = (e) => {
const port = e.ports[0];
connections.push(port);
port.onmessage = (e) => {
// 广播给所有连接的页面
connections.forEach(p => p.postMessage(e.data));
};
};
动图演示:
![]()
提醒:
- 同源标签才有效
- 不同页面创建 SharedWorker 时,若指定的脚本路径不同(即使内容相同),会创建不同的 worker 实例
- 页面与 SharedWorker 之间通过 MessagePort 通信,需通过
port.postMessage()发送消息,通过port.onmessage接收消息 - SharedWorker 无法访问 DOM、
window对象或页面的全局变量,仅能使用 JavaScript 核心 API 和部分 Web API(如fetch、WebSocket) - 兼容性一般,安卓webview全系不兼容
Service Worker
专门用于同源标签页通信的 API,创建一个频道后,所有加入该频道的页面都能收到消息
关键代码如下:
标签页1
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ServiceWorker0</title>
</head>
<body>
<h1>ServiceWorker0</h1>
<button id="sendBtn">发送消息</button>
<script>
// 注册ServiceWorker
let swReg;
navigator.serviceWorker.register('ServiceWorker.js')
.then(reg => {
swReg = reg;
console.log('SW注册成功');
});
// 发送消息
document.getElementById('sendBtn').addEventListener('click', () => {
if (swReg && swReg.active) {
swReg.active.postMessage('来自页面0的消息');
}
});
</script>
</body>
</html>
标签页2
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ServiceWorker1</title>
</head>
<body>
<h1>ServiceWorker1</h1>
<script>
// 注册ServiceWorker
navigator.serviceWorker.register('ServiceWorker.js')
.then(() => console.log('SW注册成功'));
// 接收消息
navigator.serviceWorker.addEventListener('message', (e) => {
console.log('---- Received:ServiceWorker1.html ----:', e.data);
});
</script>
</body>
</html>
ServiceWorker.js关键代码
// 快速激活
self.addEventListener('install', e => e.waitUntil(self.skipWaiting()));
self.addEventListener('activate', e => e.waitUntil(self.clients.claim()));
// 消息转发
self.addEventListener('message', e => {
self.clients.matchAll().then(clients => {
clients.forEach(client => {
if (client.id !== e.source.id) {
client.postMessage(e.data);
}
});
});
});
演示动图如下:
![]()
提醒:
- Service Worker 要求页面必须在 HTTPS 环境 下运行(
localhost除外,方便本地开发),这是出于安全考虑,防止中间人攻击篡改 Service Worker 脚本 - Service Worker 有严格的生命周期(安装、激活、空闲、销毁),一旦注册成功会长期运行在后台,更新 Service Worker 需满足两个条件:
- 脚本 URL 不变但内容有差异
- 需在
install事件中调用self.skipWaiting(),并在activate事件中调用self.clients.claim()让新 Worker 立即生效
- Service Worker 的作用域由注册路径决定,默认只能控制其所在路径及子路径下的页面,例如:
/sw.js可控制全站,/js/sw.js默认只能控制/js/路径下的页面,可通过scope参数指定作用域,但不能超出注册文件所在路径的范围 - 可在浏览器开发者工具的 Application > Service Workers 面板进行调试, • 查看当前运行的 Service Worker 状态 • 强制更新、停止或注销 Worker • 模拟离线环境
- 主流浏览器都支持,使用的时候可以通过Is service worker ready?,测试兼容性
window.open + window.opener
如果标签页是通过window.open打开的,可以直接通过opener属性通信
父窗口,打开子窗口的页面关键代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>parent</title>
</head>
<body>
<h1>window.open parent</h1>
<button id="openBtn">打开子窗口</button>
<button id="sendBtn">发送消息</button>
<div id="messageDisplay"></div>
<script>
let childWindow = null;
let messageHandler = null;
// 打开子窗口
document.getElementById('openBtn').addEventListener('click', () => {
// 如果已有窗口,先关闭
if (childWindow && !childWindow.closed) {
childWindow.close();
}
childWindow = window.open('./children.html', 'childWindow');
});
// 发送消息
document.getElementById('sendBtn').addEventListener('click', () => {
if (childWindow && !childWindow.closed) {
// window.location.origin限制接收域名
childWindow.postMessage('Hello child', window.location.origin);
} else {
alert('请先打开子窗口');
}
});
// 接收子窗口的消息
messageHandler = (e) => {
if (e.origin === window.location.origin && e.source !== window) {
document.getElementById('messageDisplay').textContent = '收到消息: ' + e.data;
console.log('父页面收到消息:', e.data);
}
};
window.addEventListener('message', messageHandler);
</script>
</body>
</html>
通过window.open打开的子页面关键代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>children</title>
</head>
<body>
<h1>子窗口</h1>
<button id="replyBtn">回复父窗口</button>
<div id="messageDisplay"></div>
<script>
let messageHandler = null;
// 只在页面加载完成后设置消息监听
window.onload = function() {
// 接收父页面消息
messageHandler = (e) => {
if (e.origin === window.location.origin && e.source !== window) {
console.log('子页面收到消息:', e.data);
// 显示收到的消息
document.getElementById('messageDisplay').textContent = '收到消息: ' + e.data;
window.opener.postMessage('子窗口已收到消息', e.origin);
}
};
window.addEventListener('message', messageHandler);
};
// 手动回复按钮
document.getElementById('replyBtn').addEventListener('click', () => {
if (window.opener) {
window.opener.postMessage('来自子窗口的回复', window.location.origin);
}
});
</script>
</body>
</html>
提醒:
- 允许跨域通信,但必须由开发者显式指定信任的源,避免恶意网站滥用
- 在事件监听的时候记得判断e.source,避免自己发送的事件自己接收了
- 若子窗口被关闭,父窗口中对它的引用(如
childWindow)会变成无效对象,调用其方法会报错 - window.open使用会有一些限制,最好是在事件中使用,有的浏览器还会有权限提示,需要用户同意才行,若
window.open被浏览器拦截(非用户主动触发),会返回null,导致后续通信失败
总结
面试官有提到Service Worker也可以,我面试完后的查询资料尝试了这些方法,都挺顺利的,就是Service Worker折腾了一会才跑通,使用起来相比前面的一些方式,它稍微复杂一些,我觉得用于消息通信只是它的冰山一角,它有一个主要功能就是用来解决一些耗性能的计算密集任务
个人技术有限,如果你有更好的跨标签页通信方式,期待你的分享,你工作中有遇到这种跨标签页通信的需求么,如果有你用的是哪一种了,期待你的留言