iframe怎么和父窗口通信

场景

如下图,iframe嵌在了父窗口中,iframe中有两个按钮,退出登录唤起扫码, 点击退出登录会回到父窗口的登录页,而唤起扫码则会调用父窗口的扫码,扫码成功后需要把扫码的结果返回给iframe

iframe.png

因为iframe和父窗口是不同源的,所以是不能直接操作父窗口的,因为获取不到。所以对于这种场景应该使用postMessage来实现通讯。接下来说下如果实现具体的场景操作。

postMessage-MDN

通信SDK

我们主要解决两个问题:

  • iframe怎么和父窗口通信,比如让父窗口回到登录页
  • 每次postMessage调用父窗口的方法,数据的格式是一个Promise

具体实现

  • 定义要提供给iframe的方法
  • iframe绑定message事件
  • iframe需要引入父窗口提供的SDK

定义要提供给iframe的方法

父窗口需要提供给iframe的方法: 如navigateToLoginscanQRCore这两个方法

// 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

最终暴露给iframeSDK.js,我们希望SDK的每个API返回都是一个Promise,那么Promiseresolve时则由父窗口postMessage时机决定。通信方式如下

post.png

// 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

引入, 即可完成和父窗口通信。

<!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方案? 主要是还是历史包袱,对接了太多部分子应用,业务方不肯改,你想推也推不动。毕竟这玩意就是一个优化方案,做好了没表扬,体验差别不大,出问题了谁背锅?

猜你喜欢

转载自juejin.im/post/7036330480949526542