理解 postMessage 通信
postMessage
是一种安全的跨源通信方法,用于在两个不同的浏览上下文(如不同的窗口、iframe、标签页)之间发送消息。
使用 postMessage
相互通信的每个源都需要有一个 message
监听器,调用对方的 postMessage
方法来向对方传递信息,对方的message
监听器根据 event
的 origin
来过滤它想要的信息。
发送消息
在使用 postMessage
时,首先需要确定要发送消息的目标窗口。可以通过 window.open
打开的窗口对象,或者通过 document.getElementById
获取的 iframe 元素的 contentWindow 属性来获取目标窗口。
// 获取目标窗口对象
const target = document.getElementById('iframe').contentWindow;
// 发送消息
target.postMessage('i am parent', 'https://example.com');
接收消息
接收消息的一方需要监听 message
事件,通过 event.data
来获取发送过来的数据。
window.addEventListener('message', function(event) {
if (event.origin === 'https://example.com') {
console.log('message:', event.data);
}
}, false);
双向通信的典型例子
// parent page
// 发送数据给 child
const iframe = document.getElementById('iframe');
iframe.contentWindow.postMessage('this is parent', 'http://www.sourceB.com');
// 接收来自 child 的数据,其实不单单是子 iframe 的数据
// 而是可以来自任何源的数据,所以要对源鉴别一下
// 来自 parent 自己的消息也是可以的,不信可以试一试
window.addEventListener('message', function (event) {
if (event.origin === 'http://www.sourceB.com') {
console.log('data from child:', event.data);
}
});
// child page
window.addEventListener('message', function (event) {
if (event.origin === 'http://www.sourceA.com') {
console.log('data from parent:', event.data);
// 处理数据
const result = event.data + ' I am from child.';
// 返回数据给 parent
event.source.postMessage(result, event.origin);
}
});
我们假设一个 h5 开发的场景,parent 是一个原生 app 中的容器页面,child 是一个内嵌的页面,内嵌页面需要从原生 app 获取一些必要信息来进行展示,它们之间的通信流程是类似以下的:内嵌页面通过 postMessage
向容器页面发送一个 event
,容器页面通过 message
监听器获取这个 event
,然后它调用原生 app 的方法处理这个 event
获得结果,容器页面再通过 postMessage
向内嵌页面发送结果,内嵌页面通过 message
监听器获取这个结果。好了,内嵌页面终于获得了它想要的信息,想想这有什么问题呢,能不能优化下?
child 和 parent 的通信方式的处理模型是不是和后端响应前端 API 请求一样的,但对于 API 请求,前端只需要一句 const response = await fetch(url)
就好了,并不需要如此多的异步代码。这般的异步代码,逻辑并不类聚,带来更大的维护成本,我们来优化吧,把它变得如同同步代码一般。
优化
把异步代码变成用同步的方式书写,无他,得利用 promise
封装一下,使其用起来像同步代码一样清晰。
class Bridge {
constructor() {
this.messageHandlers = new Map();
this.messageId = 0;
// listen for messages from parent
window.addEventListener("message", (event) => {
const { type, messageId, data } = event.data;
const handler = this.messageHandlers.get(messageId);
if (handler) {
handler(data);
this.messageHandlers.delete(messageId);
}
});
}
// send message and wait for response
async callHandler(actionName, data = {}) {
return new Promise((resolve) => {
const messageId = this.messageId++;
this.messageHandlers.set(messageId, resolve);
window.parent.postMessage(
{
type: actionName,
messageId,
data,
},
"*"
);
});
}
}
在 Bridge
的帮助下,我们上述的例子就可以如下实现,是不是清晰了许多,不需要弯弯绕绕。
const bridge = new WebViewBridge();
const userInfo = await bridge.callHandler('getUserInfo');
Preview Demo on StackBlitz