浏览器间的跨窗口通信之postMessage
origin
最近做项目恰巧遇到需要个小功能,简单描述下
从A页面新开歌窗口打开B页面,B页面完成一系列操作后,需要自动关闭B页面,然后A页面完成一个刷新的动作。
我一想这不就是跨窗口通信么,然后网上搜索引擎一顿查找,发现相关的文章却是不多,最秀的是很多都是复制粘贴的,而且都是围绕 iframe 写的,不算事真正意义上的两个窗口交互,于是就有了这么踩坑文章。
解决方法有很多种,本次只聊 postMessage。本文主要是介绍如何解决这个需求,没有其他干货。
postMessage
原本是是作为跨源通信的新特性出现的,只要能获取要发送窗口的window,就可以消息发出,在接收侧可以将一些不安全的消息进行过滤,防止出现安全问题。
看起来似乎也没有啥兼容性问题,就很棒。
实现
首先说下文章中用到了几个不同的叫法,但都是同一个意思
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> 复制代码
然后发现根本没用(还是看文档不仔细),直到我看到文档的这里
所以意思是需要其他窗口的 window 对象来调用 postMessage ,这才监听 message 事件才会被触发。
但是这里回想下最初的需求,是需要 A 页面打开一个新窗口 - B页面, B页面之后再发送一个 message 给 A 页面,此时的想法是这个样子。
第二版
如果这里调用 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 特性如下。
<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 效果如下。
第四版
现在已经可以两窗口之间进行通信了,剩下的就没有什么了,完善下基本功能即可,顺带发现了几个有趣的 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…
建议新窗口打开体验。
result
最后总结下结果
1、postMessage 发送需要获取接收方的 window 对象
2、从A页面打开B页面,需要等B页面加载完后才可以监听到事件触发,而A页面可以通过B页面的 DOMContentLoaded 事件来感知B页面加载完毕
3、其他相关的一些 API
4、这只是原生js实现,如果结合 Vue、React相关框架更会有不一样的感觉,而且他们有更加精细的生命周期。
作者:江无花
链接:https://juejin.cn/post/7004111974841712647