主应用:umi+react

@umi/max 内置了 Qiankun 微前端插件,它可以一键启用 Qiankun 微前端开发模式

配置子应用

// .umirc.ts
export default {
  qiankun: {
    master: {
      apps: [
        {
          name: 'app1',//微应用的名称 通常跟子应用的 base 保持一致
          entry: '//localhost:7001',//微应用的 HTML 地址
        },
      ],
    },
  },
};

引入子应用

在父应用中引入子应用,插件提供了三种不同实现的方式:

  • 路由绑定引入子应用。
  • <MicroApp /> 组件引入子应用。
  • <MicroAppWithMemoHistory /> 组件引入子应用。
路由绑定引入子应用

手动配置 .umirc.ts 文件中的 routes 项,通过路由的方式绑定子应用。何时使用:

  • 子应用包含完整的路由切换逻辑时。
  • 父子应用路由相互关联时。

现在,我们想要在 /app1/project 和 /app2 路由分别加载子应用 app1 和 app2,可以配置父应用的路由如下:

// .umirc.ts
export default {
  routes: [
    {
      path: '/',
      component: '@/layouts/index.tsx',
      routes: [
        {
          path: '/app1',
          component: '@/layouts/app-layout.tsx',
          routes: [
            // 配置微应用 app1 关联的路由
            {
              // 带上 * 通配符意味着将 /app1/project 下所有子路由都关联给微应用 app1
              path: '/project/*',
              microApp: 'app1',
            },
          ],
        },
        // 配置 app2 关联的路由
        {
          path: '/app2/*',
          microApp: 'app2',
        },
      ],
    },
  ],
};

配置好后,子应用的路由 base 会在运行时被设置为主应用中配置的 path

例如,在上面的配置中,我们指定了 app1 关联的 path 为 /app1/project,假如 app1 里有一个路由配置为 /user,当我们想在父应用中访问 /user 对应的页面时,浏览器的 url 需要是 base + /user,即 /app1/project/user 路径,否则子应用会因为无法匹配到正确的路由而渲染空白或 404 页面。

qiankun 插件拓展了 Umi 原有的路由对象,新增了 microApp 字段,它的值为注册子应用的 name。切换到对应路由后,Umi 将会使用 <MicroApp /> 组件渲染此子应用,并替换原来路由的 component

拓展后的 Umi 路由对象 API 可见此

<MicroApp /> 组件引入子应用

通过 <MicroApp /> 组件加载(或卸载)子应用。何时使用:

  • 子应用包含完整的路由切换逻辑时。
  • 父子应用路由相互关联时。

现在,我们想在父应用的某个页面中引入子应用 app1,可以编写代码如下:

import { MicroApp } from 'umi';
export default function Page() {
return ;
}

使用该方式引入子应用时,父子应用的路由将一一对应。例如,当父应用路由为 /some/page 时,子应用路由同样为 /some/page。切换子应用路由时,父应用将同步切换。

如果父应用的路由包含前缀,可以通过配置 base 属性保证父子应用的路由正确对应。例如,父应用路由为 /prefix/router-path/some/page 时,我们希望子应用的路由为 /some/page,可以修改代码如下:

import { MicroApp } from 'umi';
export default function Page() {
return ;
}
<MicroAppWithMemoHistory /> 组件引入子应用

通过 <MicroAppWithMemoHistory /> 组件加载(或卸载)子应用。何时使用:

  • 仅使用子应用的指定路由时。
  • 父子应用路由相互独立时。

<MicroAppWithMemoHistory /> 组件是 <MicroApp /> 组件的变体,您需要显式提供 url 属性作为子应用的路由。当父应用的路由发生变化时,子应用的路由不会改变

现在,我们想在父应用的某个组件内部引入 app2 子应用,子应用的路由为 /some/page,可以编写代码如下:

import { MicroAppWithMemoHistory } from 'umi';
export default function Page() {
return ;
}

子应用 :Vue 3 + Vite


安装qiankun

npm install vite-plugin-qiankun --save-dev
# 或者 yarn add vite-plugin-qiankun -D

配置main

import * as antIcons from '@ant-design/icons-vue';
import Antd, { message } from 'ant-design-vue';
import lodash from 'lodash';
import { createApp } from 'vue';
import JsonViewer from 'vue3-json-viewer';
import { smartSentry } from '/@/lib/smart-sentry';
import { loginApi } from '/@/api/system/login/login-api';
import constantsInfo from '/@/constants/index';
import { privilegeDirective } from '/@/directives/privilege';
import i18n from '/@/i18n/index';
import privilegePlugin from '/@/plugins/privilege-plugin';
import smartEnumPlugin from '/@/plugins/smart-enums-plugin';
import { buildRoutes, changeHome, router } from '/@/router/index';
import { store } from '/@/store/index';
import { useUserStore } from '/@/store/modules/system/user';
import { getTokenFromCookie } from '/@/utils/cookie-util';
import { renderWithQiankun, qiankunWindow } from 'vite-plugin-qiankun/dist/helper';
import App from './App.vue';
import './pablic-path';
import 'vue3-json-viewer/dist/index.css';
import '/@/theme/index.less';
// 应用实例和路由实例
let appInstance = null;
let appRouter = null;
/**
 * 创建并配置 Vue 应用
 */
function createVueApp(props = {}) {
  const container = props.container ? props.container.querySelector('#app') : '#app';
  // 创建 Vue 应用
  const app = createApp(App);
  // 使用插件
  app.use(router).use(store).use(i18n).use(Antd).use(smartEnumPlugin, constantsInfo).use(privilegePlugin).use(JsonViewer);
  // 注册权限指令
  app.directive('privilege', {
    mounted(el, binding) {
      privilegeDirective(el, binding);
    },
  });
  // 注册所有图标组件
  Object.keys(antIcons).forEach((key) => {
    app.component(key, antIcons[key]);
  });
  // 配置全局属性
  app.config.globalProperties.$antIcons = antIcons;
  app.config.globalProperties.$lodash = lodash;
  // 挂载应用
  appInstance = app.mount(container);
  return appInstance;
}
/**
 * 获取用户登录信息并构建动态路由
 */
async function setupUserInfoAndRoutes() {
  const token = getTokenFromCookie();
  if (!token) {
    changeHome();
    return;
  }
  try {
    const res = await loginApi.getLoginInfo();
    const menuRouterList = res.data.menuList.filter((e) => e.path || e.frameUrl);
    // 构建动态路由
    buildRoutes(menuRouterList);
    // 更新用户信息到 store
    useUserStore().setUserLoginInfo(res.data);
  } catch (error) {
    console.error(' 获取用户信息失败:', error);
    message.error(' 获取用户信息失败');
    smartSentry.captureError(error);
  }
}
/**
 * 初始化应用程序
 */
async function initializeApplication(props = {}) {
  try {
    // 先设置用户信息和路由
    await setupUserInfoAndRoutes();
    // 然后创建 Vue 应用
    return createVueApp(props);
  } catch (error) {
    console.error(' 应用初始化失败:', error);
    smartSentry.captureError(error);
    // 即使失败也创建基础应用
    return createVueApp(props);
  }
}
renderWithQiankun({
  /**
   * 微前端生命周期函数 - 启动
   */
  bootstrap() {
    console.log('Micro  app bootstrap');
  },
  /**
   * 微前端生命周期函数 - 挂载
   */
  mount(props) {
    console.log('Micro  app mount');
    initializeApplication(props);
  },
  /**
   * 微前端生命周期函数 - 卸载
   */
  unmount() {
    console.log('Micro  app unmount');
    if (appInstance) {
      // 正确的卸载方式
      appInstance.unmount?.();
      appInstance = null;
    }
    if (appRouter) {
      appRouter = null;
    }
  },
});
/**
 * 独立启动应用程序
 */
async function startApplication() {
  if (!qiankunWindow.__POWERED_BY_QIANKUN__) {
    await initializeApplication();
  }
}
// 启动应用
startApplication();

配置 vite.config.js

import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import qiankun from 'vite-plugin-qiankun';
 const { name } = require('./package.json');///主应用name:app1
export default defineConfig({
  plugins: [
    vue(),
    qiankun(name, { //主应用name
      useDevMode: true // 开发模式也要能上台排练
    })
});

配置router

import { createRouter, createWebHistory } from 'vue-router';
import { routerArray } from './routers';//本地路由
import {  qiankunWindow } from 'vite-plugin-qiankun/dist/helper';
 const { name } = require('./package.json');///主应用name:app1
 export const router = createRouter({
  history: createWebHistory(qiankunWindow.__POWERED_BY_QIANKUN__ ? `/${name}` : ''),
  routes: routerArray,
  strict: true,
  scrollBehavior: () => ({ left: 0, top: 0 }),
});

完整路由配置

import nProgress from 'nprogress';
import { nextTick } from 'vue';
import { createRouter, createWebHistory } from 'vue-router';
import { PAGE_PATH_404, PAGE_PATH_LOGIN, PAGE_PATH_SEARCH } from '/@/constants/common-const';
import { HOME_PAGE_NAME } from '/@/constants/system/home-const';
import SmartLayout from '/@/layout/smart-layout.vue';
import { useUserStore } from '/@/store/modules/system/user';
import { renderWithQiankun, qiankunWindow } from 'vite-plugin-qiankun/dist/helper';
import lodash from 'lodash';
import { routerArray } from './routers';
import { name } from ;
import 'nprogress/nprogress.css';
export const router = createRouter({
  history: createWebHistory(qiankunWindow.__POWERED_BY_QIANKUN__ ? `/${name}` : ''),
  routes: routerArray,
  strict: true,
  scrollBehavior: () => ({ left: 0, top: 0 }),
});
// ----------------------- 路由加载前 -----------------------
router.beforeEach(async (to, from, next) => {
  // 进度条开启
  nProgress.start();
  // 公共页面,任何时候都可以跳转
  if (to.path === PAGE_PATH_404 || to.path === PAGE_PATH_SEARCH) {
    next();
    return;
  }
  // 首页( 需要登录 ,但不需要验证权限)
  if (to.path == HOME_PAGE_NAME) {
    next();
    return;
  }
  // 下载路由对应的 页面组件,并修改组件的Name,如果修改过,则不需要修改
  let toRouterInfo = routerMap.get(to.name);
  if (toRouterInfo && lodash.isFunction(toRouterInfo.component) && toRouterInfo.meta.renameComponentFlag === false) {
    // 因为组件component 为 lazy load是个方法,所以可以直接执行 component()方法
    toRouterInfo.component().then((val) => {
      // 修改组件的name
      val.default.name = to.meta.componentName;
      // 记录已经修改过 组件的name
      toRouterInfo.meta.renameComponentFlag = true;
    });
  }
  // 是否刷新缓存
  // 当前路由是否在tag中 存在tag中且没有传递keepAlive则刷新缓存
  let findTag = (useUserStore().tagNav || []).find((e) => e.menuName == to.name);
  let reloadKeepAlive = findTag && !to.params.keepAlive;
  // 设置tagNav
  useUserStore().setTagNav(to, from);
  // 设置keepAlive 或 删除KeepAlive
  if (to.meta.keepAlive) {
    if (reloadKeepAlive) {
      useUserStore().deleteKeepAliveIncludes(to.meta.componentName);
    }
    nextTick(() => {
      useUserStore().pushKeepAliveIncludes(to.meta.componentName);
    });
  }
  next();
});
// ----------------------- 路由加载后 -----------------------
router.afterEach(() => {
  nProgress.done();
});
// ----------------------- 构建router对象 -----------------------
const routerMap = new Map();
export async function buildRoutes(menuRouterList) {
  const routerList = [];
  routerList.push({
    path: '/',
    name: 'searchHome',
    redirect: '/languge',
  });
  //2、添加到路由里
  await router.addRoute({
    path: '/',
    meta: {},
    component: SmartLayout,
    children: routerList,
  });
}
export function changeHome() {
  //2、添加到路由里
  router.addRoute({
    path: '/',
    name: 'searchHome',
    redirect: '/home',
  });
}