微前端(qiankun)接入过程

195 阅读4分钟

本次接入主应用(vue3 + vite)路由采用history模式,子应用(vue2 + webpack)路由也采用history模式;

主应用改造如下:

安装依赖;

npm i -D qiankun

创建目录src/qiankun用于存放qiankun相关的方法和配置;src/qiankun 创建config.ts,添加子应用配置:

import { Session } from '/@/utils/storage';

const isDev = !['test', 'prod'].includes(import.meta.env.MODE);

/** 魔方ui子应用前缀 */

export const magicCubeUi = 'magic-cube-ui';

// 微应用-子应用的配置

export const subAppsConfig = {

    subApps: [

        {

            name: magicCubeUi, // 子应用名称,跟package.json一致

            // entry: '//localhost:8081/', // 子应用入口,本地环境下指定端口

            entry: isDev ? '//localhost:8081/' : '/magicCubeUi/', // 子应用入口,本地环境下指定端口

            container: '#sub-container', // 挂载子应用的dom

            activeRule: /${magicCubeUi}, // 路由匹配规则

            // 主应用与子应用通信传值

            props: {

                getToken: () => Session.get('token'),

            },

        },

    ],

};

 创建src/qiankun/index.ts,添加子应用启动、通信相关方法;

import { subAppsConfig } from './config';

import { loadMicroApp, registerMicroApps, prefetchApps, start, initGlobalState } from 'qiankun';

const { subApps } = subAppsConfig;

/** 注册子应用(registerMicroApps + start) */

export const registerApps = () => {

    try {

        registerMicroApps(subApps, {

            beforeLoad: [(app) => Promise.resolve(app)],

            beforeMount: [(app) => Promise.resolve(app)],

            afterUnmount: [(app) => Promise.resolve(app)],

        });

        start({

            sandbox: {

                experimentalStyleIsolation: true,

            },

        });

    } catch (err) {

        console.log(err);

    }

};

/** 手动加载微应用,该方法加载在不切换子应用时候,可以实现keep-alive */

export const loadApp = () => {

    const vm = loadMicroApp(subApps[0], {

        sandbox: {

            experimentalStyleIsolation: true,

        },

    });

    onUnmounted(() => vm.unmount());

};

/** 预加载子应用 */

export const preFetchSubApp = () => {

    prefetchApps([

        {

            name: subApps[0].name,

            entry: subApps[0].entry,

        },

    ]);

};

/** 应用间通信 */

export const microAppCommunicate = () => {

    // 初始化 state

    const state: { action: string; payload: any } = {

        action: '',

        payload: null,

    };

    const actions = initGlobalState(state);

    return {

        state,

        actions,

    };

};

创建组件,作为启动子应用的vue挂载容器;

qiankun提供两种方式启动子应用。第一种为 registerMicroApps + start方式,该方式需要注意:第一是挂载的容器不能被transition包裹,否则路由切换后,该元素被销毁,后续无法再次拉起子应用;第二是,因为路由切换到主应用其他路由时候,子应用被销毁,所以子应用keep-alive无法生效;所以采用手动加载子应用(loadMicroApp)的方式,来拉起子应用,该方式在主应用main.ts可以预加载指定子应用,来更快拉起子应用;

添加路由,用于匹配子应用相关页面;

    {

        path: '/magic-cube-ui',

        name: 'magic-cube-ui',

        meta: {

            title: '智能运营平台',

        },

        component: () => import('/@/layout/routerView/parent.vue'),

        children: [

            {

                path: '/:pathMatch(.)',

                meta: {

                    title: '智能运营平台',

                },

                component: () => import('/@/views/qiankun/subContainer.vue'),

            },

        ],

    },

子应用改造如下:

创建src/public-path.js(需在main.js引入);

if (window.POWERED_BY_QIANKUN) {

     // eslint-disable-next-line no-undef

     webpack_public_path = window.INJECTED_PUBLIC_PATH_BY_QIANKUN

}

main.js 暴露生命周期给主应用:

let instance = null;

function render(props = {}) {

    const { container } = props;

    // 每次渲染的时候调用redirectPopup事件

    redirectPopup(props);

    instance = new Vue({

        router,

        store,

        render: (h) => h(App),

    }).$mount(container ? container.querySelector('#app') : '#app');

}

if (!window.POWERED_BY_QIANKUN) {

    render();

}

export async function bootstrap() {

    console.log('[vue] vue app bootstraped');

}

export async function mount(props) {

    // console.log("[vue] props from main framework", props);

    setToken(props.getToken());

    actions.setActions(props);

    // 加载完毕,通知主应用

    actions.setGlobalState({

        action: 'loaded',

        payload: '1',

    });

    actions.onGlobalStateChange((state, _prev) => {

        // state: 变更后的状态; prev 变更前的状态

    }, true);

    render(props);

}

export async function unmount() {

    instance.$destroy();

    instance.$el.innerHTML = '';

    instance = null;

    document.body.appendChild = originFn;

}

改造路由为history模式

const router = new VueRouter({

  mode: isQianKun() ? "history" : "hash",

  base: isQianKun() ? /magic-cube-ui : process.env.BASE_URL,

  scrollBehavior: (to, from, savedPosition) => savedPosition || { x: 0, y: 0 },

  routes: basicsRoutes,

});

创建action.js,用于和主应用之间进行通信

function emptyAction() {

    console.warn('action is empty');

}

class Action {

    actions = {

        onGlobalStateChange: emptyAction,

        setGlobalState: emptyAction,

    };

    setActions(actions) {

        this.actions = actions;

    }

    onGlobalStateChange(...args) {

        this.actions.onGlobalStateChange(...args);

    }

    setGlobalState(...args) {

        this.actions.setGlobalState(...args);

    }

}

/** 用于和主应用进行通信 */

export const actions = new Action();

vue.config.js改造

第一是需要配置headers来允许跨域;第二是需要打包成umd格式,来满足qiankun的调用要求;这里需要将package.json里面的name修改为我们子应用公共路由前缀magic-cube-ui。

const { name } = require('./package')

module.exports = { 

    devServer: { 

        port: 7002, 

        headers: { 'Access-Control-Allow-Origin': '*', }, 

    }, 

    configureWebpack: { 

        output: { 

            library: ${name}-[name], libraryTarget: 'umd', // 把微应用打包成 umd 库格式 

            jsonpFunction: webpackJsonp_${name}

        }, 

    }, 

}

到这里,在主应用和子应用的代理里面配置一下之后,就可以运行起来了(子应用的导航和顶部需要判断是否运行在微前端环境,来进行隐藏)。接入的过程中,最初是使用registerMicroApps + start方式,坑很多,比如因为transition包裹,切换路由后挂载元素被干掉,导致子应用无法拉起。比如在子应用路由下刷新页面,会导致页面主应用部分消失.....所以采用了loadMicroApp手动加载子应用的方式。到这里就剩下两个主要的问题了,第一个是部署问题,第二个是主应用、子应用样式冲突问题。第一个问题,采用的是同域部署,即在主应用(这里是播控)目录下面创建一个目录,来存放子应用文件。这里需要注意的是子应用的目录名称(magicCubeUi)要和子应用路由公共前缀(magic-cube-ui)区分开,否则将主应用路由后面部分去掉,会直接进入到子应用。然后需要主应用打包配置(vite.config.ts)里面的base修改为 / ,子应用的publicPath为 /magicCubeUi/ , 然后需要配置nginx来避免刷新后因为history路由导致页面资源404,配置如下:

        location / {

            root   iptv-ui;

            index  index.html index.htm;

            try_files uriuri uri/ /index.html;

        }

        

        location /magicCubeUi {

            root   iptv-ui;

            index  index.html index.htm;

            try_files uriuri uri/ /magicCubeUi/index.html;

        }

第二个问题,样式冲突问题主要是因为子应用是挂载在主应用的一个元素下(#sub-container),所以子应用会被主应用的全局样式影响,所以需要修改子应用下某些元素的全局样式,例如全局样式所有元素outline: none !imporant,子应用某些样式又需要outline的,就需要指定具体元素的outline,并提高优先级。虽然子应用的配置开启了样式隔离(experimentalStyleIsolation: true会将子应用的样式添加一个qiankun开头的hash,类似scoped),但是element-ui的弹框、下拉框选择项、时间选择等默认都是插入到body里面,导致样式无法生效;经过各种方案尝试,发现最合适的方法是,在子应用内容指定一些类名,然后修改子应用的document.body的appendChild方法,当判断到要插入的元素包含指定类名时,将需要插入的元素插入到调用元素而非body下,方法如下:

// 初始的document.body.appendChild事件

const originFn = document.body.appendChild.bind(document.body);

// 级联的样式如果也append到指定元素下面,会导致样式错乱。

const whiteList = [

    'el-select-dropdown',

    'el-dialog__wrapper',

    'el-dropdown-menu',

    'el-message-box__wrapper',

    'el-tooltip__popper',

    'el-date-picker',

    'el-popover',

    'el-popper',

];

const blackList = ['el-cascader__dropdown'];

function redirectPopup(container) {

    // 子应用中需要挂载到子应用的className。样式class白名单,用子应用的样式。

    document.body.appendChild = new Proxy(document.body.appendChild, {

        apply: (target, thisArg, argumentsList) => {

            const dom = argumentsList[0];

            const isInWhiteList = whiteList.some((x) => dom.className.includes(x));

            const isInBlackList = blackList.some((x) => dom.className.includes(x));

            if (isInWhiteList && container.container && !isInBlackList) {

                // 将需要处理的元素,挂载的元素挂载到子应用上,而不是主应用的 body 上

                container.container.querySelector('#app').appendChild(dom);

            } else {

                return target.apply(thisArg, argumentsList);

            }

        },

    });

}

function render(props = {}) {

    const { container } = props;

    // 每次渲染的时候调用redirectPopup事件

    redirectPopup(props);

    instance = new Vue({

        router,

        store,

        render: (h) => h(App),

    }).$mount(container ? container.querySelector('#app') : '#app');

}

// 子应用卸载时候,需要还原appendChild方法;

export async function unmount() {

    instance.$destroy();

    instance.$el.innerHTML = '';

    instance = null;

    document.body.appendChild = originFn;

}

到这里,就勉强能用了!!!