> For the complete documentation index, see [llms.txt](https://lizh.gitbook.io/knowledge/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://lizh.gitbook.io/knowledge/frontend/02-jin-jie-07web-kua-ye-mian-tong-xin.md).

# 进阶 07 Web跨页面通信

Web 跨页面通信是指在浏览器中，不同页面间的信息传递。再详细些划分，可分两类：在同一窗口，一个页面跳转到另一个页面时的通信，如：a 标签或者 `location.href` 跳转等；在不同窗口打开的多个页面间的通信，如：调用 `window.open()` 打开多个页面、在浏览器手动输入地址打开多个页面等。

另外，由于浏览器同源策略的存在，通信又可以分为：同源页面通信，非同源页面通信。

本文总结了页面间通信的技术，包括非同源页面间的通信。文章最后还分析了如何在不同场景选择不同的技术方案。

## BroadCast Channel

BroadcastChannel 表示一个命名频道，给定来源的任何浏览上下文都可以订阅该频道。它允许**相同来源**的不同文档（在不同的窗口、选项卡、框架或 iframe 中）之间进行通信。消息通过监听频道的对象上触发的事件进行广播。

首先，调用 BroadcastChannel 构造函数创建一个广播频道，该构造函数接受一个参数：频道名称。如果它是第一个连接到该广播频道名称的，则创建基础频道，并把它连接到底层的频道。

```javascript
const BC = new BroadcastChannel('zhao')
```

**注意：** 所有需要使用该频道的页面都必须用同一名称创建 BroadcastChannel 实例。如果该名称的频道已存在，则不会创建新频道，直接连接已有的底层频道。

然后，各个页面可以通过 `onmessage` 来监听被广播的消息：

```javascript
BC.onmessage = function (e) {
    console.log(`[频道@${e.target.name}]接收：${e.data}`)
}
// 或者
BC.addEventListener('message', (e) => {
    console.log(`[频道@${e.target.name}]接收：${e.data}`)
})
```

最后，在页面上调用 BroadcastChannel 实例的 `postMessage` 方法，可将任何类型的消息发送到所有监听同一频道的页面：

```javascript
BC.postMessage('Hello, This is Page A!')
BC.postMessage({
    message: 'Hello, This is Page A!',
    form: 'pageA'
})
```

调用 BroadcastChannel 实例的 `close` 方法关闭频道，断开对象与底层频道的连接，并允许它最终被垃圾回收：

```javascript
BC.close()
```

**注意：** 当前页面关闭频道，浏览器只是回收了当前页面的 BroadcastChannel 实例对象，底层频道还是存在的，其他页面仍可以广播或接收频道信息。

关闭频道后，调用 `postMessage` 方法会报错：

```shell
VM358:1 Uncaught DOMException: Failed to execute 'postMessage' on 'BroadcastChannel': Channel is closed
at <anonymous>:1:4
```

[BroadcastChannel DEMO Page A](https://1927344728.github.io/demo-lizh/html/23-commumication\(A\).html?type=1)

[BroadcastChannel DEMO Page B](https://1927344728.github.io/demo-lizh/html/23-commumication\(B\).html?type=1)

[BroadcastChannel DEMO Page C](https://1927344728.github.io/demo-lizh/html/23-commumication\(B\).html?type=1)

[**注意 BroadCastChannel 的兼容性**](https://caniuse.com/?search=BroadCast%20Channel)： 目前（2021.12），Edge、Firefox、Chrome、Opera 以及大多安卓移动端浏览器都已经支持，IE 6\~11、Safari\@15.2（包括IOS版本）及以下的不支持。

## [Service Worker](broken://spaces/-M8fDLTBWl2H-MOzligj/pages/79FHgvnNIVG9OrQmncEr#service-worker)

Service workers 本质上充当 Web 应用程序、浏览器与网络（可用时）之间的代理服务器。这个 API 旨在创建有效的离线体验，它会拦截网络请求并根据网络是否可用来采取适当的动作、更新来自服务器的的资源。它还提供入口以推送通知和访问后台同步 API。

将 Service Worker 作为消息的处理中心（中央站），它可以向作用域下的所有打开的页面发送消息，实现多页面之间的广播通信。

首先，注册 Service Worker：

```javascript
navigator.serviceWorker.register('sw.js').then(() => {
    console.log('ServiceWorker 注册成功')
})
```

其次，在 `sw.js` 添加 Service Worker 监听 `message`事件，接收所有 Service Worker 进程作用域下（即 `sw.js` 所在路径下）的 client（即当前域打开的页面）发送的信息。然后通过 `self.clients.matchAll()` 获取该作用域下的所有 client，再通过调用每个 client 的 `postMessage` 方法，向对应页面发送消息。这样，Service Worker 统一接收所有 client 的信息，再由各个 client 向各自页面发送信息，实现广播通信。

```javascript
self.addEventListener('message', (event) => {
    console.log(`[Client]接收：${event.data}`)
    event.waitUntil(
        self.clients.matchAll().then((clients) => {
            if (!clients || !clients.length) {
                return
            }
            clients.forEach((client) => {
                client.postMessage(event.data)
            })
        })
    )
})
```

然后，在页面监听 `navigator.serviceWorker` 对象的 `message` 事件，接收 ServiceWorker 发送的消息：

```javascript
navigator.serviceWorker.addEventListener('message', (e) => {
    console.log(`[ServiceWorker]接收：${e.data}`)
})
```

最后，在页面调用 `navigator.serviceWorker` 对象的 `postMessage` 方法广播消息：

```javascript
navigator.serviceWorker.controller.postMessage('Hello, This is Page A!')
```

[ServiceWorker DEMO Page A](https://1927344728.github.io/demo-lizh/html/23-commumication\(A\).html?type=2)

[ServiceWorker DEMO Page B](https://1927344728.github.io/demo-lizh/html/23-commumication\(B\).html?type=2)

[ServiceWorker DEMO Page C](https://1927344728.github.io/demo-lizh/html/23-commumication\(B\).html?type=2)

[**注意 ServiceWorker 的兼容性**](https://caniuse.com/?search=ServiceWorker)： 比 BroadcastChannel 好，目前（2021.12），除 IE 6\~11，其他浏览器的新版本基本都支持。

## LocalStorage

LocalStorage 是一种将数据缓存在浏览器中的技术。LocalStorage 与 SessionStorage 类似，但其区别在于：LocalStorage 缓存数据可以长期保留，直到手动清 除；而 SessionStorage 缓存数据会在当前会话结束——也就是说，当页面被关闭时清除 。

当 LocalStorage 变化时，会触发 `storage` 事件。利用这个特性，我们可以在发送消息时，把消息写入到某个 LocalStorage 中；然后在各个页面内，通过监听 `storage` 事件即可收到通知。

首先，在各页面监听 `storage` 事件：

```javascript
window.addEventListener('storage', (e) => {
    console.log(`[LocalStorage]接收：key-${e.key}，新数据-${e.newValue}，老数据-${e.oldValue}`)
})
```

然后，当某个页面需要广播消息时，调用 `setItem` 方法：

```javascript
window.localStorage.setItem('msg', 'Hello, This is Page A!')
```

需要注意几点：

* 对 Storage 对象进行任何修改，都会在文档上触发 `storage` 事件。如：`setItem()`、`clear()`、`removeItem()` 等方法。
* `setItem()` 方法只能存储 String 类型，对象类型需要 `JSON.stringify()` 方法字符串化。
* 只有 Storage 对象有修改才会触发 `storage` 事件。如：调用 `setItem()` 方法设置的新数据与老数据相同时，`storage` 事件不会触发：

  ```javascript
  // 连续两次调用 setItem 方法，设置同一值，只会触发一次 storage 事件。
  window.localStorage.clear("msg")
  window.localStorage.setItem('msg', 'Hello, This is Page A!')
  window.localStorage.setItem('msg', 'Hello, This is Page A!')

  // 通常我们可以在数据中加一个时间戳。
  window.localStorage.setItem('msg', `Hello, This is Page A!-${new Date().getTime()}`)
  ```
* Storage 对象的修改不会触发当前页面的 `storage` 事件。

[LocalStorage DEMO Page A](https://1927344728.github.io/demo-lizh/html/23-commumication\(A\).html?type=3)

[LocalStorage DEMO Page B](https://1927344728.github.io/demo-lizh/html/23-commumication\(B\).html?type=3)

[LocalStorage DEMO Page C](https://1927344728.github.io/demo-lizh/html/23-commumication\(B\).html?type=3)

## Shared Worker

Shared Worker 是 Worker 家族的另一个成员。普通的 Worker 之间是独立运行、数据互不相通；而多个页面注册的 Shared Worker 则可以实现**同源页面**的数据共享。它可以从多个浏览上下文访问，例如多个窗口、iframe 甚至 Worker。

首先，在页面中创建一个 SharedWorker 实例：

```javascript
// 第二个参数是 Shared Worker 名称，也可以留空
const sharedWorker = new SharedWorker('shared.worker.js', 'zhao')
```

**注意：** 多个页面调用构造函数传入同一 `shared.worker.js` 文件，只有第一个页面会创建 SharedWorker，其他页面会复用已创建的 SharedWorker，这个 SharedWorker 由那几个页面共享。

然后，在 `shared.worker.js` 文件定义消息监听的逻辑，作如下约定：定义一个变量 `shareData` 用于存储数据，如果 `message` 事件的传参是 `{get: true}`，则认为一个读数据的操作，调用 `port.postMessage(shareData)` 向页面发送消息；否则，认为是一个写数据的操作，将数据存于变量 `shareData` 。

```javascript
// shared.worker.js
var shareData = null
self.addEventListener('connect', (e) => {
    const port = e.ports[0]
    port.addEventListener('message', (event) => {
        if (event.data.get === true) {
            port.postMessage(shareData)
        } else {
            shareData = event.data
        }
    })
    port.start()
})
```

之后，页面中定义 `sharedWorker.port` 对象的 `message` 事件的监听函数，接收 Shared Worker 发送的数据：

```javascript
// 监听 Shared Worker 发送的数据
sharedWorker.port.addEventListener('message', (e) => {
    console.log('[Shared Worker]接收：', e.data)
}, false)
sharedWorker.port.start()

// 或者
sharedWorker.port.onmessage = (e) => {
    console.log('[Shared Worker]接收：', e.data)
}
```

**注意：** 使用 `sharedWorker.port.addEventListener` 来添加监听函数，需要显式调用 `sharedWorker.port.start()`；如果使用`onmessage` 绑定监听函数则不需要。

最后，在页面通过调用 `sharedWorker.port.postMessage` 读取或者写入共享数据。

```javascript
// 写数据
sharedWorker.port.postMessage({msg: 'Hello, This is Page A!'})

// 读数据，会触发 message 事件
sharedWorker.port.postMessage({get: true})
```

`sharedWorker.port` 对象上 `postMessage()` 方法的调用会触发 Shared Worker 的 `message` 事件，该事件中的 `postMessage()` 方法又会触发页面上 `sharedWorker.port` 对象的 `message` 事件，从来实现多页面之间的数据共享。

[SharedWorker DEMO Page A](https://1927344728.github.io/demo-lizh/html/23-commumication\(A\).html?type=4)

[SharedWorker DEMO Page B](https://1927344728.github.io/demo-lizh/html/23-commumication\(B\).html?type=4)

[SharedWorker DEMO Page C](https://1927344728.github.io/demo-lizh/html/23-commumication\(B\).html?type=4)

**注意：** 所有需要使用共享数据的页面，都需要基于同一个 Shared Worker 脚本文件（如，shared.worker.js）创建实例，并在页面上添加 `sharedWorker.port` 的监听函数、调用 `postMessage` 读写数据。

**注意：** `chrome://inspect/#workers` -> `Shared workers` 可以调试 `shared.worker.js`。

[**注意 Shared Worker 的兼容性**](https://caniuse.com/?search=Shared%20Worker)： 兼容性较差，目前（2021.12），IE 6\~11、safari、大部分移动端浏览器都不支持。

## IndexedDB

除了可以利用 Shared Worker 来存储共享数据，还可以使用其他一些存储方案。如：IndexedDB、LocalStorage、 Cookie 等。

其思路很简单：与 Shared Worker 方案类似，消息发送方将消息存至共享中心（IndexedDB、LocalStorage、 Cookie）；接收方（同源下所有页面）则通过类似 `get` 的方法或者在事件中获取共享中心的最新的信息。

[点击查看 IndexedDB 的简单用法。](broken://spaces/-M8fDLTBWl2H-MOzligj/pages/isclEDG7qkia2fUX3ppy#indexeddb)

## postMessage

上述所讲的五种通信方式，有一个共同的限制：\*\*只能在同源的页面间相互通信。\*\*所谓指的是具有相同的协议、相同的域名、相同的端口。

`window.postMessage()` 方法提供了一种受控机制来规避此限制，只要正确的使用，就可以安全地实现跨源通信。

从广义上讲，一个窗口可以获得对另一个窗口的 window 对象（如：`targetWindow = window.opener`），然后在窗口上调用 `targetWindow.postMessage()` 方法发送消息，该消息可以被目标窗口的全局事件 `message` 接收。

```javascript
targetWindow.postMessage(message, targetOrigin, [transfer])
```

* **targetWindow：** 其他窗口的一个引用。通过以下方法获取：
  * 调用 `window.open()` 返回的窗口对象；
  * 页面上的 iframe 对象：比如 iframe 的 `contentWindow` 属性、、或者是命名过或数值索引的[window.frames](https://developer.mozilla.org/en-US/docs/DOM/window.frames)。
* \*\*message：\*\*将要发送到其他 window 的数据。
* **targetOrigin：** 指定哪些窗口能接收该消息，其值可以是字符串 `*` ，表示无限制，也可以是一个 URI。如果目标窗口的协议、域名、端口这三者的任意一项不匹配 `targetOrigin` 提供的值，那么消息就不会被发送。
* \*\*transfer \*\* 可选，是一串和 `message` 同时传递的 `Transferable` 对象。

以 `window.open` 为例：

```javascript
// 父页面：23-commumication(A).html
var targetWindow = window.open('23-commumication(B).html?type=5')
window.addEventListener('message', (e) => {
    console.log(`[postMessage]接收：${e.data}`)
})
```

```javascript
// 子页面：23-commumication(B).html
window.addEventListener('message', (e) => {
    console.log(`[postMessage]接收：${e.data}`)
})
```

父页面向子页面发送消息：

```javascript
targetWindow.postMessage('Hello, This is Page A!')
```

子页面向父页面发送消息：

```javascript
window.opener.postMessage('Hello, This is Page B!')
```

**注意：** 如果不是当前页面调用 `window.open()` 打开或者 `iframe` 内嵌的页面，则跟当前页面没有任何关系，即取不到页面窗口的 window 对象，也就无法使用 `postMessage` 方法发送信息。

**注意：** 如果 `window.open()` 打开或者 `iframe` 内嵌的页面是同源页面，则 `postMessage` 方法中的第二个参数是不传；如果是非同源页面，则第二个参数必传（可以传 `*` 或 URI），否则会报错，如：

```shell
Failed to execute 'postMessage' on 'DOMWindow': The target origin provided ('http://localhost:5200') does not match the recipient window's origin ('http://localhost:5300').
```

### postMessage+window\.open

**实现思路：** 在页面 A （父窗口）调用 `window.open()` 打开多个子页面 B、C...（即多个子窗口），并将子页面的 window 对象存在一个数组中。如果页面 A 需要广播消息，只需遍历数组，调用数组中 window 对象的 `postMessage` 方法发送消息即可；如果子页面 B 需要广播消息，则调用 `window.opener.postMessage` 方法向页面 A 发送消息，页面 A 再在 `message` 事件中遍历数组，调用数组中 window 对象的 `postMessage` 方法发送消息。

首先，在页面 A 把 `window.open()` 方法打开的页面的 window 对象储存到数组，然后定义 `message` 事件的监听函数：

```javascript
var targetWindows = []
function fnOpenWindow(url) {
    let tWindow = window.open(url)
    targetWindows.push(tWindow)
}

window.addEventListener('message', (e) => {
    if (e.data.from === location.href) {
        return
    }
    targetWindows = targetWindows.filter(w => !w.closed)
    targetWindows.forEach(w => w.postMessage(e.data, '*'))
    console.log('[window.open]接收信息：', `msg-${e.data.msg}，from-${e.data.from}`)
    document.querySelector('.box_05').querySelector('.content').innerHTML = e.data.msg
})
```

然后，在各子页面定义 `message` 事件的监听函数：

```javascript
window.addEventListener('message', (e) => {
    if (e.data.from === location.href) {
        return
    }
    console.log('[window.open]接收信息：', `msg-${e.data.msg}，from-${e.data.from}`)
})
```

最后，在子页面（如 B）中调用 `postMessage` 方法，实现广播消息：

```javascript
window.opener.postMessage({
    msg: "Hello, This is Page B!",
    from: location.href
}, '*')
```

[window.open DEMO Page A](https://1927344728.github.io/demo-lizh/html/23-commumication\(A\).html?type=5)

**注意：** 子页面的 window 对象上的 `opener` 属性，指向的是父页面的 window 对象，因此，子页面获得了父页面的控制权。出于安全考虑，可以限制子页面的 `window.opener` 属性。如果是 `a` 标签跳转，可以加 `rel=noopener` 属性或者 `rel=noreferrer`，如果是 Js 调用 `window.open()` 方法，可以：

```javascript
let childWindow = window.open(url)
childWindow.opener = null;
```

**注意：** 该方案使用有限制，被打开页面必需要有 `window.opener` 属性，并且指向打开该页面的页面 window 对象。即，如果子页面不是通过在另一个页面内的 `window.open` 打开的（如直接在地址栏输入链接或者从其他网站链接过来），则两者之间没有联系，无法通信。

### postMessage+iframe

该方案与 `postMessage+window.open` 类似，区别在于：适用于在当前页面内嵌 iframe 子页面的场景。父页面调用类似 `window.frames[0].postMessage()` 方法向子页面发送信息，子页面在 `message` 事件中调用 `window.parent.postMessage()` 或者 `e.source.postMessage()` 向父页面通信。

**实现思路：** 非同源页面 A 和 B ，A 内嵌一个 iframe 页面 C1，B 内嵌一个 iframe 页面 C2，C1、C2 指向同一 URL（或同源下的不同 URL 也可以）。当页面 A 需要向 页面 B 通信时，先将消息发给其内嵌的 iframe C1，由于 C1、C2 同源，它们之间可以使用上述讲到的任意方法通信，即 C2 可以接收 C1 发送的消息，然后再由 C2 将发送信息给页面 B。

我们以 BroadCast Channel 为例。

首先，在中间页面创建一个 BroadCast Channel 实例，并为实例的 `message` 事件上定义函数，接收来自 BroadCast Channel 的消息，再定义一个全局的 `message` 事件定义函数，接收来自父页面的消息。

```javascript
const BC = new BroadcastChannel('zhao')
// 接收到 BroadCast Channel 广播消息，发送给父页面
BC.onmessage = function (e) {
    window.parent.postMessage(e.data, '*')
    console.log('[BroadCast Channel]接收：', `msg-${e.data.msg}，from-${e.data.from}`)
}

// 接收来自父页面的消息，调用 BroadCast Channel 广播消息
window.addEventListener('message', function (e) {
    BC.postMessage(e.data)
    console.log('[父页面]接收：', `msg-${e.data.msg}，from-${e.data.from}`)
})
```

然后，在页面 A、B 中定义全局的 `message` 事件函数，接收来自 iframe 的消息：

```javascript
window.addEventListener('message', (e) => {
    console.log('[iframe]接收：', `msg-${e.data.msg}，from-${e.data.from}`)
})
```

最后，在页面 A 或者 B 发出消息：

```javascript
window.frames[0].postMessage({
    msg: "Hello, This is Page A!",
    from: location.href
}, '*')
```

[iframe DEMO Page A](https://1927344728.github.io/demo-lizh/html/23-commumication\(A\).html?type=6)

[iframe DEMO Page B](https://1927344728.github.io/demo-lizh/html/23-commumication\(B\).html?type=6)

**注意：** 主页面调用 iframe 的 `postMessage` 方法，需要等 iframe 加载完。否则报错：

```shell
Failed to execute 'postMessage' on 'DOMWindow': The target origin provided ('http://xxx.xxx.x.xxx:xxxx') does not match the recipient window's origin
```

## window\.name+iframe

**浏览器的 window\.name 属性有个特性：无论是否同源，只要在同一个浏览器标签或者同一个 iframe 框架打开过的页面，后一个页面可以读取前一个页面设置的 window\.name 值（页面刷新后，该值还是存在）**。

**注意：** window\.name 的值大小可达 2MB。

根据这个特性，我们可以在主页面内嵌 iframe，先将 iframe 指向一个非同源的页面，然后将 iframe 指向一个与主页面同源的中间页面，再主页面访问同源是中间页面的 window\.name 值，间接实现非同源页面的通信。

比如：`http://localhost:5200/23-commumication(A).html` 要获取非同源页面 `http://localhost:5300/23-commumication(B).html` 的数据：

首先，在 `23-commumication(B).html` 页面，将数据赋值给 `window.name`：

```html
<!-- http://localhost:5300/23-commumication(B).html?type=7 -->
<script>
    window.name = "Hello, This is Page B！"
</script>
```

其次，在 `23-commumication(A).html` 内嵌一个指向 `23-commumication(B).html` 的 `iframe` 标签，在 `23-commumication(B).html`文件加载完成后，会执行文件中的 `window.name` 赋值语句。

```html
<!-- http://localhost:5200/23-commumication(A).html?type=7 -->
<iframe src="http://localhost:5300/23-commumication(B).html?type=7"></iframe>
<script>
    function getCrossOriginData() {
        const frame = document.querySelector("#windowOpenIframe")
        frame.onload = function () {
            // console.log(frame.contentWindow.name)
        }
    }
    getCrossOriginData()
</script>
```

**注意：** 由于 `23-commumication(A).html` 和 `23-commumication(B).html` 是非同源页面，无法通过 `frame.contentWindow.name` 直接获取数据。如果执行上面注释的 `console.log(frame.contentWindow.name)`，会报错：

```shell
Uncaught DOMException: Blocked a frame with origin "http://localhost:5200" from accessing a cross-origin frame.
```

然后，创建一个中间页面 `http://localhost:5200/proxy.html`（**也可以是不存在的页面，会报 404 错误，但不影响功能**），然后将 `23-commumication(A).html` 中 `iframe` 标签的 `src` 指向这个中间页面。

由于 `proxy.html` 和 `23-commumication(B).html` 是在同一个 iframe 中打开的，它们共享 `window.name`。而 `proxy.html` 和 `23-commumication(A).html` 是同源的，`23-commumication(A).html` 可以通过 `frame.contentWindow.name` 获取 `proxy.html` 的 `window.name` 值，也就是 `23-commumication(B).html` 的 `window.name` 值，如此，间接实现了的 `23-commumication(A).html` 和 `23-commumication(B).html` 的非同源通信。

修改 `getCrossOriginData` 方法如下：

```javascript
function getCrossOriginData() {
    const frame = document.querySelector("#windowOpenIframe")
    let isFirstLoad = true
    frame.onload = function () {
        // iframe 加载完 23-commumication(B).html 后，再去加载 proxy.html，两者共享 window.name 值。
        if (isFirstLoad === true) {
            isFirstLoad = false
            frame.src = 'http://localhost:5200/proxy.html'
        } else {
            // 获取 proxy.html、23-commumication(B).html 共享的 window.name 值
            console.log(frame.contentWindow.name)
            document.querySelector('.box_07').querySelector('.content').innerHTML = frame.contentWindow.name
        }
    }
}
getCrossOriginData()
```

**总结：** 该方案实现的关键在于 `proxy.html`文件，它和 `23-commumication(B).html` 是在同一个 iframe 访问的，并且它和 `23-commumication(A).html` 是同源的。

同理，该方法也可以实现**服务端跨源数据请求**：服务端需要提供一个页面地址，并将需要返回的数据赋值给 `window.name` 属性。客户端调用如下方法获取数据：

```js
function getCrossOriginData(targetUrl, proxyUrl, callback) {
    const iframe = document.createElement('iframe')
    iframe.style.display = 'none'
    iframe.src = targetUrl

    let isFirstLoad = true
    iframe.onload = function () {
        if (isFirstLoad) {
            isFirstLoad = false
            iframe.contentWindow.location = proxyUrl
        } else {
            callback(iframe)
            iframe.contentWindow.document.write('')
            iframe.contentWindow.close()
            document.body.removeChild(iframe)
        }
    }
    document.body.appendChild(iframe)
}
getCrossOriginData(
    'http://localhost:5300/23-commumication(B).html?type=7',
    'http://localhost:5200/proxy.html.html',
    (iframe) => {
        console.log(iframe.contentWindow.name)
        document.querySelector('.box_07').querySelector('.content').innerHTML = iframe.contentWindow.name
    }
)
```

[查看 window.name DEMO](https://1927344728.github.io/demo-lizh/html/23-commumication\(A\).html?type=7)

## location.hash+iframe

hash 指的是 URL 的 `#` 号后面的部分。URL 的 hash 变化，浏览器不会刷新页面。

在父窗口中，通过 iframe 内嵌子窗口。父窗口将数据写入子窗口 URL 的 hash 中，子窗口可通过监听 `hashchange`事件获取：

```js
// 父窗口
function sendLocationHashToFrame () {
    const frame = document.querySelector('#locationHashIframe')
    frame.src = frame.src + `#Hello, This is Page A`
}
window.onhashchange = () => {
    console.log(window.location.hash)
}
```

同理，子窗口也可以向父窗口的 hash 写入数据，被父窗口的 `hashchange` 事件获取：

```js
// 子窗口
function sendLocationHashToParent (e) {
    window.parent.location.href = 'http://localhost:5200/23-commumication(A).html?type=8#Hello, This is Page B!'
}
window.onhashchange = () => {
    console.log(window.location.hash)
}
```

[查看 location.hash DEMO](https://1927344728.github.io/demo-lizh/html/23-commumication\(A\).html?type=8)

**注意：** 只有当 hash 值有变化才能触发 `message` 事件，即如果设置的 hash 值与当前链接相同，不会触发 `message` 事件。

**注意：** `window.parent.location.href` 是唯一的可以跨源访问的 Location 对象属性，且该属性**只能跨源赋值，不能跨源取值**。

## 其他方法

### URL?param=xxx

即，在页面跳转或打开一个新链接时，在 URL 地址后面加参数。

比如：跳转到 `http://localhost:5200/23-commumication(A).html`。

```javascript
location.href = 'http://localhost:5200/23-commumication(A).html?param=value'
```

这种方式的局限在于，只能从当前页面向被打开的页面传消息，被打开的页面无法反向传消息。

### WebSocket

WebSockets 是一种先进的技术，它可以在用户的浏览器和服务器之间打开交互式通信会话。

**实现思路：** 将 WebSockets 当作消息中转中心，浏览器页面将要发送的消息，发到 WebSockets，再由 WebSockets 发送到各页面。实现原理与 Service workers 类似。

## 方法分析

以上方法的特点：

* **BroadCast Channel：** 广播监听模式，适用于多个窗口打开的同源页面之间的通信。兼容性较差，Safari\@15.2（包括IOS版本）及以下的不支持。
* **Service workers：** 广播监听模式，与 BroadCast Channel 类似，但兼容性比较好，除 IE，其他浏览器的新版本基本都支持。
* **LocalStorage：** 广播监听模式，与 BroadCast Channel 类似，但兼容性好，主流浏览器都支持。
* **Shared Worker：** 数据共享模式，适用于同源页面之间通信，需要主动去获取数据。兼容性差，IE、safari、大部分移动端浏览器都不支持。
* **IndexedDB：** 数据共享模式，与 Shared Worker 类似。但兼容性较好，除 IE，其他主流浏览器都支持。
* **postMessage：** 广播监听模式，适用于有关联的（window\.open 打开的、frame 内嵌的）多个窗口的**同源和非同源**页面之间通信，兼容性好，主流浏览器都支持。
  * postMessage+window\.open： 适用于 `window.open` 打开的多个窗口之间通信。
  * postMessage+iframe。适用于 frame 内嵌的多个窗口之间通信。
* **window\.name+iframe：** 数据共享模式，适用于同一个窗口或 iframe 打开的**同源和非同源**页面之间通信。
* **location.hash+iframe：** 传递监听模式，适用于当前页面与 iframe 之间的**同源和非同源**页面通信。
* **其他：** URL?param=xxx，传递模式，适用于后一个页面获取前一个**同源和非同源**页面传递的信息；WebSocket，广播监听模式，适用于**同源和非同源**页面之间通信。此外，还有以服务端或者 LocalStorage 等为存储中心的数据共享模式等。

数据共享模式主动获取数据的方式有两种：

* 轮询： 使用 `setInterval`、`setTimeout` 等方法轮询。
* 事件触发： 比如页面生命周期，通过 `onvisibilitychange`、`onpageshow` 等事件回调：

```javascript
// 选项卡的内容变得可见或被隐藏时，触发 visibilitychange 事件。
document.addEventListener("visibilitychange", function() {
    if (document.visibilityState === 'visible') {
    }
})
// 页面切换后显示之后，触发 pageshow 事件
window.addEventListener("pageshow", () => {
    ...
})
```

**总结：**

* 同一窗口同源页面通信，推荐使用 LocalStorage 数据共享模式；
* 同一窗口非同源页面通信，推荐使用 window\.name 数据共享模式；
* 不同窗口同源页面通信，推荐使用 localStorage 广播监听模式；
* 不同窗口非同源页面，并且子页面是同一主面调用 window\.open 打开的，页面之间通信，推荐使用 postMessage+window\.open 广播监听模式；
* 不同窗口非同源页面，并且窗口之间没有关联的页面之间的通信，推荐使用 postMessage+iframe 广播监听模式，或者 window\.name+iframe 数据共享模式；
* iframe 父子窗口的通信，推荐使用 location.hash+iframe 传递监听模式。

Web 开发中比较常见的两种页面通信场景：

* 跳新窗口，让用户登录授权：登录完成后，将信息返回上一页面。推荐使用 postMessage+window\.open。
* 同一窗口中，A 页面跳转到 B 页面，B 页面回退后，需要更新 A 页面数据： 同源页面推荐使用 LocalStorage，非同源页面推荐使用 window\.name、或者以服务端为共享中心的数据共享模式。

## 参考资料

[MDN BroadcastChannel](https://developer.mozilla.org/en-US/docs/Web/API/BroadcastChannel)

[前端跨页面通信，你知道哪些方法？](https://segmentfault.com/a/1190000018731597)
