Qiankun 微前端配置详解

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


目录

  1. 架构概述
  2. 核心概念
  3. [子应用接入步骤(以 Vite + Vue3 为例)](#子应用接入步骤(以 Vite + Vue3 为例) "#3-%E5%AD%90%E5%BA%94%E7%94%A8%E6%8E%A5%E5%85%A5%E6%AD%A5%E9%AA%A4")
  4. 配置详解(逐文件)
  5. 父子应用通信机制
  6. [CSS 样式隔离方案](#CSS 样式隔离方案 "#6-css-%E6%A0%B7%E5%BC%8F%E9%9A%94%E7%A6%BB%E6%96%B9%E6%A1%88")
  7. 在新系统中复用的标准模板
  8. 常见问题排查

1. 架构概述

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

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

重要说明:

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

2. 核心概念

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

3. 子应用接入步骤

Step 1:安装依赖

bash 复制代码
npm install vite-plugin-qiankun --save-dev
npm install postcss-prefix-selector autoprefixer --save-dev

Step 2:修改 index.html

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

html 复制代码
<!-- 修改前 -->
<div id="app"></div>

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

Step 3:配置 vite.config.ts

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

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

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

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 相关配置项的说明:

ts 复制代码
// 子应用唯一名称(须与父应用注册时的 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

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 存储)

ts 复制代码
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)

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

ts 复制代码
// 父应用侧
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 钩子中接收:

ts 复制代码
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 子应用调用父应用方法

ts 复制代码
// 在子应用的任意组件或 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 会导致样式隔离失效。需适配挂载点:

ts 复制代码
// 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"] 前缀,使样式只作用于子应用根容器内部。

css 复制代码
/* 处理前 */
.my-button { color: red; }

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

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

ts 复制代码
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 插件修复:

ts 复制代码
// 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 复用清单(新接入子应用时逐项检查)

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

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

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'
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 复用模板

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 关键片段复用模板

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:

    ts 复制代码
    server: {
      cors: true,
      headers: { 'Access-Control-Allow-Origin': '*' }
    }

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

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

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

相关推荐
英俊潇洒美少年2 小时前
Vue3 的 JSX 函数组件,每次更新都会重新运行吗?
前端·javascript·vue.js
木斯佳2 小时前
前端八股文面经大全:腾讯前端暑期AI面(2026-03-26)·面经深度解析
前端·人工智能·ai·智能体·暑期实习
invicinble2 小时前
对于一个基本的前端后台管理框架的分析和认识
前端
恋猫de小郭2 小时前
Android 17 新适配要求,各大权限进一步收紧,适配难度提升
android·前端·flutter
高桥凉介发量惊人2 小时前
UI 与交互篇 (3/6):动画体系:隐式动画到自定义动画
前端
cyforkk2 小时前
前端架构实战:当服务器关闭时,如何优雅提示 502 错误?
服务器·前端·架构
高桥凉介发量惊人2 小时前
UI 与交互篇(1/6):组件化思路:从页面复制到可复用组件
前端
kyriewen2 小时前
Generator 函数:那个能“暂停”的函数,到底有什么用?
前端·javascript·面试
打酱油的D2 小时前
01. Node.js 运行时
前端·后端