[实战] umi3.0 + qiankun 接入 vite vue3 子应用方法

1,265 阅读7分钟

最近我们公司承接了一个新的项目,该系统集成的任务是将其无缝嵌入到之前开发好的主应用中。经过一周的技术探索以后,我成功填满qiankun以及vben admin下的坑。现在我将这些经验记录下来,希望为团队和社区成员提供参考和帮助。

我们的技术栈包括:

主应用子应用
umi + antd-V4 + react-V18vben admin + antdv-V4 + vite

主应用配置

config下新建qiankun属性

  qiankun: {
    master: {
      apps: [
        {
          name: 'ziemsb20',
          entry: '//localhost:5174/',
        },
        {
          name: 'zioms',
          entry: '//localhost:8003/',
          prefetch: true,
        },

      ],
    },
  },

在路由中配置微前端与路由的对应关系

    {
      name: 'ziemsb20',
      clientId: 'ziemsb20_console',
      path: '/setting/ziemsb20',
      microApp: 'ziemsb20',
      microAppProps: {
        autoSetLoading: true,
      },
      group: '工具',
    },

另外如果同时启动主应用和子应用需要配置子应用的接口代理

    '/api': {
      target: 'http://10.32.28.117:1889/',
      changeOrigin: true,
    },

至此,主应用的框架搭建已经顺利完成。其实主应用的职责在于协调各个路由与地址的映射关系。后面的登录验证可以根据项目实际情况进行调整。本文重点介绍qiankun的引入,这部分内容就不细说了。

子应用配置

子应用采用vben admin框架,vite作为打包工具。

  1. 引入qiankun处理包 pnpm install --save-dev vite-plugin-qiankun

vben admin 这个框架对组件封装的比较多,我是在vite-config中直接添加qiankun包的配置。在vite.config.ts 中写也是可以的。

image.png 2. 修改main.ts文件。其实这个原理很简单,就是函数通过参数来加载app。参数为空时子应用是单独运行直接mount app,有值时为微前端模式,加载传递的值。我这边的处理是直接传值,传递什么值就加载什么值。因为我调用的时候就已经确定了当前处于那种模式。下面是全部的代码,vben下可以直接替换使用:

import 'uno.css'
import '@/design/index.less'
import '@/components/VxeTable/src/css/index.scss'
import 'ant-design-vue/dist/reset.css'
import 'vant/lib/index.css'
import './global.css'
import './css/variable.scss'
import './css/common.scss'
// Register icon sprite
import 'virtual:svg-icons-register'
import { createApp } from 'vue'
import { registerGlobComp } from '@/components/registerGlobComp'
import { setupGlobDirectives } from '@/directives'
import { setupI18n } from '@/locales/setupI18n'
import { setupErrorHandle } from '@/logics/error-handle'
import { initAppConfigStore } from '@/logics/initAppConfig'
import { router, setupRouter } from '@/router'
import { setupRouterGuard } from '@/router/guard'
import { setupStore } from '@/store'
import ant from 'ant-design-vue'
import { renderWithQiankun, qiankunWindow } from 'vite-plugin-qiankun/dist/helper'

import App from './App.vue'

let app: any = null

async function bootstrapVue3(container) {
  app = createApp(App)
  app.use(ant)
  // Configure store
  // 配置 store
  setupStore(app)
  
  // Initialize internal system configuration
  // 初始化内部系统配置
  initAppConfigStore()

  // Register global components
  // 注册全局组件
  registerGlobComp(app)

  // Multilingual configuration
  // 多语言配置
  // Asynchronous case: language files may be obtained from the server side
  // 异步案例:语言文件可能从服务器端获取
  await setupI18n(app)

  // Configure routing
  // 配置路由
  setupRouter(app)

  // router-guard
  // 路由守卫
  setupRouterGuard(router)

  // Register global directive
  // 注册全局指令
  setupGlobDirectives(app)

  // Configure global error handling
  // 配置全局错误处理
  setupErrorHandle(app)

  // https://next.router.vuejs.org/api/#isready
  // await router.isReady();

  app.mount(container)
}

// bootstrapVue3()

const initQianKun = () => {
  console.log('处于子应用模式...')

  renderWithQiankun({
    // 当前应用在主应用中的生命周期
    // 文档 https://qiankun.umijs.org/zh/guide/getting-started#
    mount(props) {
      console.log('b20 ziems 子应用 收到的 props:', props)
      bootstrapVue3(props.container?.querySelector('#app'))
      //  可以通过props读取主应用的参数:msg
      // 监听主应用传值以及我本身的一些业务逻辑
      props.setLoading(false)
      props.onGlobalStateChange((res) => {
        console.log('res:', res)
      })
    },
    bootstrap() {
      console.log('bootstrap')
      // 做一些一次性的初始化的工作,因为只触发一次
    },
    update() {
      console.log('update')
    },
    unmount(props) {
      // 卸载子应用
      app.unmount()
      app = null
    },
  })
}

// 判断当前应用是否在主应用中
qiankunWindow.__POWERED_BY_QIANKUN__ ? initQianKun() : bootstrapVue3(document.getElementById('app'))

官网和网上有些教程说要写一个publicPath文件,引入到main.ts。实际用下来,不能说没有用,只能说那是完全没卵用,qiankunWindow这个变量是可以判断是否在微前端模式下。

做完上述步骤后,子应用确实可以加载。但是/login 跑到前面去了。等关闭main.ts 中的路由守卫时,就变成下面这种图了 image.png

image.png

出现这个问题一定要检查路由配置。

出现这个问题一定要检查路由配置。

出现这个问题一定要检查路由配置。

image.png

image.png

如果你项目中允许出现路由前缀,可以在createRouter直接传递常量, 这样就不需要判断

export const router = createRouter({
  history: createWebHistory(import.meta.env.VITE_PUBLIC_PATH), // !!!这句话很重要,微前端下需要配置路由前缀
  // 应该添加到路由的初始路由列表。
  routes: basicRoutes as unknown as RouteRecordRaw[],
  // 是否应该禁止尾部斜杠。默认为假
  strict: true,
  scrollBehavior: () => ({ left: 0, top: 0 }),
})

恭喜你,到这里你的子系统应该可以正常加载了。后面记录下加载以后遇到的问题。

  1. 图片加载失败

系统中需要引入本地图时,会出现由于路由原因加载失败的情况。这是因为把子应用放入到基座后,静态资源会默认走主应用地址去访问,但是主应用又没有这些静态资源文件,其结果显而易见,肯定是404了。不管是配置webpack的publicPath还是vite中的base, 都无法解决问题。qiankun是通过import-html-entry注入方式实现子系统的引入。然后通过eval方法执行生成的js脚本。如果系统中有通过import或者require方式引入的资源,插件本身是解决不了的。我在vite 官网上 找到了一个解决方法。其实就是通过new URL方法将本地资源生成一个url,进而加载该url来显示图片。

将原来的import dashbaord from 'assets/svg/menu/数据看板.svg' 变成 const dashboard = new URL('../assets/svg/menu/数据看板.svg', import.meta.url).href

image.png

  1. css 中 背景图片url() 失效,显示图片无法加载

这篇文章说的很清楚,大家感兴趣可以去瞧瞧。其实问题也是出在import-html-entry库。这个库会请求子应用的css文件,并将请求到的文件内容用嵌到子应用的标签中。这就导致了子应用的外联样式变成了内联样式,url()中的相对路径从原来的相对css文件地址变成了相对html文档地址了。有人提出了解决方案并提了pr给umi,结果umi觉得是库的问题直接给拒了。有人说是改源码,有人说是在打包工具上做个配置。

我的方法就比较简单粗暴。上面通过new URL生成了url地址不是可以正常访问嘛?我可以在js中定义一个变量,并想办法把这个变量手动赋值到dom的background-image属性上不就可以了么?

less语法虽然不可以直接处理new URL(),但是可以进行变量赋值啊。话不多说,直接上代码:

const createImageUrl = () => {
  const imageUrl = new URL('@/assets/svg/table-icon.svg', import.meta.url).href;
  document.documentElement.style.setProperty('--pseudo-bg-image', `url(${imageUrl})`);
};

onMounted(() => {
  createImageUrl();
});

&::before {
    content: '';
    display: block;
    width: 16px;
    height: 16px;
    margin-right: 8px;
    background-image: var(--pseudo-bg-image);;
    // background-image: url('@/assets/svg/table-icon.svg');
    background-size: 100% 100%;
}

需要说明的是上面这句话 document.documentElement.style.setProperty('--pseudo-bg-image', url(${imageUrl}));

这行代码的作用是将名为--pseudo-bg-image的CSS变量的值设置为由imageUrl变量提供的URL。这个变量可以在CSS或Less中通过var(--pseudo-bg-image)来引用。

例如,如果你的imageUrl变量包含值'http://example.com/image.png',那么执行这行代码后,CSS变量--pseudo-bg-image的值将被设置为'url("http://example.com/image.png")'。然后你可以在样式中这样使用这个变量

  1. 填补最后一坑,部署

首先说明下我的主应用与子应用路由定义:

主应用路由地址 http://127.0.0.1/html/zifoms其中 /html/zifoms是我的路由前缀。想要通过路由 /setting/ziems/ 定位到vben admin子应用中去。这个时候主应用的路由地址为 http://127.0.0.1/html/zifoms/setting/ziems

子应用路由前缀为/html/ziems。由于主,子两个应用部署在同一个端口下,子应用访问地址为http://127.0.0.1/html/ziems

image.png

vben admin 环境变量有个参数VITE_PUBLIC_PATH。他会自动将这个变量同时设置成路由前缀和目录地址。现实的问题是当我设置成/html/zimes时,子路由会劫持主应用路由。而设置成/html/zifoms/setting/ziems时,页面能正常现实,但当我刷新页面时,子应用会脱离qiankun框架,直接现实子应用内容。

最后的实现方式是目录依然保持/html/ziems 在路由定义时做个判断,在微前端下设置成/html/zifoms/setting/ziems, 正常情况依然保持/html/ziems配置。如下图

image.png

最后附上nginx 配置

image.png