一道出现频率较高的面试题:在浏览器中,如何实现跨 标签 页通信?
一、概述
简单来讲,就是某个标签页能够实时地发消息给另一个标签页,并且另一个标签页能及时做出响应。
二、常见的实现方案
一)LocalStorage 监听storage事件(SessionStorage 也可)
概述
-
当前页面使用的 storage 被其他页面修改时会触发 StorageEvent 事件 (同源) 。
-
事件在同一个域下的不同页面之间触发,即在 A 页面注册了 storge 的监听处理,只有在跟 A 同域名下的 B 页面操作 storage 对象,A 页面才会被触发 storage 事件。
-
StorageEvent对象的属性如下:
- type: 事件名
- canBubble: 布尔值,代表是否可以通过DOM冒泡
- cancelable: 布尔值,代表是否可以注销事件
- key: 事件结果时被改变的值对应的属性名称
- oldValue: 旧值
- newValue: 新值
- url: 事件初始化时页面的url
- storageArea: 发生在哪个storage对象上
使用示例
PS:本地调试时,需要启动一个开发服务器来调试页面。本地文件无法触发 storage 事件
- 页面A
xml
<body>
<script>
// localStorage.info1 = "测试storage事件--1";
/// localStorage.info2 = "测试storage事件--2";
localStorage.setItem("info1", "测试storage事件--1");
localStorage.setItem("info2", "测试storage事件--2");
console.log("设置成功啦!");
</script>
</body>
- 页面B
xml
<body>
<script>
const info1 = localStorage.getItem("info1");
const info2 = localStorage.getItem("info2");
console.log(info1, info2);
window.addEventListener("storage", (e) => {
console.log(`${e.key}从${e.oldValue}被修改为了${e.newValue}`);
console.log(e);
}, true);
</script>
</body>
二)BroadCast Channel
概述
- Broadcast Channel API 可以实现 同 **源** 下浏览器不同窗口,Tab 页,frame 或者 iframe 下的 浏览器上下文 (通常是同一个网站下不同的页面) 之间的简单通讯。
- BroadcastChannel 接口代理了一个命名频道,可以让指定 origin 下的任意 browsing context 来订阅它。它允许同源的不同浏览器窗口,Tab 页,frame 或者 iframe 下的不同文档之间相互通信。通过触发一个 message 事件,消息可以广播到所有监听了该频道的 BroadcastChannel 对象。(此特性在 Web Worker 中可用)
使用示例
- 页面A
xml
<body>
<div id="app">
<input type="text" id="inpEl" />
<button id="btn1">发送数据</button>
<button id="btn2">关闭</button>
</div>
<script>
// 创建一个链接到命名频道的对象。
const channel = new BroadcastChannel("channel-demo");
btn1.addEventListener("click", () => {
// 向所有监听了相同频道的 BroadcastChannel 对象发送一条消息,消息内容可以是任意类型的数据。
channel.postMessage({ data: inpEl.value });
})
btn2.addEventListener("click", () => {
// 关闭频道对象,告诉它不要再接收新的消息,并允许它最终被垃圾回收
channel.close();
})
</script>
</body>
- 页面B
xml
<body>
<script>
const channel = new BroadcastChannel("channel-demo"); //要接收到数据,BroadcastChannel对象的名字必须相同
// 当频道收到一条消息时触发。 也可以使用 onmessage 属性访问。
channel.addEventListener("message", (event) => {
console.log(event.data);
});
// 当频道收到一条无法反序列化的消息时触发。 也可以使用 onmessageerror 属性访问。
channel.addEventListener("messageerror", (event) => {
console.error(event);
});
</script>
</body>
三)window.open、window.postMessage
概述
- window.postMessage( ) 方法可以安全地实现跨源****通信。通常,对于两个不同页面的脚本,只有当执行它们的页面位于具有相同的协议(通常为https),端口号(443为https的默认值),以及主机 (两个页面的模数 Document.domain设置为相同的值) 时,这两个脚本才能相互通信。window.postMessage( ) 方法提供了一种受控机制来规避此限制,只要正确的使用,这种方法就很安全。
- 从广义上讲,一个窗口可以获得对另一个窗口的引用(比如 targetWindow = window.opener),然后在窗口上调用 targetWindow.postMessage( ) 方法分发一个 MessageEvent 消息。接收消息的窗口可以根据需要自由处理此事件 (en-US)。传递给 window.postMessage( ) 的参数(比如 message )将通过消息事件对象暴露给接收消息的窗口。
使用示例
MDN文档:
- window.open: developer.mozilla.org/zh-CN/docs/...
- window.postMessage: developer.mozilla.org/zh-CN/docs/...
- 页面A
ini
<body>
<button id="openBtn">打开新窗口</button>
<input type="text" name="" id="inpEl">
<button id="sendBtn">发送</button>
<script>
let opener = null; // 保存打开窗口的window对象引用
openBtn.onclick = () => {
opener = window.open("页面B的URL", "hhhxxxhhh", "height=400,width=400,top=10,resizable=yes");
}
sendBtn.onclick = () => {
const data = { data: inpEl.value }
// data 代表的是发送是数据,origin 用来指定哪些窗口能接收到消息事件,其值可以是字符串"*"(表示无限制)或者一个 URI。
opener.postMessage(data, "*");
}
</script>
</body>
- 页面B
xml
<body>
<p>页面B</p>
<script>
// 监听message事件
window.addEventListener('message', function (e) {
console.log(e.data);
}, false);
</script>
</body>
安全问题:
- 如果您不希望从其他网站接收 message,请不要为 message 事件添加任何事件侦听器。 这是一个完全万无一失的方式来避免安全问题。
- 如果您确实希望从其他网站接收 message,请始终使用 origin 和 source 属性验证发件人的身份。任何窗口(包括例如 evil.example.com)都可以向任何其他窗口发送消息,并且您不能保证未知发件人不会发送恶意消息。但是,验证身份后,您仍然应该始终验证接收到的消息的语法。否则,您信任只发送受信任邮件的网站中的安全漏洞可能会在您的网站中打开跨网站脚本漏洞。
- 当您使用 postMessage 将数据发送到其他窗口时,始终指定精确的目标 origin,而不是 *。 恶意网站可以在您不知情的情况下更改窗口的位置,因此它可以拦截使用 postMessage 发送的数据。
四)ServiceWorker
概述
Service Worker 实际上是浏览器和服务器之间的代理服务器,它最大的特点是在页面中注册并安装成功后,运行于浏览器后台,不受页面刷新的影响,可以监听和截拦作用域范围内所有页面的 HTTP 请求。
Service Worker 的作用在于离线缓存,转发请求和网络代理。
Service worker 运行在 worker 上下文:因此它无法访问 DOM,相对于驱动应用的主 JavaScript 线程,它运行在其他线程中,所以不会造成阻塞。它被设计为完全异步;因此,同步 XHR 和 Web Storage 不能在 service worker 中使用。
出于安全考量,Service worker 只能由 HTTPS 承载,毕竟修改网络请求的能力暴露给中间人攻击会非常危险,如果允许访问这些强大的 API,此类攻击将会变得很严重。在 Firefox 浏览器的用户隐私模式,Service Worker 不可用。
它和WebWorker的区别:juejin.cn/s/web%20wor...
使用示例
- 页面A
xml
<body>
<input id="inpEl" type="text">
<button id="sendBtn">发送</button>
<script>
// 1.注册serviceWorker
navigator.serviceWorker.register('sw.js').then(() => {
console.log('serviceWorker 注册成功');
})
sendBtn.onclick = () => {
// 2.向serviceWorker发送消息
navigator.serviceWorker.controller.postMessage({
value: inpEl.value
})
}
</script>
</body>
- 页面B
xml
<body>
<script>
// 1.注册serviceWorker
navigator.serviceWorker.register('sw.js').then(() => {
console.log('serviceWorker 注册成功');
})
// 5.监听navigator.serviceWorker的message事件
navigator.serviceWorker.onmessage = ({ data }) => {
console.log(data);
}
</script>
</body>
- sw.js
javascript
self.addEventListener('message', async (e) => {
// 3.获取所有注册了serviceWorker的client
const clients = await self.clients.matchAll();
clients.forEach((client) => {
// 4.向各个client发送消息(目标client可以通过navigator.serviceWorker的"message"事件接收消息)
client.postMessage(e.data);
});
});
五)WebSocket
概述
WebSocket
对象提供了用于创建和管理 WebSocket 连接,以及可以通过该连接发送和接收数据的 API。- 特点在于:服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,属于服务器推送技术的一种。
使用示例
- server.js(在本地启动一个服务器)
javascript
const { WebSocketServer } = require("ws");
const wss = new WebSocketServer({ port: 8080 });
const clients = []; //暂存所有已连接到服务器的客户端对象
wss.on("connection", (client) => {
if (clients.indexOf(client) !== -1) {
return;
}
// 就将当前连接保存到数组备用
clients.push(client);
console.log("有" + clients.length + "客户端在线");
// 为每个 client 对象绑定 message 事件,当某个客户端发来消息时,自动触发
client.on("message", (msg) => {
console.log("收到消息" + msg);
// 遍历 clients 数组中每个其他客户端对象,并发送消息给其他客户端
for (const cur of clients) {
// 排除自己这个客户端连接,把消息发给别人
cur !== client && cur.send(msg.toString());
}
});
// 当客户端断开连接时触发该事件
client.onclose = () => {
clients.splice(clients.indexOf(client), 1);
console.log("有" + clients.length + "客户端在线");
};
});
console.log('WebSocketServer 已启动,正在监听 8080 端口!');
- 页面A
xml
<body>
<input type="text" id="inpEl">
<button id="sendBtn">发送</button>
<script>
// 建立到服务端 WebSoket 连接
const ws = new WebSocket("ws://localhost:8080");
sendBtn.onclick = () => {
ws.send(inpEl.value);
}
// 断开 websoket 连接
window.onbeforeunload = () => {
ws.close();
}
</script>
</body>
- 页面B
ini
<body>
<script>
//建立到服务端 WebSocket 连接
const ws = new WebSocket("ws://localhost:8080");
let count = 1;
// 当有消息发过来时,就将消息放到显示元素上
ws.onmessage = (e) => {
const div = document.createElement("div");
div.innerHTML = `第${count++}次接收到的消息:${e.data}`;
document.body.appendChild(div);
};
// 断开 websoket 连接
window.onbeforeunload = () => {
ws.close();
};
</script>
</body>
额外的方式:1. 利用SharedWorker可以在同源页面之间共享的特性实现;2. 定时器轮询cookie的变化;3. 定时器轮询IndexedDB的变化;......
PS:本文内容仅供交流学习,转载请注明出处。