浏览器跨窗口通信

预计阅读时间: 5 分钟

前言

  前期主要开发 PC 端网站管理系统,现在在基于前者的基础上需要开发数据统计相关的数据大屏展示。结合现有项目和后续的扩展,综合考虑后决定新开项目主做数据相关的数据大屏展示,以便于后续更新和维护。第一期工作主要是数据展示,基于 PC 端网站携带参数跳转过来。难点是数据大屏的适配,这个网上有很多案例,可以结合自身实际情况,选择适合自己的方案,此处推荐一个数据大屏适配插件「autofit.js」做为参考。

  因为是基于 PC 端网站跳转过来的网站链接进行打开预览的,当 PC 端 登录超时 或者 Token 失效 时通过链接跳转的页面无法及时收到通知,还需等待调用 API 时才能知晓。查阅文档后,发现有几种方式是符合我现在项目的使用场景的,现将功能实现做个笔记,便于后续查阅。

方案一

  借助 localStorage 实现同源窗口通信;当存储区域(localStoragesessionStorage)被修改时,会触发storage”事件,通过监听“storage”事件来判断状态的改动,依据获取到的存储区域数据状态改变的数值来对当前页面窗口进行相应的逻辑处理。

代码案例:localStorage
1// 设置值:value 仅支持 string 类型
2localStorage.setItem("key", "value");
3
4// 获取值:返回对应的值,获取不到返回 null
5localStorage.getItem("key");
6
7// 移除值
8localStorage.removeItem("key");
9
10// 清除值
11localStorage.clear();
12
13// 设置监听
14window.addEventListener("storage", function (evt) {
15	console.log("evt", evt);
16});
17
18// 移除监听
19window.removeEventListener("storage", function () {
20	console.log("evt", evt);
21});
代码案例:sessionStorage
1// 设置值:value 仅支持 string 类型
2sessionStorage.setItem("key", "value");
3
4// 获取值:返回对应的值,获取不到返回 null
5sessionStorage.getItem("key");
6
7// 移除值
8sessionStorage.removeItem("key");
9
10// 清除值
11sessionStorage.clear();
12
13// 设置监听
14window.addEventListener("storage", function (evt) {
15	console.log("evt", evt);
16});
17
18// 移除监听
19window.removeEventListener("storage", function () {
20	console.log("evt", evt);
21});
注意事项

  同源的不同文件路径下可以监听到 “storage” 的变化,当前文件存储值发生更新时,当前页面路径下无法监听到当前文件的变化。

方案二

  借助 BroadcastChannel 实现同源窗口页面间的通信;BroadcastChannel 接口代理了一个命名频道,可以让指定 origin 下的任意 browsing context 来订阅;其允许同源的不同浏览器窗口Tab 页面frameiframe 下的不同文档之间互相通信。通过触发监听一个 “message” 事件来接收 BroadcastChannel 发送的消息。通过 BroadcastChannel 实例的 “postMessage” 来发送信息,从而实现多窗口间的消息通知。

代码案例:BroadcastChannel
1// 判断当前浏览器是否支持
2if ("BroadcastChannel" in window) {
3	// 当前浏览器支持 BroadcastChannel
4	console.log(true);
5} else {
6	// 当前浏览器不支持 BroadcastChannel
7	console.log(false);
8	return;
9}
10
11// 创建实例:相同 origin 才可以通信
12const channel = new BroadcastChannel("MSG");
13
14/* 监听方式一 */
15channel.onmessage = function (event) {
16	// 获取收到的信息
17	let msg = event.data;
18	console.log("msg", msg);
19	if (msg === "xxx") {
20		// 吧啦吧啦
21	}
22};
23
24channel.onmessageerror = function (error) {
25	// 监听失败处理
26	console.log("error", error);
27	// 吧啦吧啦
28};
29
30/* 监听方式二 */
31channel.addEventListener("message", function (evt) {
32	// 获取收到的信息
33	let msg = evt.data;
34	console.log("msg", msg);
35	if (msg === "xxx") {
36		// 吧啦吧啦
37	}
38});
39
40channel.removeEventListener("message", function (evt) {
41	// 获取收到的信息
42	let msg = evt.data;
43	console.log("msg", msg);
44	if (msg === "xxx") {
45		// 吧啦吧啦
46	}
47});

注:当前发送页面的窗口,无法监听到发送的信息;除发信息窗口的外的同源窗口可以监听到发送的信息。

Class 实现方式

代码案例:class 版本
1export default class Channel {
2	constructor(typeName) {
3		this.typeName = typeName || "MSG";
4		this.bcl = null;
5	}
6	create(cb) {
7		if ("BroadcastChannel" in window) {
8			this.bcl = new BroadcastChannel(this.typeName);
9			this.bcl.addEventListener("message", cb);
10		}
11	}
12	close(cb) {
13		if (this.bcl) {
14			this.bcl.removeEventListener("message", cb);
15			this.bcl.close();
16			this.bcl = null;
17		}
18	}
19	sender(msg) {
20		if (this.bcl) {
21			this.bcl.postMessage(msg || "");
22		}
23	}
24}
代码实例:使用 class
1import Channel from "./channel.js";
2
3let bcl = null;
4
5// 监听事件
6const changeChannel = ({ data }) => {
7	if (data === "CLOSE") {
8		// todo
9		window.close();
10		let timer = setTimeout(() => {
11			if (!window.closed) location.reload();
12			clearTimeout(timer);
13		}, 200);
14	}
15};
16
17// 初始化实例
18const initChannel = () => {
19	if (bcl) bcl.close(changeChannel);
20	bcl = new Channel("LOGIN");
21	bcl.create(changeChannel);
22};
23
24// 页面卸载
25window.onunload = function () {
26	if (bcl) bcl.close(changeChannel);
27};

其他

  本次主要的功能是夸窗口管理当前页的登录状态;主要是前期一个 窗口退出登录 或者 token 失效 时,会 自动刷新 页面 退出登录 跳转 登录页,但是其他页面需要点一下,在 API 请求报错时才能跳转登录页面,感觉这个交互有点繁琐,就在思考能否当前页登出后,其他同源页面也同时退出登录后,跳转登录页或者关闭当前窗口,只留下当前触发激活的窗口的页面。

  最终尝试在收到信息的窗口通过触发 window.close() 的方式来关闭当前窗口。于此同时还有一个问题就是,此种方式只能关闭通过 js 打开的窗口,但是非 js 打开的窗口调用 window.close() 方式是不生效的,为了处理这种方式,最终采用如下方式来进行页面的刷新。

代码实例:关闭/刷新窗口
1channel.onmessage = function (event) {
2	if (event.data === "close") {
3		window.close();
4
5		let timer = setTimeout(() => {
6			if (!window.closed) {
7				window.location.reload();
8				clearTimeout(timer);
9			}
10		}, 100);
11	}
12};
注意事项:

  通过判当前窗口是否已关闭的状态来启动延时定时器实现窗口的刷新。

总结

  实现窗口之间通信的方式有好多,此处简单罗列的两种方式,其中 “方式二” 是我用在项目中的,已基本满足当前项目的需求,若有更好的方式后续会不断完善和优化。