咱们先聊点实际的。你是否经历过这种“窒息感”:项目大了之后,改一行代码要构建半小时,发个版需要协调前端、后端、测试、运维四个团队开三天会,稍微动一下公共组件,整个系统跟着抖三抖。这就是单体应用(Monolith)在规模扩张后的典型痛点。
微前端(Micro-Frontends)这几年被炒得很热,但很多文章只讲概念,一到落地就卡壳。今天我不讲虚的,直接带你深入实战,把独立开发部署、样式隔离和跨应用通信这三个最核心的硬骨头啃下来。我会用 Vue 3 + Vite + qiankun 作为主案例,因为这是目前国内企业落地最成熟的组合,但背后的思想完全通用。
为什么我们迫切需要微前端?
在动手之前,你得明白“为什么”。不是为了追赶潮流,而是为了解决组织架构问题。
早期的单体应用,技术栈是固定的(比如全员 Vue 2),所有开发者共用一个 Git 仓库。随着业务增长,团队分化:A 团队负责订单,B 团队负责用户中心,C 团队负责支付。如果还在一个仓库里混着写,必然会出现以下混乱:
- 部署耦合:A 团队只想发个按钮颜色的修改,却要把整个庞大的主应用重新打包,耗时且风险高。
- 技术栈锁定:C 团队想用最新的 React 18 特性,但主应用是 Vue 2,强行升级主应用会引发全链路回归测试,没人敢动。
- 代码依赖地狱:A 团队依赖了某个版本的 lodash,B 团队依赖了另一个版本,安装时直接冲突。
微前端的核心理念是:将单体应用拆分为一组小型应用,每个应用可以独立开发、独立部署、独立运行,最终组合成一个整体。 它不是要消灭单体,而是在大型复杂系统中引入“可控的分裂”。
架构选型:为什么是 qiankun?
市面上微前端框架不少,有 single-spa、Module Federation(Webpack 5)、Ice.js 等。对于大多数国内企业,我推荐 qiankun。理由很实在:
- 基于 single-spa,稳定性经过阿里内部大规模验证。
- 开箱即用,不需要复杂的 Webpack 配置,对 Vue、React、Angular 都有良好支持。
- 样式隔离机制完善,天然支持 JS 沙箱和 CSS 沙箱。
- 社区活跃,遇到问题容易找到解决方案。
当然,如果你已经是 React 重度用户且使用 Webpack 5,Module Federation 也是一个极好的选择,因为它实现了真正的模块共享,不需要重复打包第三方库。但在本指南中,为了照顾更广泛的兼容性,我们以 qiankun 为例展开。
第一部分:搭建骨架——主应用与子应用的协作
微前端架构中,有两个核心角色:主应用(Base App) 和 子应用(Sub App)。
- 主应用:负责路由分发、布局框架、全局状态管理,以及子应用的加载和卸载。
- 子应用:独立的业务模块,可以是 Vue、React 甚至 jQuery 应用,它们拥有自己的生命周期。
1.1 主应用的核心配置
假设我们有一个主应用 micro-base,使用 Vue 3 开发。我们需要引入 qiankun 并注册子应用。
// main.js (主应用入口)
import { createApp } from 'vue'
import { registerMicroApps, start, initGlobalState } from 'qiankun'
import App from './App.vue'
const app = createApp(App)
// 1. 定义全局状态(用于后续通信)
const initialState = {
userInfo: null,
theme: 'light'
}
const { onGlobalStateChange, setGlobalState } = initGlobalState(initialState)
// 实时监听状态变化
onGlobalStateChange((value, prev) => {
console.log('[Global State] changed from', prev, 'to', value)
// 可以在这里更新 UI 或触发其他逻辑
})
// 2. 注册子应用
registerMicroApps([
{
name: 'app-react', // 唯一标识
entry: '//localhost:7100', // 子应用地址,生产环境替换为实际域名
container: '#react-container', // 挂载点
activeRule: '/react', // 路由匹配规则
},
{
name: 'app-vue',
entry: '//localhost:7200',
container: '#vue-container',
activeRule: '/vue',
},
])
// 3. 启动 qiankun
start({
sandbox: {
strictStyleIsolation: true, // 启用严格的样式隔离(见后文详解)
},
})
app.mount('#app')
在 App.vue 中,你需要预留挂载点:
<template>
<div id="app">
<header>
<nav>
<router-link to="/">首页</router-link>
<router-link to="/react">React 订单中心</router-link>
<router-link to="/vue">Vue 用户中心</router-link>
</nav>
</header>
<!-- 子应用挂载区域 -->
<router-view />
<div id="react-container"></div>
<div id="vue-container"></div>
</div>
</template>
关键点解析:
entry:子应用的入口地址。如果是同源部署,可以是相对路径;如果是独立域名,必须是完整 URL。activeRule:路由匹配规则。qiankun 会根据这个规则决定何时加载哪个子应用。sandbox:沙箱配置。这是解决样式和 JS 冲突的关键,后面会细说。
1.2 子应用的改造
子应用不需要知道自己是“微应用”,它看起来就是一个普通应用。但需要暴露三个生命周期钩子:bootstrap、mount、unmount。
以 React 子应用为例:
// src/index.js
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
let instance = null
// 导出生命周期钩子
export async function bootstrap() {
console.log('[react] bootstrap')
}
export async function mount(props) {
console.log('[react] mount', props)
const { container, basename } = props
// 渲染应用
instance = ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
container ? container.querySelector('#root') : document.getElementById('root')
)
}
export async function unmount() {
console.log('[react] unmount')
if (instance) {
ReactDOM.unmountComponentAtNode(
container ? container.querySelector('#root') : document.getElementById('root')
)
instance = null
}
}
// 开发环境支持 HMR
if (!window.__QIANKUN__) {
bootstrap().then(mount)
}
这里有一个巨大的陷阱:子应用的路由配置。
在 Vue 子应用中,你必须使用 history 模式 并设置正确的 base。
// 子应用 router/index.js
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(window.__POWERED_BY_QIANKUN__ ? '/vue' : '/'),
// 关键判断:如果是微前端环境,base 必须是主应用配置的 activeRule
routes: [
{ path: '/', redirect: '/home' },
{ path: '/home', component: Home },
{ path: '/profile', component: Profile },
]
})
export default router
同时,在子应用的 Vite 或 Webpack 配置中,需要暴露端口供主应用加载,并设置 publicPath:
// vite.config.js (子应用)
export default {
base: process.env.NODE_ENV === 'production' ? '/vue/' : '/',
server: {
port: 7200,
cors: true,
headers: {
'Access-Control-Allow-Origin': '*', // 开发环境允许跨域
}
}
}
第二部分:样式隔离——解决“互相污染”的噩梦
这是微前端最头疼的问题之一。在同一个 DOM 树中,子应用 A 的 CSS 可能会意外地选中子应用 B 的元素,或者主应用的通用样式污染了子应用。
qiankun 提供了三种隔离方案,从宽松到严格:
2.1 作用域隔离(默认,宽松)
qiankun 默认给子应用的根节点添加一个独特的 attribute(如 data-qiankun="app-name"),并通过 JS 沙箱隔离全局变量。但对于 CSS,它不会自动限制作用域。
如果你的子应用用的是普通 CSS,必须手动给所有样式加上作用域选择器,或者使用 CSS Modules。
/* 错误示范:全局污染 */
.button {
color: red;
}
/* 正确示范:手动限制作用域 */
[data-qiankun="app-vue"] .button {
color: red;
}
2.2 JS 沙箱隔离(默认开启)
qiankun 使用 Proxy 代理 window 对象,并在应用卸载后恢复环境。这解决了 window.xxx 变量冲突的问题,但不解决 DOM 节点的全局修改。
2.3 严格样式隔离(Strict Style Isolation)—— 推荐
这是 qiankun 最强大的功能之一。开启 strictStyleIsolation: true 后,qiankun 会将子应用的样式包裹在一个 <style scoped> 标签中,并使用 Shadow DOM 技术进行隔离。
原理:qiankun 会在子应用容器外围创建一个 Shadow DOM,或者更准确地说,它通过动态创建 <style> 标签并修改选择器作用域来实现。实际上,qiankun 实现的是 CSS 作用域隔离,而非真正的 Shadow DOM,因为它要保留与主应用的通信能力(Shadow DOM 会阻断事件冒泡和某些布局)。
// 主应用配置
start({
sandbox: {
strictStyleIsolation: true, // 开启严格样式隔离
},
})
开启后,子应用的所有 CSS 都会被自动处理。例如,你写了 .title { color: blue; },qiankun 会自动将其转换为 .title[qiankun-hash-xxx] { color: blue; },并确保这个 attribute 只存在于子应用的 DOM 树中。
注意:启用严格样式隔离后,子应用无法穿透到主应用 DOM,主应用也无法直接通过 CSS 选择器样式化子应用内部元素。如果需要定制,请通过 props 传递样式配置。
2.4 特殊情况:第三方组件库的样式污染
有时候,你引入了 Element Plus 或 Ant Design,它们的样式是全局的。在严格样式隔离下,这些样式也会被自动处理。但如果你发现某些图标显示异常,或者弹窗(Popover/Modal)的位置计算错误,这是因为弹窗渲染到了 document.body 下,脱离了微前端的隔离容器。
解决方案:
- 修改弹窗容器:很多 UI 库允许配置
popup-container。将其指向微前端的根节点。 - 使用 Portal:在子应用中,手动将弹窗挂载到微前端容器内。
// Vue 3 子应用中,修改 Element Plus 的挂载点
import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
const app = createApp(App)
app.use(ElementPlus, {
size: 'small',
zIndex: 3000,
})
// 关键:让 popover 和 dropdown 等悬浮层挂载到子应用根节点
const container = window.__POWERED_BY_QIANKUN__
? document.querySelector('#vue-container')
: document.body
app.mount(container)
第三部分:通信难题——子应用与主应用的数据交换
在单体应用中,数据共享很简单,直接 import 或 Vuex/Pinia 即可。但在微前端中,应用之间是物理隔离的,通信必须通过机制来完成。
qiankun 提供了三种通信方式,我按推荐程度排序:
3.1 主从通信:props 传递(最简单)
主应用在加载子应用时,可以通过 props 向子应用传递数据。子应用通过生命周期钩子的参数接收。
// 主应用:注册时传递 props
registerMicroApps([
{
name: 'app-react',
entry: '//localhost:7100',
container: '#react-container',
activeRule: '/react',
props: {
userInfo: { id: 1, name: 'Alice' },
onThemeChange: (theme) => console.log('theme changed', theme)
}
}
])
// 子应用:接收 props
export async function mount(props) {
const { userInfo, onThemeChange } = props
console.log('Received userInfo:', userInfo)
}
这种方式适合主应用向子应用推送一次性数据,比如用户登录信息、主题配置等。
3.2 全局状态管理:initGlobalState(推荐)
这是 qiankun 内置的状态管理机制,基于 Zustand 的思想。它允许主应用和子应用之间双向通信。
// 主应用:初始化全局状态
const initialState = { user: null, permissions: [] }
const { onGlobalStateChange, setGlobalState } = initGlobalState(initialState)
// 监听状态变化
onGlobalStateChange((value, prev) => {
console.log('Global state changed:', value)
})
// 修改状态
setGlobalState({ user: { name: 'Bob' } })
// 子应用:同样引入 qiankun 来获取同一个状态实例
import { initGlobalState } from 'qiankun'
const initialState = { user: null, permissions: [] }
const { onGlobalStateChange, setGlobalState } = initGlobalState(initialState)
// 子应用修改状态,主应用能收到通知
setGlobalState({ user: { name: 'Charlie' } })
重要提示:initGlobalState 必须在主应用和子应用中独立调用,传入相同的初始状态结构。qiankun 内部会通过事件总线机制同步它们。这种方式轻量、简单,适合中小规模的状态共享。
3.3 子到主通信:dispatchEvent(最灵活)
当子应用需要触发主应用的行为时(例如:子应用点击“登出”,主应用需要跳转到登录页),可以使用 window.dispatchEvent。
// 子应用:触发自定义事件
window.dispatchEvent(new CustomEvent('qiankun.custom.event', {
detail: { action: 'logout' }
}))
// 主应用:监听事件
window.addEventListener('qiankun.custom.event', (e) => {
const { action } = e.detail
if (action === 'logout') {
// 执行登出逻辑
router.push('/login')
}
})
为了规范,建议约定事件命名空间,避免冲突,例如 qiankun.{appName}.{event}。
3.4 高级方案:共享模块(Shared Modules)
如果多个子应用都需要 lodash、moment 或 axios,重复加载会浪费带宽。使用 Webpack 5 的 Module Federation 或 qiankun 的插件可以共享这些模块。
在 qiankun 中,可以通过 externals 配置将这些库排除在子应用打包之外,由主应用统一提供。
// 主应用 webpack/vite 配置
// 将 react, react-dom 标记为外部依赖
externals: {
'react': 'React',
'react-dom': 'ReactDOM'
}
// 子应用同样配置 externals,并通过 script 标签引入主应用提供的版本
第四部分:路由同步——让浏览器前进后退正常工作
微前端最容易被忽视的问题是:当你在子应用中点击浏览器的前进/后退按钮,或者刷新页面时,主应用的路由是否能正确响应?
4.1 主应用路由配置
主应用必须配置通配符路由,将子应用的路由“吞掉”,防止 404。
// 主应用 router/index.js
import { createRouter, createWebHistory } from 'vue-router'
const routes = [
{ path: '/', component: Home },
// 通配符:所有未匹配的路由都交给微前端处理
{ path: '/:pathMatch(.*)', name: 'MicroAppWrapper', component: () => import('@/views/MicroApp.vue') }
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
4.2 子应用路由配置
子应用必须使用相对路径或basename,并且必须使用 history 模式。
// 子应用 router/index.js
import { createRouter, createWebHistory } from 'vue-router'
// 关键:basename 必须与主应用的 activeRule 一致
const basename = window.__POWERED_BY_QIANKUN__ ? '/vue' : '/'
const router = createRouter({
history: createWebHistory(basename),
routes: [
{ path: '/', component: Home },
{ path: '/profile', component: Profile }
]
})
export default router
4.3 路由监听与同步
当主应用检测到 URL 变化时,需要通知当前活跃的子应用更新路由。qiankun 默认会处理这个逻辑,但如果你发现刷新页面后子应用状态丢失,可能是因为子应用没有正确读取 window.location.pathname。
建议在子应用的 mount 钩子
