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相关框架更会有不一样的感觉,而且他们有更加精细的生命周期。