一些微前端架构的实践总结_微前端最佳实践

一些微前端架构的实践总结_微前端最佳实践

编程文章jaq1232025-10-08 18:34:0523A+A-

前言:在平时接触的项目中需要用到微前端架构解决巨石应用的问题。在经过一段时间的调研和实践中总结出一些微前端架构的经验。

技术选择

  • 主应用:react(基于umi和antd-pro)
  • 子应用:react(基于umi搭建)、vue(基于vue-cli搭建) ——(举两种常用的技术栈,其他技术栈以后再补充)

基础配置

主应用qiankun配置(umi项目)

使用@umijs/plugin-qiankun配置

该方法适合子应用都是基于umi架构的react项目,如果子应用是vue项目(项目复杂度比较高),用该插件配置的主应用会出现切换vue子应用时,因为js沙箱失效导致404问题。

  • 安装插件
  • yarn add qiankun
    yarn add @umijs/plugin-qiankun -D
  • 注册子应用
  • // config/config.ts
    
    export default defineConfig({
      .... // 其他配置代码
      qiankun: {
      master: {
        apps: [
                {
                  name: 'sub-app-1',
                  entry: '//localhost:8001'
                },
                {
                  name: 'sub-vue-app',
                  entry: '//localhost:9528'
                }
        		]
      	}
    	}
    })
  • 装载子应用
  • // config/routes.ts
    
    export default [
       ....  // 其他路由配置
       {
          name: 'sub-app-1',
          icon: 'smile',
          path: '/sub-app-1',
          microApp: 'sub-app-1',
          microAppProps: {
              className: 'sub-app-1',
          },
        },
        {
          name: 'sub-vue-app',
          icon: 'smile',
          path: '/sub-vue-app',
          microApp: 'sub-vue-app',
          microAppProps: {
              className: 'sub-vue-app',
          },
      	},
    ]

    不使用@umijs/plugin-qiankun配置,直接用qiankun提供的配置方法

    该方法兼容不同技术栈的子应用

  • 安装插件
  • yarn add qiankun
  • 注册子应用
  • // app.tsx
    import { registerMicroApps, setDefaultMountApp, start } from 'qiankun';
    
    const microAppsOptions = [
      {
        name: 'sub-app-1',
        entry: '//localhost:8001',
        container: '#subapp-container',
        activeRule: '/sub-app-1',
        className: 'sub-app-1',
      },
    {
        name: 'sub-vue-app',
        entry: '//localhost:9528',
        container: '#subapp-container',
        activeRule: '/sub-vue-app',
        className: 'sub-vue-app',
    },
    ];
    
    registerMicroApps(microAppsOptions);
    setDefaultMountApp('/sub-app-1');
    
    ....
    // 添加子应用渲染容器
    export const layout: RunTimeLayoutConfig = ({ initialState }) => {
    ....
    childrenRender: (children) => {
          let activeClassName = '';
          microAppsOptions.forEach((item) => {
            if (history.location.pathname.startsWith(item.activeRule)) {
              activeClassName = item.className;
            }
          });
          return (
            <>
              {children}{' '}
              {history.location.pathname !== loginPath && (
                <div id="subapp-container" className={activeClassName} />
              )}
            </>
          );
     },
    }
  • 装载子应用
  • // config/routes.ts
    
    export default [
      ....  // 其他路由配置
      {
          name: 'sub-app-1',
          icon: 'smile',
          path: '/sub-app-1',
          microApp: 'sub-app-1',
        },
        {
          name: 'sub-vue-app',
          icon: 'smile',
          path: '/sub-vue-app',
          microApp: 'sub-vue-app',
      },
    ]
    

    子应用qiankun配置

    vue项目

    • 入口文件main.js
    // 声明一个变量,可以用于卸载
    let instance: any = null;
    let router: any = null;
    // 挂载到自己的html中,基座会拿到这个挂载后的html插入进去
    function render(props = {}) {
      const container:any = (props as any).container;
    
      router = new VueRouter({
        base: (window as any).__POWERED_BY_QIANKUN__ ? '/sub-vue-app/' : '/',
        mode: 'history',
        routes,
      });
    
      instance = new Vue({
        router,
        store,
        render: h => h(App)
      }).$mount(container ? container.querySelector('#app') : '#app')
    }
    
    // // webpack打包公共文件路径
    if ((window as any).__POWERED_BY_QIANKUN__) {
       __webpack_public_path__ = (window as any).__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
    }
    
    // 独立运行
    if (!(window as any).__POWERED_BY_QIANKUN__) {
      render()
    }
    
    // 子组件的协议,必须暴露三个函数
    export async function bootstrap(props: any) {
      console.log('bootstrap函数:', props)
    }
    export async function mount(props: any) {
      console.log('mount函数:', props)
      render(props)
    }
    export async function unmount(props: any) {
      console.log('unmount函数:', props)
      instance.$destroy()
      instance = null
    }
    • 配置文件vue.config.js
    const port = process.env.port || process.env.npm_config_port || 9528 // dev port
    const { name } = require('./package.json');
    
    module.exports = {
        devServer: {
            port,
            headers: {
                'Access-Control-Allow-Origin': '*'
            }
        },
        configureWebpack: {
            output: {
                library: `${name}`,
                libraryTarget: 'umd', // 把微应用打包成 umd 库格式
                jsonpFunction: `webpackJsonp_${name}`,
            }
        },
    }

    react项目(umi项目)

    • 安装 qiankun 插件 @umijs/plugin-qiankun
    yarn add @umijs/plugin-qiankun -D
    • 插件注册(.umirc.ts)
    export default defineConfig({
      ..... // 其他配置代码
      qiankun: {
        slave: {}
      }
    });
    • 配置运行时生命周期钩子(可选)

    插件会自动为你创建好 qiankun 子应用需要的生命周期钩子,但是如果你想在生命周期期间加一些自定义逻辑,可以在子应用的 src/app.ts 里导出 qiankun 对象,并实现每一个生命周期钩子,其中钩子函数的入参 props 由主应用自动注入。

    export const qiankun = {
        // 应用加载之前
        async bootstrap(props) {
          console.log('app1 bootstrap', props);
        },
        // 应用 render 之前触发
        async mount(props) {
          console.log('app1 mount', props);
        },
        // 应用卸载之后触发
        async unmount(props) {
          console.log('app1 unmount', props);
        },
    };

    react项目(ant-design-pro项目)

    • 安装 qiankun 插件 @umijs/plugin-qiankun
    yarn add @umijs/plugin-qiankun -D
    • 当使用ant-design-pro搭建项目时,默认运行会加上以项目名称为命名的根路径(ROUTER_BASE),接入主应用及部署时需要确认子应用单独运行时的根路径名称(ROUTER_BASE)和在主应用里的根路径名称(MICRO_ROUTE_BASE)
    // src/consts.ts
    export const MICRO_ROUTER_BASE = '/sub-app-1/';
    
    export const ROUTER_BASE = '/app-1/';
    • 插件注册(config/config.ts)
    ....
    import { MICRO_ROUTER_BASE, ROUTER_BASE } from '../src/consts';
    
    const { REACT_APP_ENV, UMI_ENV } = process.env;
    
    const isProd = UMI_ENV === 'prod';
    
    const logoPath = isProd ? ROUTER_BASE : '/';
    
    defaultSettings.logo = logoPath + 'logo.svg';
    
    export default defineConfig({
    ...
    manifest: {
        basePath: ROUTER_BASE,
    },
    qiankun: {
        slave: {},
      },
      define: {
        MICRO_ROUTER_BASE,
        ROUTER_BASE,
      },
      ...
    });
    • 全局配置ROUTER_BASE(src/pages/document.ejs)
    ...
    <body>
    <script>
        if (window.__POWERED_BY_QIANKUN__) {
          window.routerBase = "<%= context.config.define.MICRO_ROUTER_BASE %>"
        } else {
          window.routerBase = "<%= context.config.define.ROUTER_BASE %>"
        }
    
      </script>
    ....
    </body>

    主子应用实现通信配置

    主应用umi项目使用@umijs/plugin-qiankun配置

    向umi子应用通信(使用@umijs/plugin-qiankun提供的通信方式)

    • 在主应用app.ts中导出通信方法
    export function useQiankunStateForSlave(): object {
      const { initialState, setInitialState } = useModel('@@initialState');
    
      return {
        initialState,
        setInitialState,
      };
    }
    • 在子应用组件中使用qiankunStateFromMaster获取主应用数据
    import { useModel } from 'umi';
    import styles from './index.less';
    
    function IndexPage() {
      // 获取主应用的登录信息
      const masterProps = useModel('@@qiankunStateFromMaster');
    
      const { initialState } = masterProps;
      return (
        <>
          <h1 className={styles.title}>Page index</h1>
          <div>从主应用获取用户名:{initialState?.currentUser?.name}</div>
          <div>从主应用获取的色值:{initialState?.currentTheme?.theme?.primaryColor}</div>
        </>
      );
    }
    
    export default IndexPage;

    向vue子应用通信(使用qiankun提供的通信方式)

    • 在主应用src目录下新建action.ts文件
    import { initGlobalState, MicroAppStateActions } from 'qiankun';
    
    // 初始化 state
    const actions: MicroAppStateActions = initGlobalState({ color: '#1890ff' });
    
    actions.onGlobalStateChange((state, prev) => {
        console.log(state, prev);
    });
    
    actions.offGlobalStateChange();
    
    export default actions;
    • 在子应用入口文件main.js配置

    主应用传递的数据initialState、监听数据方法onGlobalStateChange、设置数据方法setGlobalState都会以参数的形式由qiankun生命周期mount函数传递给全局的render函数,配合vuex修改全局状态。

    /* eslint-disable */
    import './public-path.js';
    import Vue from 'vue';
    import VueRouter from 'vue-router';
    import App from './App.vue';
    import routes from './router';
    import store from './store';
    
    Vue.config.productionTip = false;
    
    // 声明一个变量,可以用于卸载
    let instance: any = null;
    let router: any = null;
    // 挂载到自己的html中,基座会拿到这个挂载后的html插入进去
    function render(props = {}) {
    
      const container:any = (props as any).container;
      const currentUser = (props as any)?.initialState?.currentUser;
      const currentTheme = (props as any)?.initialState?.currentTheme;
    
      store.dispatch('user/getInfo', {name: currentUser?.name || ''});
    
      store.dispatch('theme/getColor', currentTheme?.theme?.primaryColor || '');
      
      (props as any).onGlobalStateChange((state:any, prev:any) => {
        // state: 变更后的状态; prev 变更前的状态
        console.log(state, prev);
        store.dispatch('theme/getColor', state?.color || '')
      });
    
      router = new VueRouter({
        base: (window as any).__POWERED_BY_QIANKUN__ ? '/sub-vue-app/' : '/',
        mode: 'history',
        routes,
      });
    
      instance = new Vue({
        router,
        store,
        render: h => h(App)
      }).$mount(container ? container.querySelector('#app') : '#app')
    }
    
    // // webpack打包公共文件路径
    // if ((window as any).__POWERED_BY_QIANKUN__) {
    //   __webpack_public_path__ = (window as any).__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
    // }
    
    // 独立运行
    if (!(window as any).__POWERED_BY_QIANKUN__) {
      render()
    }
    
    // 子组件的协议,必须暴露三个函数
    export async function bootstrap(props: any) {
      console.log('bootstrap函数:', props)
    }
    export async function mount(props: any) {
      console.log('mount函数:', props)
      render(props)
    }
    export async function unmount(props: any) {
      console.log('unmount函数:', props)
      instance.$destroy()
      instance = null
    }

    主应用umi项目不使用@umijs/plugin-qiankun配置,直接用qiankun官方文档提供的方式通信

    向umi子应用通信

    • 在主应用src目录下新建action.ts文件
    import { initGlobalState, MicroAppStateActions } from 'qiankun';
    import storage from 'store';
    import { MAIN_APP_THEME } from '@/consts';
    
    // 初始化 state
    const actions: MicroAppStateActions = initGlobalState({ color: '#1890ff' });
    
    store.set(MAIN_APP_THEME, '#1890ff')
    
    actions.onGlobalStateChange((state, prev) => {
        console.log(state, prev);
    });
    
    actions.offGlobalStateChange();
    
    export default actions;
    • 在子应用组件中使用qiankunStateFromMaster获取变化后的主应用变化的数据,用localStorage获取主应用用localStorage存储的初始值
    import { useModel, useEffect } from 'umi';
    import storage from 'store';
    import { MAIN_APP_THEME } from '@/consts';
    import styles from './index.less';
    
    function IndexPage() {
      const { initialState, setInitialState } = useModel('@@initialState');
      // 获取主应用的登录信息
      const masterProps = useModel('@@qiankunStateFromMaster');
    
      useEffect(() => {
        if (masterProps) {
           const storageColor = storage.get(MAIN_APP_THEME);
           setInitialState((s) => ({
            ...s,
            settings: {
              ...((initialState && initialState.settings) || {}),
              currentTheme: {primaryColor: storageColor},
            },
       		 }));
          const { onGlobalStateChange } = masterProps;
          if (onGlobalStateChange) { 
            onGlobalStateChange((state: any) => {
            // state: 变更后的状态; prev 变更前的状态
                  const currentTheme = {primaryColor: state.color};
                  setInitialState((s) => ({
                    ...s,
                    settings: {
                      ...((initialState && initialState.settings) || {}),
                      currentTheme,
                    },
            }));
          }
    		}
        }
      }, [])
      return (
        <div>
          <h1 className={styles.title}>Page index</h1>
          <div>从主应用获取的色值:{initialState?.currentTheme?.primaryColor}</div>
        </div>
      );
    }
    
    export default IndexPage;

    向vue子应用通信(使用qiankun提供的通信方式)

    • 在主应用src目录下新建action.ts文件
    import { initGlobalState, MicroAppStateActions } from 'qiankun';
    import storage from 'store';
    import { MAIN_APP_THEME } from '@/consts';
    
    // 初始化 state
    const actions: MicroAppStateActions = initGlobalState({ color: '#1890ff' });
    
    store.set(MAIN_APP_THEME, '#1890ff')
    
    actions.onGlobalStateChange((state, prev) => {
        console.log(state, prev);
    });
    
    actions.offGlobalStateChange();
    
    export default actions;
    • 主应用传递的监听数据方法onGlobalStateChange、设置数据方法setGlobalState 都会已参数的形式由qiankun生命周期mount函数传递给全局的render函数,配合vuex修改全局状态, 用localStorage获取主应用用localStorage存储的初始值
    /* eslint-disable */
    import './public-path.js';
    import Vue from 'vue';
    import VueRouter from 'vue-router';
    import App from './App.vue';
    import routes from './router';
    import store from './store';
    import { MAIN_APP_THEME } from './consts';
    
    Vue.config.productionTip = false;
    
    // 声明一个变量,可以用于卸载
    let instance: any = null;
    let router: any = null;
    // 挂载到自己的html中,基座会拿到这个挂载后的html插入进去
    function render(props = {}) {
    
      const container:any = (props as any).container;
      // const currentUser = (props as any)?.initialState?.currentUser;
      const currentColor = store.get(MAIN_APP_THEME);
    
      // store.dispatch('user/getInfo', {name: currentUser?.name || ''});
    
      store.dispatch('theme/getColor', currentColor || '');
      
      (props as any).onGlobalStateChange((state:any, prev:any) => {
        // state: 变更后的状态; prev 变更前的状态
        console.log(state, prev);
        store.dispatch('theme/getColor', state?.color || '')
      });
    
      router = new VueRouter({
        base: (window as any).__POWERED_BY_QIANKUN__ ? '/sub-vue-app/' : '/',
        mode: 'history',
        routes,
      });
    
      instance = new Vue({
        router,
        store,
        render: h => h(App)
      }).$mount(container ? container.querySelector('#app') : '#app')
    }
    
    // // webpack打包公共文件路径
    // if ((window as any).__POWERED_BY_QIANKUN__) {
    //   __webpack_public_path__ = (window as any).__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
    // }
    
    // 独立运行
    if (!(window as any).__POWERED_BY_QIANKUN__) {
      render()
    }
    
    // 子组件的协议,必须暴露三个函数
    export async function bootstrap(props: any) {
      console.log('bootstrap函数:', props)
    }
    export async function mount(props: any) {
      console.log('mount函数:', props)
      render(props)
    }
    export async function unmount(props: any) {
      console.log('unmount函数:', props)
      instance.$destroy()
      instance = null
    }

    本文为原创内容,若转载请注明出处,转发感激不尽。

    点击这里复制本文地址 以上内容由jaq123整理呈现,请务必在转载分享时注明本文地址!如对内容有疑问,请联系我们,谢谢!

    苍茫编程网 © All Rights Reserved.  蜀ICP备2024111239号-21