场景
如下图,iframe
嵌在了父窗口中,iframe
中有两个按钮,退出登录
和唤起扫码
, 点击退出登录
会回到父窗口的登录页,而唤起扫码
则会调用父窗口的扫码,扫码成功后需要把扫码的结果返回给iframe
。
因为iframe
和父窗口是不同源的,所以是不能直接操作父窗口的,因为获取不到。所以对于这种场景应该使用postMessage
来实现通讯。接下来说下如果实现具体的场景操作。
通信SDK
我们主要解决两个问题:
- iframe怎么和父窗口通信,比如让父窗口回到登录页
- 每次postMessage调用父窗口的方法,数据的格式是一个Promise
具体实现
- 定义要提供给
iframe
的方法 iframe
绑定message
事件iframe
需要引入父窗口提供的SDK
定义要提供给iframe
的方法
父窗口需要提供给iframe
的方法: 如navigateToLogin
和scanQRCore
这两个方法
// ACTIONS.js
export default {
async navigateToLogin(query) {
await store.$dispatch('user/logout, query');
return router.push({
path: '/login',
query,
})
},
async scanQRCore(query) {
return await JsBridge.call({
type: 'SCAN:CODE',
query
});
}
}
复制代码
iframe
绑定message
事件
vue
组件为例,在组件内完成对iframe
绑定message
事件。
<template>
<iframe ref="merchant">
</iframe>
</template>
<script>
import ACTION_MAP from './ACTIONS';
export default {
created() {
window.addEventListener('message', this.messageListener);
this.$once('hook:beforeDestroy', () => {
window.removeEventListener('message', this.messageListener);
});
},
methods: {
async messageListener({ data }) {
const iframe = this.$refs.merchant;
try {
const { action, params } = data;
if (action && ACTION_MAP[action]) {
console.log('SDK事件', action, '参数', JSON.stringify(params));
try {
const response = await ACTION_MAP[action](params);
iframe.contentWindow.postMessage({
action,
response
});
} catch (error) {
iframe.contentWindow.postMessage({
action,
response: Promise.reject(error)
});
}
}
} catch (error) {}
}
}
};
</script>
复制代码
iframe
需要引入父窗口提供的SDK
最终暴露给iframe
的SDK.js
,我们希望SDK
的每个API
返回都是一个Promise
,那么Promise
的resolve
时则由父窗口postMessage
时机决定。通信方式如下
// https://parent.com/helpers/sdk.js
!(function(root, fatctory) {
root.SDK = fatctory();
})(window, function() {
let topWindow = null;
const APIS = {};
const ACTION_MAP = [
'scanQRCore',
'navigateToLogin',
].reduce((r, c) => (r[c] = c) && r, {});
// 确保iframe已经加载,否则topWindow可能还是null
const loadPromise = new Promise((resolve, reject) => {
window.addEventListener('load', () => {
resolve();
topWindow = window.top || window.parent;
window.addEventListener('message', e => {
const data = e.data;
try {
const { action, response } = data;
// 如果收到父窗口的postMessage,则resolve该API
if (action) {
APIS[action].rs(response);
}
} catch (error) {}
});
});
});
const postMessagePromise = (action, params = {}) => {
return new Promise((rs, rj) => {
// 每个api什么时候resolve?
APIS[action] = { rs, rj };
loadPromise.then(() => {
SDK.postMessage({
action,
params
});
});
});
};
const SDK = {
[ACTION_MAP.postMessage](data) {
data.action = data.action || ACTION_MAP.postMessage;
// 向父窗口发出消息
topWindow && topWindow.postMessage(data, '*');
},
// 每个API都是返回一个Promise
[ACTION_MAP.scanQRCore](data) {
return postMessagePromise(ACTION_MAP.scanQRCore, data);
},
[ACTION_MAP.navigateToLogin](data) {
return postMessagePromise(ACTION_MAP.navigateToLogin, data);
},
}
return SDK;
});
复制代码
上面的SDK.js包含了Promise语法,使用rollup打包的时候需要引入es-promise的polyfill
扫描二维码关注公众号,回复: 13470549 查看本文章![]()
引入, 即可完成和父窗口通信。
<!DOCTYPE html>
<html lang="en">
<head>
<script src="https://parent.com/helpers/sdk.js"></script>
</head>
<body>
</body>
</html>
复制代码
why not qiankun
如果不考虑体验问题,iframe是最合适微前端的,天生的隔离机制,但是使用iframe有以下痛点
- cookie无法跨域携带,子应用在做免登的时候处理麻烦
- UI 不同步,DOM 结构不共享。iframe里来要一个相对页面垂直水平居中的弹框还要通过父窗口位置计算
- url 不同步。浏览器刷新 iframe url 状态丢失、后退前进按钮无法使用
- postMessage 传输的数据只能为字符串
上面的问题,也许很多都是开发和体验上的一些问题,虽然最后都有一些手段hack,但是这么搞主应用跟 iframe 应用都得累死。 为什么我们还用iframe方案? 主要是还是历史包袱,对接了太多部分子应用,业务方不肯改,你想推也推不动。毕竟这玩意就是一个优化方案,做好了没表扬,体验差别不大,出问题了谁背锅?