前几天做了一个小项目,其中用到了跨域数据传递 window.postMessage
,这里记录一下遇到的几个问题。
首先我们来简单了解下 postMessage 的用法,消息的传递包含发送方和接受方,首先双方要能够获取到对方的 window 实例,然后发送方调用接受方的 window.postMessage 来发送一条消息:
发送消息
targetWindow.postMessage('这是 a 发送的一条数据', 'http://localhost:8001')
第一个参数是发送数据,第二个参数是一个字符串,发送之前会拿这个网址去和目标 window 进行跨域校验,必须协议、域名、端口相同,信息才会正常发送。
这个是用于 避免发送方的数据泄露 而设置的规则。因为发送方无法通过直接读目标 window 对象的方式确认其身份(会被跨域拦下来)。发送方也不知道打开的目标网址会不会重定向到其他地方,为了避免发送密码之类的隐私数据泄露出去,浏览器提供了这个参数,由浏览器对目标窗口进行检查。如果检查不通过就会取消发送。也可以通过显式设置为 "*"
来一直允许发送消息。
注意,消息是否取消发送这件事,发送方是无法得知的,无论成功与否,都不会有任何返回值和异常抛出,这个有点坑。
接受消息
在消息接受方,可以通过 window.addEventListener
添加一个 message
事件监听来获取其他窗口发来的消息,如下:
window.addEventListener('message', console.log)
// MessageEvent {isTrusted: true, data: "这是 a 发送的一条数据", origin: "http://localhost:8002", lastEventId: "", source: Window, …}
接收到的 MessageEvent
里有两个属性需要关注,一个是 data
,其他窗口发来的数据就保存在这里。另一个是 source
,指向了消息的发送窗口,可以直接通过这个 window 回发消息(但是依旧无法直接访问)。
简单了解了怎么用之后,下面写一些开发的时候遇到的问题,主要都是 window.open
和 iframe
的区别导致的。
问题 1:消息发送不过去
一开始写的代码是这样的:
// a.html
const targetWindow = window.open('http://localhost:8002/b.html');
targetWindow.onload = () => {
targetWindow.postMessage('这是 b 发过来的一条数据', 'http://localhost:8001')
}
但是并不行,发现 onload
事件无论如何都无法触发。这个的原因是网上的教程大多都是使用 iframe
打开的新窗口,监听的是 iframe
的 onload 事件。而使用 window.open 方式打开的新窗口是无法访问打开窗口 window 对象的 onload 事件的。
那么应该如何监听目标页面是否初始化完成的呢?直接发送消息肯定是不行的,新窗口在加载完成之前如果向其发送了消息,那么发送就会被直接取消。这里可以通过进行一个简单的“握手”操作来实现:
- 发送方添加 message 监听,同时打开接受方窗口
- 接受方加载完成后向父窗口 postMessage,通知父窗口我已就绪
- 父窗口触发 message 回调,向接受方发送信息
具体代码如下:
发送方 a.html
window.addEventListener('message', ({ origin, data, source }) => {
// 确认身份及对象是否准备完成
if (origin !== 'http://localhost:8002' || data !== 'ready') return;
// 确认完成,发送数据
source.postMessage('我是真正的数据', 'http://localhost:8002');
})
window.open('http://localhost:8002/b.html');
接受方 b.html
window.addEventListener('message', ({ origin, data }) => {
if (origin !== 'http://localhost:8001') return;
console.log('收到数据', data);
})
window.opener.postMessage('ready', 'http://localhost:8001');
问题 2:信息错误的发给了自己
一开始,我在 b.html 中写的发送消息是这样的:
window.parent.postMessage('ready', 'http://localhost:8001');
结果无论如何 a.html 都无法获取到对应的 ready 信息,并且离谱的是 b.html 自己却收到了消息,后来查阅了 文档 后才发现,window.parent
默认指向自己,而不是指向空。并且只有在以 iframe 或者 object 标签打开时才会指向其父窗口!也就是说使用 window.open 打开的窗口是无法通过这个属性获取到打开者窗口的,那么怎么访问呢?
其实上文已经贴出来了,就是 winodw.opener
属性,这个属性指向了其打开者。但是用 iframe 标签打开新窗口时,这个属性并不会指向其父窗口,而是 null。总结一下就是下面这样:
打开方式 | window.parent | window.opener |
---|---|---|
<iframe /> | 指向父窗口 | null |
window.open | 指向自己 | 指向父窗口(打开者) |
问题3:报错 Uncaught DOMException
具体错误内容如下:
![](https://img.haomeiwen.com/i13523736/cf62ede369525d09.png)
这就是刚才提到的,如果直接访问其他 window 对象的话就会被跨域拦下来。无论是访问 window 本身还是其属性,但是 postMessage 是个特例(毕竟本身就是为了跨域通讯而生的):
console.log(window.parent)
// Uncaught DOMException: ...
console.log(window.parent.parent.window)
// Uncaught DOMException
console.log(window.parent.postMessage)
// ƒ () { [native code] }
这个异常不仅限于 window.parent,只要访问能得到的任意其他窗口的 window 实例,都会报这个错。
网友评论