浏览器间的跨窗口通信之postMessage

4,099 阅读4分钟

origin

最近做项目恰巧遇到需要个小功能,简单描述下

从A页面新开歌窗口打开B页面,B页面完成一系列操作后,需要自动关闭B页面,然后A页面完成一个刷新的动作。

我一想这不就是跨窗口通信么,然后网上搜索引擎一顿查找,发现相关的文章却是不多,最秀的是很多都是复制粘贴的,而且都是围绕 iframe 写的,不算事真正意义上的两个窗口交互,于是就有了这么踩坑文章。

解决方法有很多种,本次只聊 postMessage。本文主要是介绍如何解决这个需求,没有其他干货。

postMessage

原本是是作为跨源通信的新特性出现的,只要能获取要发送窗口的window,就可以消息发出,在接收侧可以将一些不安全的消息进行过滤,防止出现安全问题。

看起来似乎也没有啥兼容性问题,就很棒。

image.png

实现

首先说下文章中用到了几个不同的叫法,但都是同一个意思

t1.html === 表示 A 页面 === 最开始打开的页面

t2.html === 表示 B 页面 === window.open 打开后的页面

无用版

看了 MDN 文档,天真的我以为只需要如下代码就可以完成。

// t1.html
<script>
  window.addEventListener('message', function(event) {
    console.log('event :>> ', event);
  })
</script>
// t2.html
<script>
  window.postMessage('123', '/')
</script>

然后发现根本没用(还是看文档不仔细),直到我看到文档的这里

image.png

所以意思是需要其他窗口的 window 对象来调用 postMessage ,这才监听 message 事件才会被触发。

但是这里回想下最初的需求,是需要 A 页面打开一个新窗口 - B页面, B页面之后再发送一个 message 给 A 页面,此时的想法是这个样子。

image.png

第二版

如果这里调用 postMessage 需要用到 otherWindow 的话,就是意味着需要 B 页面也需要获取 A 页面的 window 对象才可以发送消息。

理下逻辑

1、A 页面打开 B页面,A页面可以从window.open返回值,拿到B页面的window

2、A页面发送 message 给B页面

3、B页面接收到A页面发来的 message,并从中获取到A页面的window

4、此时A页面有B页面的window,B页面也有A页面的window,只要双方都监听 message 事件,就可以双向通信了。

于是有了如下代码

// t1.html
<h1>这是 T1</h1>
<button id="btn1">打开t2</button>
<button id="btn2">发送消息到t2</button>
<pre id="text"></pre>

<script>
  var t2
  var btn1 = document.getElementById('btn1')
  var btn2 = document.getElementById('btn2')
  var text = document.getElementById('text')

  btn1.addEventListener('click', function() {
    t2 = window.open('/t2.html')
  })

  btn2.addEventListener('click', function() {
    console.log('btn2 点击');
    // type 是为了区别和别的应用发送的消息,最后一个参数 '/' 表示只在当前域下有效
    t2.postMessage({type: 'popring', message: 't1 发送出去的消息'}, '/')
  })

  window.addEventListener('message', function(event) {
    // 过滤非当前域下的消息
    if(event.origin !== 'http://127.0.0.1:5500' || !event.data) {
      return
    }
    // 过滤其他非本应用传递过来的消息,例如 chrome 的插件就可能也会发送消息(表示 wappalyzer 就会)
    if(event.data?.type !== 'popring') {
      return
    }
    text.innerText += (JSON.stringify(event.data)+'\n')
  })
</script>
// t2.html
<h1>这是 T2</h1>
<button id="btn">发送消息到 t1</button>
<pre id="text"></pre>

<script>
  var btn = document.querySelector('#btn')
  var text = document.getElementById('text')
  var t1

  btn.addEventListener('click', function() {
    t1.postMessage({type: 'popring', message: 't2 发送出去的消息'}, '/')
  })

  window.addEventListener('message', function(event) {
    if(event.origin !== 'http://127.0.0.1:5500' || !event.data) {
      return
    }
    if(event.data?.type !== 'popring') {
      return
    }
    t1 = event.source
    text.innerText += (JSON.stringify(event.data)+'\n')
  })
</script>

此时从A页面打开B页面后,需要在A页面点击按钮,发送消息到B页面,这样B页面接收到消息才能获取到A页面的window,略有麻烦,不如把这部做成自动化。

第三版

根据以上遇到的问题,解决办法其实很简单,既然可以获取到B页面的window,那就等重写B页面加载完毕后触发事件就可。那么解决方案就来了,onload、DOMContentLoaded。

经过几次尝试,发现直接A页面使用onload,若B页面也是使用onload事件则会覆盖掉A页面的事件。需要使用 addEventListener 来监听 window 的 load 或者 DOMContentLoaded 可以生效。addEventListener 特性如下。

image.png

<h1>这是 T1</h1>
<button id="btn1">打开t2</button>
<button id="btn2">发送消息到t2</button>
<pre id="text"></pre>

<script>
  var t2
  var btn1 = document.getElementById('btn1')
  var btn2 = document.getElementById('btn2')
  var text = document.getElementById('text')

  btn1.addEventListener('click', function() {
    t2 = window.open('/t2.html')
    // 若t2页面没有重写 onload 方法,则在t1页面这么写是ok的,但若t2页面已重写 onload 方法,则以下方法不生效。
    t2.onload = () => {
      btn2.click()
    }
    // 可以改写为以下写法
    t2.window.addEventListener('load', function() {
        btn2.click()
    })
  })

  btn2.addEventListener('click', function() {
    console.log('btn2 点击');
    t2.postMessage({type: 'popring', message: 't1 发送出去的消息'}, '/')
  })

  window.addEventListener('message', function(event) {
    // 过滤非当前域下的消息
    if(event.origin !== 'http://127.0.0.1:5500' || !event.data) {
      return
    }
    // 过滤其他非本应用传递过来的消息,例如 chrome 的插件就可能也会发送消息(表示 wappalyzer 就会)
    if(event.data?.type !== 'popring') {
      return
    }
    text.innerText += (JSON.stringify(event.data)+'\n')
  })
</script>
<h1>这是 T2</h1>
<button id="btn">发送消息到 t1</button>
<pre id="text"></pre>

<script>
  var btn = document.querySelector('#btn')
  var text = document.getElementById('text')
  var t1
  
  // t2 定义的 onload 事件
  window.onload = function() {
    console.log('t2 onload');
  }
  
  
  btn.addEventListener('click', function() {
    t1.postMessage({type: 'popring', message: 't2 发送出去的消息'}, '/')
  })

  window.addEventListener('message', function(event) {
    if(event.origin !== 'http://127.0.0.1:5500' || !event.data) {
      return
    }
    if(event.data?.type !== 'popring') {
      return
    }
    t1 = event.source
    text.innerText += (JSON.stringify(event.data)+'\n')
  })
</script>

本次使用 DOMContentLoaded 效果如下。

image.png

第四版

现在已经可以两窗口之间进行通信了,剩下的就没有什么了,完善下基本功能即可,顺带发现了几个有趣的 API。

关闭窗口

window.close()

只需注意这条命令只可以关闭使用js打开的窗口。

聚焦窗口

window.focus()

经过测试只可以从A页面聚焦到B页面,B页面无法调用此函数到A页面。

检查窗口是否关闭

window.closed

最后完成的原生 html、js代码

<h1>这是 T1</h1>
<button id="btn1">打开t2</button>
<button id="btn2">发送消息到t2</button>
<pre id="text"></pre>

<script>
  var t2
  var btn1 = document.getElementById('btn1')
  var btn2 = document.getElementById('btn2')
  var text = document.getElementById('text')

  btn1.addEventListener('click', function() {
    t2 = window.open('/t2.html')
    window.focus()
    t2.addEventListener('DOMContentLoaded', function() {
      t2.console.log('t1 挂载在 t2 的 DOMContentLoaded');
      btn2.click()
    })
  })

  btn2.addEventListener('click', function() {
    console.log('btn2 点击');
    // t2 页面是否已关闭
    if(t2.closed) {
      return
    }
    // postMessage 第三个参数设置为 '/' 表示当前域下传递消息
    t2.postMessage({type: 'popring', message: 't1 发送出去的消息'}, '/')
    t2.focus()
  })

  window.addEventListener('message', function(event) {
    // 过滤非当前域下的消息
    if(event.origin !== 'http://127.0.0.1:5500' || !event.data) {
      return
    }
    // 过滤其他非本应用传递过来的消息,例如 chrome 的插件就可能也会发送消息(表示 wappalyzer 就会)
    if(event.data?.type !== 'popring') {
      return
    }
    text.innerText += (JSON.stringify(event.data)+'\n')
  })
</script>
<h1>这是 T2</h1>
<button id="btn">发送消息到 t1</button>
<pre id="text"></pre>

<script>
  window.addEventListener('DOMContentLoaded', function() {
    console.log('t2 DOMContentLoaded');
  })

  var btn = document.querySelector('#btn')
  var text = document.getElementById('text')
  var t1

  btn.addEventListener('click', function() {
    // t1 页面是否已关闭
    if(t1.closed) {
      return
    }
    t1.postMessage({type: 'popring', message: 't2 发送出去的消息'}, '/')
    t1.focus()
  })

  window.addEventListener('message', function(event) {
    if(event.origin !== 'http://127.0.0.1:5500' || !event.data) {
      return
    }
    if(event.data?.type !== 'popring') {
      return
    }
    t1 = event.source
    text.innerText += (JSON.stringify(event.data)+'\n')
  })
</script>

可以在 codesandbox 体验下 codesandbox.io/s/modest-sh…

建议新窗口打开体验。

image.png

result

最后总结下结果

1、postMessage 发送需要获取接收方的 window 对象

2、从A页面打开B页面,需要等B页面加载完后才可以监听到事件触发,而A页面可以通过B页面的 DOMContentLoaded 事件来感知B页面加载完毕

3、其他相关的一些 API

4、这只是原生js实现,如果结合 Vue、React相关框架更会有不一样的感觉,而且他们有更加精细的生命周期。

reference

MDN postMessage

姊妹篇

浏览器间的跨窗口通信之storage