> For AI agents: the complete documentation index is available at /llms.txt, the full documentation bundle is available at /llms-full.txt.

# 跨窗口与跨域通信

### 前言

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

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

### 方案一

借助 **localStorage** 实现同源窗口通信；当存储区域（**localStorage** 或 **sessionStorage**）被修改时，会触发[“**storage**”事件](https://developer.mozilla.org/zh-CN/docs/Web/API/Window/storage_event)，通过监听“**storage**”事件来判断状态的改动，依据获取到的存储区域数据状态改变的数值来对当前页面窗口进行相应的逻辑处理。

```javascript title="代码案例：localStorage"
// 设置值：value 仅支持 string 类型
localStorage.setItem("key", "value");

// 获取值：返回对应的值，获取不到返回 null
localStorage.getItem("key");

// 移除值
localStorage.removeItem("key");

// 清除值
localStorage.clear();

// 设置监听
window.addEventListener("storage", function (evt) {
    console.log("evt", evt);
});

// 移除监听
window.removeEventListener("storage", function () {
    console.log("evt", evt);
});
```

```js title="代码案例：sessionStorage"
// 设置值：value 仅支持 string 类型
sessionStorage.setItem("key", "value");

// 获取值：返回对应的值，获取不到返回 null
sessionStorage.getItem("key");

// 移除值
sessionStorage.removeItem("key");

// 清除值
sessionStorage.clear();

// 设置监听
window.addEventListener("storage", function (evt) {
    console.log("evt", evt);
});

// 移除监听
window.removeEventListener("storage", function () {
    console.log("evt", evt);
});
```

:::info 注意事项
**同源**的不同文件路径下可以监听到 **“storage”** 的变化，当前文件存储值发生更新时，**当前** 页面路径下无法监听到当前文件的变化。
:::

### 方案二

借助 [**BroadcastChannel**](https://developer.mozilla.org/zh-CN/docs/Web/API/BroadcastChannel) 实现同源窗口页面间的通信；**BroadcastChannel** 接口代理了一个命名频道，可以让指定 **origin** 下的任意 **browsing context** 来订阅；其允许**同源的不同浏览器窗口**、**Tab 页面**、**frame** 或 **iframe** 下的不同文档之间互相通信。通过触发监听一个 **“message”** 事件来接收 **BroadcastChannel** 发送的消息。通过 **BroadcastChannel** 实例的 “**postMessage**” 来发送信息，从而实现多窗口间的消息通知。

```jsx title="代码案例：BroadcastChannel"
// 判断当前浏览器是否支持
if ("BroadcastChannel" in window) {
    // 当前浏览器支持 BroadcastChannel
    console.log(true);
} else {
    // 当前浏览器不支持 BroadcastChannel
    console.log(false);
    return;
}

// 创建实例：相同 origin 才可以通信
const channel = new BroadcastChannel("MSG");

/* 监听方式一 */
channel.onmessage = function (event) {
    // 获取收到的信息
    let msg = event.data;
    console.log("msg", msg);
    if (msg === "xxx") {
        // 吧啦吧啦
    }
};

channel.onmessageerror = function (error) {
    // 监听失败处理
    console.log("error", error);
    // 吧啦吧啦
};

/* 监听方式二 */
channel.addEventListener("message", function (evt) {
    // 获取收到的信息
    let msg = evt.data;
    console.log("msg", msg);
    if (msg === "xxx") {
        // 吧啦吧啦
    }
});

channel.removeEventListener("message", function (evt) {
    // 获取收到的信息
    let msg = evt.data;
    console.log("msg", msg);
    if (msg === "xxx") {
        // 吧啦吧啦
    }
});
```

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

#### Class 实现方式

```jsx title="代码案例：class 版本"
export default class Channel {
    constructor(typeName) {
        this.typeName = typeName || "MSG";
        this.bcl = null;
    }
    create(cb) {
        if ("BroadcastChannel" in window) {
            this.bcl = new BroadcastChannel(this.typeName);
            this.bcl.addEventListener("message", cb);
        }
    }
    close(cb) {
        if (this.bcl) {
            this.bcl.removeEventListener("message", cb);
            this.bcl.close();
            this.bcl = null;
        }
    }
    sender(msg) {
        if (this.bcl) {
            this.bcl.postMessage(msg || "");
        }
    }
}
```

```jsx title="代码实例：使用 class"
import Channel from "./channel.js";

let bcl = null;

// 监听事件
const changeChannel = ({ data }) => {
    if (data === "CLOSE") {
        // todo
        window.close();
        let timer = setTimeout(() => {
            if (!window.closed) location.reload();
            clearTimeout(timer);
        }, 200);
    }
};

// 初始化实例
const initChannel = () => {
    if (bcl) bcl.close(changeChannel);
    bcl = new Channel("LOGIN");
    bcl.create(changeChannel);
};

// 页面卸载
window.onunload = function () {
    if (bcl) bcl.close(changeChannel);
};
```

### 其他

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

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

```js title="代码实例：关闭/刷新窗口"
channel.onmessage = function (event) {
    if (event.data === "close") {
        window.close();

        let timer = setTimeout(() => {
            if (!window.closed) {
                window.location.reload();
                clearTimeout(timer);
            }
        }, 100);
    }
};
```

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

### 总结

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

***

## 跨域窗口间 postMessage 通信

> 非同源项目借助 iframe / window\.open + postMessage 的通信方案。

#### 使用场景

在非同源 \[跨域] 项目间进行项目间的通信，一般是基于 `iframe` 或者 `object` 标签进行项目网址的嵌套，借助 `postMessage` 来进行项目间的数据通信工作。但是，在跨域的项目间进行数据通信时，需要进项相应的逻辑判断处理，来实现通讯的，一般有数据更新、token 等用户信息的时效性校验等方面，需要借助 `postMessage` 来实现数据通信。

#### 使用方法

##### 发送数据

```javascript title="代码示例"
// 数据：父窗口传值给子窗口
const toChildData = { key: "fromParent", value: "fromParent" };

// 子窗口：要打开窗口的实例
const childTargetWindow = window.open("https://child.com", "_blank");

// 检测子窗口是否加载
if (childTargetWindow && !childTargetWindow.closed) {
    childTargetWindow.postMessage(toChildData, "https://child.com");
}
```

##### 接收数据

```javascript title="代码示例"
window.addEventListener("message", (event) => {
    // 检测消息来源
    if (event.origin !== "https://child.com") {
        return;
    }
    // 接收消息
    const msgData = event.data;
    console.log("接收消息：", msgData);
});
```

####

tip 提示
具体使用方法请查看[MDN 官方文档](https://developer.mozilla.org/zh-CN/) ，或[postMessage 相关文档](https://developer.mozilla.org/zh-CN/docs/Web/API/Window/postMessage)；
:::

:::danger 注意事项

- 跨浏览器 tab 窗口通信时，使用 `window.postMessage` 时，需要检测消息来源，防止 `open` 的实例因刷新失效。在打开的子窗口中可以通过 `domain.refer` 获取 `open` 窗口的父级窗口源地址。
- 父级窗口在打开子窗口时，保存的浏览器对象实例，父级项目在刷新后将失效，需要通过 `window.open` 方法重新打开子窗口来创建对应 Window 实例。
- 跨域项目间数据通信时，健建议使用 `iframe` 标签内嵌网址的形式来金进行页面和组件直接的连接。从而更有效的进行数据交互。

:::
