Qiankun 微前端配置详解

37 阅读5分钟

本文档以本项目(odp-center-vue)和子系统(odp-opcard-vue)的实际配置为基础,详细说明 qiankun 的接入流程及在其他系统中的复用方案。


目录

  1. 架构概述
  2. 核心概念
  3. 子应用接入步骤(以 Vite + Vue3 为例)
  4. 配置详解(逐文件)
  5. 父子应用通信机制
  6. CSS 样式隔离方案
  7. 在新系统中复用的标准模板
  8. 常见问题排查

1. 架构概述

本项目的微前端层级关系如下:

主应用(顶层父应用)
    └── odp-center-vue(本项目,作为"中间层"子应用)
            └── odp-opcard-vue(下级子应用,由 odp-center-vue 承载)

重要说明:

  • odp-center-vue 对上层主应用而言是子应用(Slave),对 odp-opcard-vue 而言是父应用(Master)。
  • 每一层都使用相同的技术栈:vite-plugin-qiankun + Vue3。
  • 所有子应用均支持独立运行(无需主应用也可正常启动)。

2. 核心概念

概念说明
vite-plugin-qiankunVite 生态下的 qiankun 适配插件,替代直接安装 qiankun
qiankunWindow插件提供的沙箱化 window 对象,避免全局变量污染
__POWERED_BY_QIANKUN__标识当前是否运行在 qiankun 环境中的全局变量
renderWithQiankun注册子应用生命周期钩子的核心函数
data-qiankunHTML 根容器的标识属性,CSS 隔离的锚点
useDevMode开发模式下允许跨域加载,生产环境必须关闭

3. 子应用接入步骤

Step 1:安装依赖

npm install vite-plugin-qiankun --save-dev
npm install postcss-prefix-selector autoprefixer --save-dev

Step 2:修改 index.html

在根容器上添加 data-qiankun 属性,值为子应用名称:

<!-- 修改前 -->
<div id="app"></div>

<!-- 修改后 -->
<div id="app" data-qiankun="你的子应用名称"></div>

Step 3:配置 vite.config.ts

import { defineConfig } from 'vite'
import qiankun from 'vite-plugin-qiankun'

export default defineConfig(({ command }) => {
  const APP_NAME = 'your-app-name'  // 子应用唯一名称

  return {
    base: command === 'serve' ? '/' : `/${APP_NAME}`,

    plugins: [
      // ... 其他插件
      qiankun(APP_NAME, {
        useDevMode: command === 'serve'  // 开发环境开启,生产关闭
      })
    ],

    css: {
      postcss: {
        plugins: [
          // CSS 隔离(仅在 qiankun 环境下生效)
          // 详见第 6 节
        ]
      }
    }
  }
})

Step 4:修改 src/router/index.ts

import { qiankunWindow } from 'vite-plugin-qiankun/dist/helper'

// 关键:qiankun 环境下 base 必须与 vite 的 build base 一致
const base = qiankunWindow.__POWERED_BY_QIANKUN__ ? '/your-app-name' : '/'

export const router = createRouter({
  history: createWebHistory(base),
  routes
})

Step 5:改造 src/main.ts

这是最核心的一步,将应用挂载逻辑抽取为可复用函数:

import { createApp } from 'vue'
import { createPinia } from 'pinia'
import { renderWithQiankun, qiankunWindow } from 'vite-plugin-qiankun/dist/helper'
import App from './App.vue'
import { router } from './router'

let app: ReturnType<typeof createApp> | null = null

function createMyApp() {
  const instance = createApp(App)
  instance.use(createPinia())
  instance.use(router)
  return instance
}

function setupFn(appInstance: ReturnType<typeof createApp>, container: string | HTMLElement) {
  appInstance.mount(container)
}

if (!qiankunWindow.__POWERED_BY_QIANKUN__) {
  // 独立运行模式
  app = createMyApp()
  setupFn(app, '#app')
} else {
  // qiankun 子应用模式
  renderWithQiankun({
    bootstrap() {
      return Promise.resolve()
    },
    async mount(props) {
      app = createMyApp()

      // 接收父应用传递的数据
      const { parentStore, parentRouter, parentEvents } = props
      app.config.globalProperties.parentStore = parentStore

      // 存储父应用 props 到 store(可选)
      // const commonStore = useCommonStore()
      // commonStore.setParentProps({ parentStore, parentRouter, parentEvents })

      // 挂载到父应用提供的容器中
      setupFn(app, props.container?.querySelector('#app') as HTMLElement)
    },
    update() {},
    unmount() {
      app?.unmount()
      app = null
    }
  })
}

4. 配置详解(逐文件)

4.1 vite.config.ts 完整 qiankun 相关配置

基于 odp-opcard-vue 的实际配置,以下是所有 qiankun 相关配置项的说明:

// 子应用唯一名称(须与父应用注册时的 name 一致)
const APP_NAME = 'odp-opcard-vue'

export default defineConfig(({ command }) => ({
  // 1. base 路径:开发环���为 /,生产环境为 /子应用名
  base: command === 'serve' ? '/' : `/${APP_NAME}`,

  plugins: [
    // 2. qiankun 插件(放在其他插件之后)
    qiankun(APP_NAME, {
      useDevMode: command === 'serve'
    }),

    // 3. 修复 scoped CSS 与 qiankun 前缀冲突的自定义插件(见第 6 节)
    {
      name: 'fix-css-selector-qiankun-global',
      // ...
    }
  ],

  css: {
    postcss: {
      plugins: [
        // 4. CSS 前缀隔离(仅在 qiankun 环境下启用)
        ...(qiankunWindow.__POWERED_BY_QIANKUN__ ? [
          prefixer({
            prefix: `div[data-qiankun="${APP_NAME}"]`,
            transform(prefix, selector, prefixedSelector) {
              // 跳过全局选择器
              if (['#app', 'body', 'html'].includes(selector)) return selector
              return prefixedSelector
            }
          })
        ] : [])
      ]
    }
  }
}))

4.2 index.html

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <title>子应用标题</title>
</head>
<body>
  <!-- data-qiankun 属性是 CSS 隔离的锚点,值必须与 APP_NAME 一致 -->
  <div id="app" data-qiankun="your-app-name"></div>
  <script type="module" src="/src/main.ts"></script>
</body>
</html>

4.3 src/store/modules/common.ts(父应用 props 存储)

import { ref } from 'vue'
import { defineStore } from 'pinia'

export const useCommonStore = defineStore('common', () => {
  const parentProps = ref<Record<string, any>>({})

  const setParentProps = (data: Record<string, any>) => {
    parentProps.value = { ...parentProps.value, ...data }
  }

  return { parentProps, setParentProps }
})

5. 父子应用通信机制

5.1 父应用向子应用传递数据(通过 Props)

父应用在注册子应用时传入数据:

// 父应用侧
registerMicroApps([
  {
    name: 'your-app-name',
    entry: '//localhost:9001',
    container: '#subapp-container',
    activeRule: '/your-app-name',
    props: {
      parentStore: store,       // 父应用的 Pinia store
      parentRouter: router,     // 父应用的路由实例
      parentEvents: eventBus,   // 父子通信事件总线
    }
  }
])

子应用在 mount 钩子中接收:

async mount(props) {
  const { parentStore, parentRouter, parentEvents } = props

  // 方式一:挂载到全局属性(任何组件可通过 getCurrentInstance() 访问)
  app.config.globalProperties.parentStore = parentStore

  // 方式二:存入 Pinia store(推荐,响应式)
  const commonStore = useCommonStore()
  commonStore.setParentProps({ parentStore, parentRouter, parentEvents })
}

5.2 子应用调用父应用方法

// 在子应用的任意组件或 store 中
import { useCommonStore } from '@/store/modules/common'

const commonStore = useCommonStore()

// 调用父应用的退出登录方法
commonStore.parentProps.parentStore.user.dispatchLogOut()

// 使用父应用路由跳转
commonStore.parentProps.parentRouter.push('/other-system')

5.3 弹窗容器挂载适配

在 qiankun 沙箱中,弹窗默认挂载到 document.body 会导致样式隔离失效。需适配挂载点:

// App.vue 或全局配置
const getPopupContainer = (el: HTMLElement) => {
  if (qiankunWindow.__POWERED_BY_QIANKUN__) {
    // 挂载到子应用根容器内,保持样式隔离
    return document.querySelector('#app[data-qiankun="your-app-name"]') || document.body
  }
  return document.body
}

// Ant Design Vue 配置示例
// <a-config-provider :get-popup-container="getPopupContainer">

6. CSS 样式隔离方案

6.1 方案原理

使用 postcss-prefix-selector 为所有 CSS 选择器自动添加 div[data-qiankun="APP_NAME"] 前缀,使样式只作用于子应用根容器内部。

/* 处理前 */
.my-button { color: red; }

/* 处理后 */
div[data-qiankun="your-app-name"] .my-button { color: red; }

6.2 完整配置(包含选择器过滤规则)

import prefixer from 'postcss-prefix-selector'
import autoprefixer from 'autoprefixer'
import { qiankunWindow } from 'vite-plugin-qiankun/dist/helper'

const APP_NAME = 'your-app-name'

// 仅在 qiankun 环境下启用
const postcssPlugins = qiankunWindow.__POWERED_BY_QIANKUN__ ? [
  prefixer({
    prefix: `div[data-qiankun="${APP_NAME}"]`,
    transform(prefix, selector, prefixedSelector, filePath) {
      // 1. 跳过全局根选择器(避免破坏布局)
      if ([
        '#app', 'body', 'html', ':root',
        '.menu', '.ant-scrolling-effect'
      ].some(s => selector.startsWith(s))) {
        return selector
      }

      // 2. 非 Vue 组件文件中的 Ant Design 原生样式不加前缀
      // (避免与全局 antd 样式冲突)
      if (!filePath.includes('src/') && selector.includes('.ant-')) {
        return selector
      }

      // 3. Vue 组件和业务代码中的样式添加前缀
      return prefixedSelector
    }
  }),
  autoprefixer({})
] : []

6.3 修复 Scoped CSS 与 qiankun 前缀冲突

Vue 的 scoped 样式会生成如 .my-class[data-v-xxxxxx] 的选择器,与 qiankun 前缀叠加后可能出现格式错误。需要自定义 Vite 插件修复:

// vite.config.ts plugins 中添加
{
  name: 'fix-css-selector-qiankun-global',
  // 处理开发环境中的 transform
  transform(code, id) {
    if (!id.includes('.vue')) return code
    // 修复形如:div[data-qiankun="xxx"].foo[data-v-yyy]
    // 变为:div[data-qiankun="xxx"] .foo[data-v-yyy]
    return code.replace(
      /div\[data-qiankun="[^"]+"\](\.[^{,\s]+\[data-v-)/g,
      (match, p1) => match.replace(p1, ` ${p1}`)
    )
  },
  // 处理构建产物中的 CSS 文件
  generateBundle(options, bundle) {
    for (const [fileName, chunk] of Object.entries(bundle)) {
      if (fileName.endsWith('.css') && chunk.type === 'asset') {
        chunk.source = (chunk.source as string).replace(
          /div\[data-qiankun="[^"]+"\](\.[^{,\s]+\[data-v-)/g,
          (match, p1) => match.replace(p1, ` ${p1}`)
        )
      }
    }
  }
}

7. 在新系统中复用的标准模板

7.1 复用清单(新接入子应用时逐项检查)

#文件修改内容关键值
1package.json添加依赖vite-plugin-qiankun, postcss-prefix-selector
2index.html根容器加属性data-qiankun="APP_NAME"
3vite.config.ts注册插件,配置 base 和 CSSAPP_NAME, useDevMode
4src/router/index.ts动态设置 baseqiankunWindow.__POWERED_BY_QIANKUN__
5src/main.ts注册生命周期钩子renderWithQiankun, 四个生命周期
6src/store/modules/common.ts存储父应用 propssetParentProps
7src/App.vue弹窗容器适配getPopupContainer

7.2 main.ts 复用模板(直接复制,替换 APP_NAME)

import { createApp } from 'vue'
import { createPinia } from 'pinia'
import { renderWithQiankun, qiankunWindow } from 'vite-plugin-qiankun/dist/helper'
import App from './App.vue'
import { router } from './router'
import { useCommonStore } from './store/modules/common'

// ======== 修改这里 ========
const APP_NAME = 'your-app-name'
// ==========================

let app: ReturnType<typeof createApp> | null = null

function createMyApp() {
  const instance = createApp(App)
  const pinia = createPinia()
  instance.use(pinia)
  instance.use(router)
  return instance
}

function setupFn(
  appInstance: ReturnType<typeof createApp>,
  container: string | HTMLElement
) {
  appInstance.mount(container)
}

if (!qiankunWindow.__POWERED_BY_QIANKUN__) {
  app = createMyApp()
  setupFn(app, '#app')
} else {
  renderWithQiankun({
    bootstrap() {
      return Promise.resolve()
    },
    async mount(props) {
      app = createMyApp()
      const { parentStore, parentRouter, parentEvents } = props

      app.config.globalProperties.parentStore = parentStore

      const commonStore = useCommonStore()
      commonStore.setParentProps({ parentStore, parentRouter, parentEvents })

      setupFn(app, props.container?.querySelector('#app') as HTMLElement)
    },
    update() {},
    unmount() {
      app?.unmount()
      app = null
    }
  })
}

7.3 router/index.ts 复用模板

import { createRouter, createWebHistory } from 'vue-router'
import { qiankunWindow } from 'vite-plugin-qiankun/dist/helper'

// ======== 修改这里 ========
const APP_NAME = 'your-app-name'
// ==========================

const routes = [
  // 你的路由配置...
]

const base = qiankunWindow.__POWERED_BY_QIANKUN__ ? `/${APP_NAME}` : '/'

export const router = createRouter({
  history: createWebHistory(base),
  routes
})

7.4 vite.config.ts 关键片段复用模板

import qiankun from 'vite-plugin-qiankun'
import prefixer from 'postcss-prefix-selector'
import autoprefixer from 'autoprefixer'
import { qiankunWindow } from 'vite-plugin-qiankun/dist/helper'

// ======== 修改这里 ========
const APP_NAME = 'your-app-name'
// ==========================

export default defineConfig(({ command }) => ({
  base: command === 'serve' ? '/' : `/${APP_NAME}`,

  plugins: [
    vue(),
    qiankun(APP_NAME, { useDevMode: command === 'serve' }),
    // CSS 选择器修复插件(直接从本项目复制)
  ],

  css: {
    postcss: {
      plugins: [
        ...(qiankunWindow.__POWERED_BY_QIANKUN__ ? [
          prefixer({
            prefix: `div[data-qiankun="${APP_NAME}"]`,
            transform(prefix, selector, prefixedSelector, filePath) {
              if (['#app', 'body', 'html', ':root'].some(s => selector.startsWith(s))) {
                return selector
              }
              return prefixedSelector
            }
          }),
          autoprefixer({})
        ] : [])
      ]
    }
  }
}))

8. 常见问题排查

Q1:子应用独立运行正常,但在主应用中加载空白

检查项:

  1. vite.config.ts 中的 base 是否配置正确(生产环境需要 /${APP_NAME}
  2. index.htmldata-qiankun 属性是否与注册的 name 一致
  3. 主应用注册时的 entry 路径和 container 选择器是否正确

Q2:样式污染(子应用样式影响主应用)

检查项:

  1. postcss-prefix-selector 是否正确配置
  2. transform 函数中是否有遗漏的全局选择器未被过滤
  3. 弹窗类组件的 getContainer/getPopupContainer 是否指向子应用容器

Q3:路由跳转后白屏或 404

检查项:

  1. routerbase 是否在 qiankun 环境下设为 /${APP_NAME}
  2. 主应用的 activeRule 是否与子应用路由 base 一致
  3. Nginx/服务器是否将 /${APP_NAME}/* 的请求都指向子应用的 index.html

Q4:父应用 store 在子应用中访问为空

检查项:

  1. mount 钩子中是否正确解构了 props
  2. createPinia() 是否在 mount 内部(每次 mount 都要新建,不能复用)
  3. setParentProps 是否在 app.use(pinia) 之后调用

Q5:开发环境跨域报错

检查项:

  1. useDevMode: true 是否在 command === 'serve' 时开启
  2. Vite devServer 是否配置了 CORS:
    server: {
      cors: true,
      headers: { 'Access-Control-Allow-Origin': '*' }
    }
    

附录:本项目实际使用的子应用名称

系统APP_NAME开发端口生产 base
odp-center-vueodp-center-vue9001/odp-center-vue
odp-opcard-vueodp-opcard-vue(查看其 vite.config)/odp-opcard-vue

注意:每个子应用的 APP_NAME 必须全局唯一,且在主应用注册时的 namevite.config 的插件参数、index.htmldata-qiankun、路由 base 四处保持完全一致。