背景
到 iframe 虽然存在诸多弊病,但在开发程序的过程中,有时候还是不可避免的需要使用到 iframe。
对于 iframe 父子窗口通信,其中一种比较常用的方式就是 postMessage,且跨域的时候也只有这种办法能用得上。
但这种通信方式是基于事件的发送和监听实现的,使用的是回调的方式。
如果业务场景中期望“父窗口往子窗口发消息后,要堵塞等待子窗口的返回,再去执行接下来的逻辑”,直接使用这种回调的方式就实现不了。
封装思路
由于本人主要是用 vue 开发前端项目,联想到 vue 中有一个 $once
函数,可以进行一个一次性的事件监听。那么可以尝试把这个特性利用起来。(实际上,如果不是 vue 技术栈,也可以用其他的第三方库或者自己封装出一个 $once
,这里为了图省事就用 vue 自带的 api 了)
父窗口的逻辑:
- 监听 message 事件,如果收到子窗口发来的消息,就调用
this.$emit()
触发一次自定义事件。 - 调用
postMessage()
给子窗口发消息,发送之后使用this.$once()
监听一次自定义事件。
子窗口的逻辑:
- 监听 message 事件,如果收到父窗口发来的消息,就调用
postMessage
给父窗口回信。
可以看到大致思路:在 message 事件之外,又使用了一个自定义事件,作为中间层。
代码实现
父窗口逻辑:
<template>
<div>
<div>
<h1>这里是父窗口</h1>
<button @click="handleClick">点这里给子窗口发消息</button>
</div>
<iframe id="iframe" src="/#chilren"></iframe>
</div>
</template>
<script>
export default {
methods: {
async handleClick() {
let resp = await this.sendAndGetResponse();
console.log(resp);
},
// 发送数据给子窗口
sendAndGetResponse() {
let targetWindow = document.getElementById("iframe").contentWindow;
// 用这个来标识父子窗口之间的每一次会话,同时也用作事件名
let requestId = `request_${new Date().getTime()}`;
// 父窗口给子窗口发消息
let requestMsg = {
requestId: requestId,
params: {
msg: "父窗口发给子窗口的消息......",
},
};
targetWindow.postMessage(requestMsg, "*");
// 超时的Promise
let timeOutPromise = new Promise((resolve, reject) => {
setTimeout(() => {
reject("超时");
}, 500);
});
// 正常的Promise
let normalPromise = new Promise((resolve, reject) => {
this.$once(requestId, (responseMsg) => {
if (responseMsg.resCode == "0") {
resolve(responseMsg.result);
} else {
reject(responseMsg.resMsg || "返回值格式错误");
}
});
});
// 用Promise.race进行超时处理
return Promise.race([timeOutPromise, normalPromise]);
},
receiveMessage(event) {
let responseMsg = event.data;
if (!responseMsg.requestId) {
return;
}
this.$emit(responseMsg.requestId, responseMsg);
},
},
mounted() {
window.addEventListener("message", this.receiveMessage, false);
},
};
</script>
子窗口逻辑:
<template>
<div>
<h1>这里是子窗口</h1>
</div>
</template>
<script>
export default {
methods: {
receiveMessage(event) {
let requestMsg = event.data;
let responseMsg = {
requestId: requestMsg.requestId,
resCode: "0",
resMsg: "成功",
result: {},
};
event.source.postMessage(responseMsg, event.origin);
},
},
mounted() {
window.addEventListener("message", this.receiveMessage, false);
},
};
</script>
代码中的几处细节:
- 考虑到子窗口有可能无响应的情况,加了一个超时处理,防止父窗口的 await 长时间堵塞等待。
- 跨窗口通信无法直接传递异常,所以把消息对象进行了封装,加了状态码的字段用来区分正常返回还是异常返回。
- 实际使用中需要关注一下 postmessage 的安全问题,在发送和接收消息的时候要对通信目标进行过滤。这里为了代码逻辑简单就没做这个事情。