本文档以本项目(
odp-center-vue)和子系统(odp-opcard-vue)的实际配置为基础,详细说明 qiankun 的接入流程及在其他系统中的复用方案。
目录
- 架构概述
- 核心概念
- [子应用接入步骤(以 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")
- 配置详解(逐文件)
- 父子应用通信机制
- [CSS 样式隔离方案](#CSS 样式隔离方案 "#6-css-%E6%A0%B7%E5%BC%8F%E9%9A%94%E7%A6%BB%E6%96%B9%E6%A1%88")
- 在新系统中复用的标准模板
- 常见问题排查
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:子应用独立运行正常,但在主应用中加载空白
检查项:
vite.config.ts中的base是否配置正确(生产环境需要/${APP_NAME})index.html的data-qiankun属性是否与注册的name一致- 主应用注册时的
entry路径和container选择器是否正确
Q2:样式污染(子应用样式影响主应用)
检查项:
postcss-prefix-selector是否正确配置transform函数中是否有遗漏的全局选择器未被过滤- 弹窗类组件的
getContainer/getPopupContainer是否指向子应用容器
Q3:路由跳转后白屏或 404
检查项:
router的base是否在 qiankun 环境下设为/${APP_NAME}- 主应用的
activeRule是否与子应用路由 base 一致 - Nginx/服务器是否将
/${APP_NAME}/*的请求都指向子应用的index.html
Q4:父应用 store 在子应用中访问为空
检查项:
mount钩子中是否正确解构了propscreatePinia()是否在mount内部(每次 mount 都要新建,不能复用)setParentProps是否在app.use(pinia)之后调用
Q5:开发环境跨域报错
检查项:
-
useDevMode: true是否在command === 'serve'时开启 -
Vite devServer 是否配置了 CORS:
tsserver: { 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必须全局唯一,且在主应用注册时的name、vite.config的插件参数、index.html的data-qiankun、路由base四处保持完全一致。