微前端 架构搭建 (基座:umi ,子项目:vue||react 辅助:qiankun)

2,660 阅读4分钟

应项目要求,需要将一个旧的vue 项目集成到新的react 项目中,但是同时会对vue 项目(只)进行维护。所以采用了现有的这一套技术方案来实现这一功能。

主要是使用qiankun,基座umi.js。子项目可以使用vue 以及react。目前项目已经上线。

主应用搭建(基座)

1.1 使用umi.js 脚手架创建项目

yarn create umi main-app

ant-design-pro
pro v4
ts
simple
antd@4


项目结构 image.png

1.2 基座应用umi.js。可以安装qiankun 插件。所以在基座项目中执行命令

yarn add @umijs/plugin-qiankun -D

1.3 基座项目动态注册子应用和子路由

将子项目注册到基座中,我们首先需要配置基座项目的config。这里是将子项目静态直接注入,后面例子会给到动态注入

config/config.ts


export default defineConfig({
    ...
    
     qiankun: {
        master: {
          apps: [
              {
                  name: 'sub-vue',
                  entry: '//localhost:8001',
               },
          ],
        },
      },

    ...

})

将子项目路由,配置到config/routes.ts (脚手架会创建两个页面)例子路由下面

子页面路由需要要求我们的microApp 的值必须和上面注册apps 的name 一样。同样的,路由的path 字段最好也和microApp值一样。不一样的话,不会报错,但是qiankun会给出 warning 提示

{
  name: 'sub-vue',
  icon: 'smile',
  path: '/sub-vue',
  microApp: 'sub-vue',
},

然而项目通常都是根据当前登录人。动态的通过接口返回子项目,以及子项目的部分路由。所以我们这里采用了动态注入apps 和routes 。所以可以把上面的两部分修改下,config/routes.ts下修改的可以全都撤销。config/config.ts 文件中只需要把apps 的值清空就可以了。

config/config.ts


export default defineConfig({
    ...
    
     qiankun: {
        master: {
          apps: [],
        },
      },

    ...

})

我们会请求接口,返回apps字段和 接下来就是在基座项目中动态注入apps 和routes 的方式。这里的路由并不是真正的子项目业务路由,而是子项目的的入口路由。所以格式化一下apps 的结构就能拿到,清洗格式就是下面app.tsx的代码

let routeArr  = apps.map((item:any)=>{
            return{
                microApp:item?.name,
                path:`/${item?.name}`,
                exact:true
            }
        });

子项目 入口 路由格式

{
  path: '/sub-vue',
  microApp: 'sub-vue',
},

子项目 业务 路由格式

{
  name: 'sub-vue1',   // 菜单栏展示名称
  icon: 'smile',      // 菜单栏展示图标
  path: '/sub-vue/vue1',   // 子项目业务路由(/vue1) 如果这里的路由前缀不是和microApp 一样,需要添加microApp字段
  microApp: 'sub-vue',
},

src/app.tsx 如果这个文件没有,就新建一个文件

import { dynamic } from 'umi';
import LoadingComponent from '@/components/PageLoading';
import { getToken } from '@/utils/auth';
import type {
    BasicLayoutProps as RouteProps,
  } from '@ant-design/pro-layout';
let extraRoutes: object[] = [];

let API = '/api';
const url = API+'/user/menus';

const fetchUrl = fetch(url,{
    headers:{
        'Authorization':getToken()
    }
}).then((res) => {
        return res.json();
})


const formatRoute = function (routes:RouteProps['route'][],type:string) {
    let route:RouteProps['route'];
    let arr:any;
    arr= routes?.map(item => {
        if (Array.isArray(item?.routes)) {
            route = formatRoute(item?.routes||[],type);
        }
        if(type==='app'){
            return {
                microApp: item?.microApp,
                path: item?.path,
            }
        }else if(type==='route'){
            return {
                name: item?.name,
                icon: 'table',
                exact: true,
                path: item?.path,
                routes:route,
                component: dynamic({
                    loader: () =>
                        import(/* webpackChunkName: 'layouts__MicroAppLayout' */ '@/layouts/MicroAppLayout'),
                    loading: LoadingComponent,
                }),
            }
        }
    })
    return arr;
}
 
export const qiankun = fetchUrl
   .then((res ) => {
        const  {data:{menus:{apps=[],routes=[]}}} = res;
        // 格式化路由结构,子项目入口路由是通过apps的值map 出来的
        let routeArr  = apps.map((item:any)=>{
            return{
                microApp:item?.name,
                path:`/${item?.name}`,
                exact:true
            }
        });
        // 此处是通过接口将apps 和格式化的子项目入口路由动态的加载到基座项目中
        return Promise.resolve({
            apps,
            routes:routeArr,
            lifeCycles: {
                afterMount: (props: any) => {
                },
            },
        });
    }).catch(()=>{
        return Promise.resolve({
            apps:[],
            routes:[]
        })
    });

export function patchRoutes({ routes }:any) {
    let routeArr = formatRoute(extraRoutes||[],'route');
    routes[0]?.routes[1].routes[0].routes.push(...routeArr);
}

export async function render(oldRender: any) {
    fetchUrl.then((resJson) => {
            extraRoutes = resJson?.data?.menus?.routes;
            // extraRoutes =  resJson.routes;
            oldRender();
        }).catch(()=>{
            return Promise.resolve({
                apps:[],
                routes:[]
            })
        });
}

patchRoutes这里需要注意一点,可以结合自己的业务将入参routes 追加到 自己的路由中 routes[0]?.routes[1].routes[0].routes.push(...routeArr);此路由是antd 中的路由嵌套。 还有一点需要注意的是,这个app.tsx 文件,会在项目启动的时候就加载。这个时候还没有登录,接口相当于没有token。所以想了一个这种的办法,就是当login 成功以后,路由不通过push去加载,而是通过location.href去加载 ,相当做了一次刷新,同样的这个接口会请求两次。这样暂时把这个问题解决了。如果有更好的方法欢迎提出来。 贴一下接口结构

image.png

子应用搭建

注意创建子项目的时候,所有子项目的package.json 中的name 属性要与刚才基座项目的microApp 的值对应。

2.1 创建 改造 子项目(react)。

yarn create @umijs/umi-app sub-react
cd sub-react
yarn add @umijs/plugin-qiankun -D

下载 并加载 插件 在 .umirc.ts 文件中添加以下代码:

    qiankun: {
      slave: {}
    }

同样的在 src/app.ts 没有则新建

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);
    },
};

image.png

2.2 创建子项目 (vue)

注意使用vue-cli 创建vue 项目的时候,需要注意vue-cli 的版本,这次项目构建使用的版本是3.12.1。版本再高一点,会使用createApp 去构建。版本低一点,没有vue.config.js。所以使用这个版本。

vue init webpack sub-vue

一步一步改造

  1. src/router/index.js 这里是直接将VueRouter 向外报露出去,如果是使用了多个 <route-view>,则需要对路由进行改造
import Vue from 'vue'
import VueRouter  from 'vue-router'
import HelloWorld from '@/components/HelloWorld'
import Vue1 from '@/views/view1'
import Vue2 from '@/views/view2'
Vue.use(VueRouter)
import './public-path';

const baseUrl = window.__POWERED_BY_QIANKUN__?'/sub-vue':'';

export default new VueRouter({
  mode:'history',
  routes: [
    {
      path: baseUrl+'/',
      name: 'HelloWorld',
      component: HelloWorld
    },
    {
      path: baseUrl+'/vue1',
      name: 'Vue1',
      component: Vue1
    },
    {
      path: baseUrl+'/vue2',
      name: 'Vue2',
      component: Vue2
    },
  ]
})

  1. src/main.js 这里是将route 整合到app中。
import Vue from 'vue'
import App from './App.vue';
import router from './router';
import './public-path';

Vue.config.productionTip = false;

let instance = null;
function render(props={}) {
  const { container } = props;
  instance = new Vue({
    router,
    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);
  // storeTest(props);
  render(props);
}
export async function unmount() {
  instance.$destroy();
  instance.$el.innerHTML = '';
  instance = null;
}

同样的需要在 main.js 同级创建public-path.js 文件,文件内容

if (window.__POWERED_BY_QIANKUN__) {
    // eslint-disable-next-line no-undef
    __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__
  }
  
  1. 修改vue.config.js
...
devServer: {
       ...
        // 配置跨域请求头,解决开发环境的跨域问题
        headers: {
            "Access-Control-Allow-Origin": "*",
        },
        ...
}
...
 configureWebpack: {
       ...
        output: {
            // 微应用的包名,这里与主应用中注册的微应用名称一致
            library: name,
            // 将你的 library 暴露为所有的模块定义下都可运行的方式
            libraryTarget: "umd",
            // 按需加载相关,设置为 webpackJsonp_VueMicroApp 即可
            jsonpFunction: `webpackJsonp_${name}`,
        ...
        },
}
    ...

2.3 在react 子项目中,页面的路由不需要添加sub-react前缀。也就是说当我们在基座项目中访问 /sub-react/first 的时候,first 是react 项目的路由。但是vue 稍有不同。vue 需要通过当前是否是在微前端中,动态的拼接当前路由。这一点如果有更好的处理方式烦请告知。

以上已经经过实测,项目是公司项目,git地址无法提供。 如有遗漏或者运行不正确可以添加联系方式 18501719005