三、关键问题解决
3.1 路由劫持与同步
问题描述
主应用和子应用都有自己的路由系统,如何保证路由同步?浏览器前进后退如何处理?
解决方案
1. 主应用路由劫持
// 主应用 main.js
import { registerMicroApps, start } from 'qiankun'
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/',
component: Home
},
// 子应用路由占位
{
path: '/app1/:pathMatch(.*)*',
name: 'app1',
component: () => import('./views/SubAppContainer.vue')
}
]
})
// qiankun 会自动劫持 history API
registerMicroApps([
{
name: 'app1',
entry: '//localhost:8081',
container: '#subapp-container',
activeRule: '/app1',
props: {
// 传递路由 base
routerBase: '/app1'
}
}
])
start()
2. 子应用路由配置
// 子应用 main.js
import { createRouter, createWebHistory } from 'vue-router'
function render(props = {}) {
const { container, routerBase } = props
// 使用主应用传递的 routerBase
const router = createRouter({
history: createWebHistory(routerBase || '/app1'),
routes: [
{
path: '/',
component: Home
},
{
path: '/list',
component: List
},
{
path: '/detail/:id',
component: Detail
}
]
})
const app = createApp(App)
app.use(router)
app.mount(container ? container.querySelector('#app') : '#app')
}
3. 路由监听与转发
// 主应用监听路由变化
import { useRouter } from 'vue-router'
const router = useRouter()
router.afterEach((to, from) => {
console.log('主应用路由变化', to.path)
// 可以在这里做一些全局处理
// 比如记录路由历史、埋点等
})
// 子应用路由变化会自动同步到浏览器地址栏
// 因为 qiankun 劫持了 history API
4. 处理浏览器前进后退
// qiankun 自动处理,无需额外配置
// 原理:劫持了 pushState、replaceState、popstate
// 如果需要自定义处理
window.addEventListener('popstate', (event) => {
console.log('浏览器前进/后退', event.state)
})
5. 路由守卫处理
// 主应用全局路由守卫
router.beforeEach((to, from, next) => {
// 检查用户权限
const token = localStorage.getItem('token')
if (!token && to.path !== '/login') {
next('/login')
return
}
// 检查子应用权限
if (to.path.startsWith('/app1')) {
const hasApp1Permission = checkPermission('app1')
if (!hasApp1Permission) {
next('/403')
return
}
}
next()
})
// 子应用路由守卫
const router = createRouter({...})
router.beforeEach((to, from, next) => {
// 子应用内部路由守卫
console.log('子应用路由守卫', to.path)
next()
})
完整示例代码
<!-- 主应用 App.vue -->
<template>
<div id="main-app">
<nav>
<router-link to="/">首页</router-link>
<router-link to="/app1">子应用1</router-link>
<router-link to="/app1/list">子应用1-列表</router-link>
<router-link to="/app1/detail/123">子应用1-详情</router-link>
</nav>
<div class="content">
<router-view v-if="!isSubApp"></router-view>
<div id="subapp-container" v-show="isSubApp"></div>
</div>
<div class="route-info">
<p>当前路由: {{ currentRoute }}</p>
<p>路由历史: {{ routeHistory.join(' -> ') }}</p>
</div>
</div>
</template>
<script setup>
import { ref, computed, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
const route = useRoute()
const router = useRouter()
const currentRoute = computed(() => route.path)
const routeHistory = ref([route.path])
const isSubApp = computed(() => {
return route.path.startsWith('/app1') ||
route.path.startsWith('/app2')
})
// 记录路由历史
watch(() => route.path, (newPath) => {
if (routeHistory.value[routeHistory.value.length - 1] !== newPath) {
routeHistory.value.push(newPath)
if (routeHistory.value.length > 10) {
routeHistory.value.shift()
}
}
})
</script>
3.2 全局状态共享 (initGlobalState)
问题描述
主应用和子应用之间如何共享状态?如用户信息、权限数据、主题配置等。
解决方案
1. 初始化全局状态
// 主应用 main.js
import { initGlobalState } from 'qiankun'
// 初始化状态
const initialState = {
user: {
name: 'Admin',
role: 'admin',
avatar: 'https://xxx.com/avatar.jpg'
},
theme: 'light',
token: localStorage.getItem('token'),
permissions: ['user:read', 'user:write']
}
const actions = initGlobalState(initialState)
// 监听全局状态变化
actions.onGlobalStateChange((state, prev) => {
console.log('全局状态变化', state, prev)
// 状态变化时的业务逻辑
if (state.token !== prev.token) {
if (state.token) {
console.log('用户登录')
} else {
console.log('用户登出')
router.push('/login')
}
}
})
// 修改全局状态
actions.setGlobalState({
user: { name: 'New User' }
})
// 获取全局状态
const currentState = actions.getGlobalState()
// 注销监听
// actions.offGlobalStateChange()
2. 传递给子应用
// 主应用注册子应用
registerMicroApps([
{
name: 'app1',
entry: '//localhost:8081',
container: '#subapp-container',
activeRule: '/app1',
props: {
// 传递全局状态的方法
getGlobalState: actions.getGlobalState,
setGlobalState: actions.setGlobalState,
onGlobalStateChange: actions.onGlobalStateChange,
offGlobalStateChange: actions.offGlobalStateChange
}
}
])
3. 子应用使用全局状态
// 子应用 main.js
let globalStateActions = null
export async function mount(props) {
const {
getGlobalState,
setGlobalState,
onGlobalStateChange,
offGlobalStateChange
} = props
// 保存全局状态操作方法
globalStateActions = {
getGlobalState,
setGlobalState,
onGlobalStateChange,
offGlobalStateChange
}
// 获取全局状态
const globalState = getGlobalState()
console.log('子应用获取全局状态', globalState)
// 监听状态变化
onGlobalStateChange((state, prev) => {
console.log('子应用监听到状态变化', state, prev)
// 可以同步到子应用的 store
if (window.__APP_STORE__) {
window.__APP_STORE__.commit('updateUser', state.user)
}
}, true) // true 表示立即执行一次
render(props)
}
export async function unmount() {
// 卸载时注销监听
if (globalStateActions) {
globalStateActions.offGlobalStateChange()
globalStateActions = null
}
instance.unmount()
}
// 子应用内修改全局状态
function updateUserInfo(newUser) {
if (globalStateActions) {
globalStateActions.setGlobalState({
user: newUser
})
}
}
4. 结合 Pinia/Vuex 使用
// 主应用 store
import { createPinia } from 'pinia'
import { initGlobalState } from 'qiankun'
const pinia = createPinia()
// 用户 store
export const useUserStore = defineStore('user', {
state: () => ({
user: null,
token: null
}),
actions: {
setUser(user) {
this.user = user
// 同步到全局状态
if (window.__QIANKUN_ACTIONS__) {
window.__QIANKUN_ACTIONS__.setGlobalState({ user })
}
},
setToken(token) {
this.token = token
localStorage.setItem('token', token)
// 同步到全局状态
if (window.__QIANKUN_ACTIONS__) {
window.__QIANKUN_ACTIONS__.setGlobalState({ token })
}
}
}
})
// 初始化全局状态
const actions = initGlobalState({
user: null,
token: localStorage.getItem('token')
})
// 保存到 window 供其他地方使用
window.__QIANKUN_ACTIONS__ = actions
// 监听全局状态变化,同步到 pinia
actions.onGlobalStateChange((state) => {
const userStore = useUserStore()
if (state.user) userStore.user = state.user
if (state.token) userStore.token = state.token
})
5. 完整通信示例
<!-- 主应用组件 -->
<template>
<div class="main-app">
<div class="user-panel">
<h3>用户信息</h3>
<p>姓名: {{ globalState.user?.name }}</p>
<p>角色: {{ globalState.user?.role }}</p>
<button @click="updateUser">更新用户</button>
<button @click="logout">退出登录</button>
</div>
<div class="theme-switch">
<label>主题:</label>
<select v-model="globalState.theme" @change="changeTheme">
<option value="light">浅色</option>
<option value="dark">深色</option>
</select>
</div>
</div>
</template>
<script setup>
import { reactive, onMounted } from 'vue'
const globalState = reactive({
user: null,
theme: 'light',
token: null
})
onMounted(() => {
// 监听全局状态
if (window.__QIANKUN_ACTIONS__) {
const state = window.__QIANKUN_ACTIONS__.getGlobalState()
Object.assign(globalState, state)
window.__QIANKUN_ACTIONS__.onGlobalStateChange((state) => {
Object.assign(globalState, state)
})
}
})
const updateUser = () => {
window.__QIANKUN_ACTIONS__?.setGlobalState({
user: {
name: 'Updated User',
role: 'admin'
}
})
}
const changeTheme = () => {
window.__QIANKUN_ACTIONS__?.setGlobalState({
theme: globalState.theme
})
document.body.setAttribute('data-theme', globalState.theme)
}
const logout = () => {
window.__QIANKUN_ACTIONS__?.setGlobalState({
user: null,
token: null
})
localStorage.removeItem('token')
}
</script>
3.3 样式隔离方案
问题描述
主应用和子应用的样式可能冲突,如何实现样式隔离?
三种方案对比
方案1: Shadow DOM (严格隔离)
// 主应用启动配置
start({
sandbox: {
strictStyleIsolation: true
}
})
// 优点:完全隔离,最彻底
// 缺点:某些组件库不支持(如 antd Modal)
解决 Modal 等弹窗问题
// 子应用中
import { Modal } from 'ant-design-vue'
// 方法1: 修改 getContainer
Modal.confirm({
title: '提示',
content: '确认操作?',
getContainer: () => document.getElementById('app') // 挂载到子应用容器内
})
// 方法2: 全局配置
import { ConfigProvider } from 'ant-design-vue'
app.use(ConfigProvider, {
getPopupContainer: (node) => {
return node?.parentNode || document.getElementById('app')
}
})
方案2: Scoped CSS (宽松隔离)
// 主应用启动配置
start({
sandbox: {
experimentalStyleIsolation: true
}
})
// 原理:自动添加属性选择器
// 原始: .title { color: red; }
// 处理后: div[data-qiankun-app1] .title { color: red; }
// 优点:兼容性好,性能好
// 缺点:隔离不彻底,可能有样式泄露
方案3: 手动添加命名空间
<!-- 子应用根组件 -->
<template>
<div class="app1-container">
<!-- 所有样式都加前缀 -->
<div class="app1-title">标题</div>
<div class="app1-content">内容</div>
</div>
</template>
<style scoped>
/* 使用 scoped 或手动加前缀 */
.app1-container {
/* 样式 */
}
.app1-title {
color: #333;
}
.app1-content {
font-size: 14px;
}
</style>
方案4: CSS Modules
<template>
<div :class="$style.container">
<h1 :class="$style.title">标题</h1>
</div>
</template>
<style module>
.container {
padding: 20px;
}
.title {
font-size: 24px;
}
</style>
方案5: 动态样式加载/卸载
// 子应用 mount 时加载样式
export async function mount(props) {
// 创建样式节点
const styleNodes = []
// 加载样式
const styles = ['style1.css', 'style2.css']
styles.forEach(href => {
const link = document.createElement('link')
link.rel = 'stylesheet'
link.href = href
document.head.appendChild(link)
styleNodes.push(link)
})
// 保存样式节点引用
window.__APP_STYLE_NODES__ = styleNodes
render(props)
}
// 子应用 unmount 时卸载样式
export async function unmount() {
// 移除样式节点
if (window.__APP_STYLE_NODES__) {
window.__APP_STYLE_NODES__.forEach(node => {
document.head.removeChild(node)
})
window.__APP_STYLE_NODES__ = null
}
instance.unmount()
}
推荐方案
// 组合使用效果最佳
// 1. 主应用:开启实验性样式隔离
start({
sandbox: {
experimentalStyleIsolation: true
}
})
// 2. 子应用:使用 scoped 或 CSS Modules
<style scoped>
/* 子应用样式 */
</style>
// 3. 公共样式:放在主应用,子应用可以继承
// 主应用 global.css
:root {
--primary-color: #1890ff;
--font-size: 14px;
}
// 4. 弹窗组件:指定挂载容器
Modal.config({
getContainer: () => document.getElementById('app')
})
3.4 静态资源路径问题 (publicPath)
问题描述
子应用部署后,图片、字体等静态资源 404,路径不对。
解决方案
1. 配置 publicPath
// 子应用 public-path.js
if (window.__POWERED_BY_QIANKUN__) {
// qiankun 环境,使用动态 publicPath
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__
} else {
// 独立运行,使用固定 publicPath
__webpack_public_path__ = '/'
}
// main.js 顶部引入
import './public-path'
2. Webpack 配置
// vue.config.js
module.exports = {
publicPath: process.env.NODE_ENV === 'production'
? '/app1/' // 生产环境使用子应用的部署路径
: '/', // 开发环境使用根路径
devServer: {
port: 8081,
headers: {
'Access-Control-Allow-Origin': '*'
}
},
configureWebpack: {
output: {
library: `app1-[name]`,
libraryTarget: 'umd',
jsonpFunction: `webpackJsonp_app1`
}
}
}
3. 运行时设置
// 子应用 main.js
export async function mount(props) {
// 方式1: 从 props 获取
const { publicPath } = props
if (publicPath) {
__webpack_public_path__ = publicPath
}
// 方式2: 从主应用配置获取
if (window.__QIANKUN_PUBLIC_PATH__) {
__webpack_public_path__ = window.__QIANKUN_PUBLIC_PATH__
}
render(props)
}
4. 主应用配置
// 主应用注册子应用
registerMicroApps([
{
name: 'app1',
entry: process.env.NODE_ENV === 'production'
? 'https://cdn.example.com/app1/' // 生产环境 CDN
: '//localhost:8081', // 开发环境本地
container: '#subapp-container',
activeRule: '/app1',
props: {
publicPath: 'https://cdn.example.com/app1/'
}
}
])
5. 图片路径处理
<template>
<div>
<!-- 方式1: 使用 require 动态引入 -->
<img :src="require('@/assets/logo.png')" />
<!-- 方式2: 放在 public 目录,使用绝对路径 -->
<img :src="`${publicPath}logo.png`" />
<!-- 方式3: 使用 CDN -->
<img src="https://cdn.example.com/logo.png" />
</div>
</template>
<script setup>
import { ref } from 'vue'
const publicPath = ref(window.__QIANKUN_PUBLIC_PATH__ || '/')
</script>
6. 字体文件处理
/* 方式1: 使用相对路径 */
@font-face {
font-family: 'MyFont';
src: url('./fonts/myfont.woff2') format('woff2');
}
/* 方式2: 使用 CDN */
@font-face {
font-family: 'MyFont';
src: url('https://cdn.example.com/fonts/myfont.woff2') format('woff2');
}
3.5 子应用独立运行配置
问题描述
子应用既要能在微前端环境运行,也要能独立运行和开发。
完整解决方案
// 子应用 main.js
import { createApp } from 'vue'
import { createRouter, createWebHistory } from 'vue-router'
import App from './App.vue'
import routes from './router'
import './public-path'
let instance = null
let router = null
// 渲染函数
function render(props = {}) {
const { container, routerBase } = props
// 创建路由
router = createRouter({
// qiankun 环境使用主应用传入的 base,独立运行使用根路径
history: createWebHistory(
window.__POWERED_BY_QIANKUN__ ? routerBase : '/'
),
routes
})
// 创建应用
instance = createApp(App)
instance.use(router)
// 如果有 pinia/vuex,也要在这里注册
// instance.use(pinia)
// 挂载
const containerEl = container
? container.querySelector('#app')
: document.querySelector('#app')
instance.mount(containerEl)
}
// 独立运行
if (!window.__POWERED_BY_QIANKUN__) {
render()
}
// qiankun 生命周期
export async function bootstrap() {
console.log('[app1] bootstrap')
}
export async function mount(props) {
console.log('[app1] mount', props)
render(props)
}
export async function unmount() {
console.log('[app1] unmount')
instance.unmount()
instance = null
router = null
}
export async function update(props) {
console.log('[app1] update', props)
}
环境判断逻辑
// utils/env.js
export const isQiankun = () => {
return window.__POWERED_BY_QIANKUN__
}
export const getBaseURL = () => {
if (isQiankun()) {
return window.__QIANKUN_BASE_URL__ || '/api'
}
return process.env.VUE_APP_BASE_URL || '/api'
}
export const getPublicPath = () => {
if (isQiankun()) {
return window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__
}
return '/'
}
// 使用
import { isQiankun, getBaseURL } from '@/utils/env'
const apiClient = axios.create({
baseURL: getBaseURL()
})
if (isQiankun()) {
// 微前端环境特殊处理
console.log('运行在微前端环境')
} else {
// 独立运行
console.log('独立运行')
}
开发环境配置
// vue.config.js
module.exports = {
devServer: {
port: 8081,
headers: {
'Access-Control-Allow-Origin': '*'
},
// 独立开发时的代理配置
proxy: {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true
}
}
},
configureWebpack: {
output: {
// 开发环境也配置 library,方便调试
library: `app1-[name]`,
libraryTarget: 'umd',
jsonpFunction: `webpackJsonp_app1`
}
}
}
独立运行时的入口 HTML
<!-- public/index.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>子应用1 - 独立运行</title>
</head>
<body>
<div id="app"></div>
<!-- 独立运行时可以加载一些全局依赖 -->
<script>
// 标记独立运行环境
window.__INDEPENDENT_RUN__ = true
</script>
</body>
</html>
四、简历撰写指南
4.1 项目经验描述模板
项目名称: qiankun 微前端架构落地实践
项目时间: 2024.06 - 2024.12
项目描述: 负责公司核心业务系统的微前端改造,采用 qiankun 框架将单体应用拆分为主应用 + 8 个子应用。实现了各业务线的技术独立和团队自治,解决了大型单体应用的开发、部署、维护难题。
核心职责
- qiankun 技术选型与架构设计
- 深入研究 qiankun 核心原理:single-spa、import-html-entry、沙箱隔离机制
- 设计主子应用通信方案、路由管理方案、状态共享方案
- 制定代码规范、开发流程、部署策略
- 主应用架构实现
- 实现应用注册、生命周期管理、全局状态管理(initGlobalState)
- 开发统一的 Layout、导航菜单、权限控制、错误边界
- 实现子应用预加载、缓存策略,首屏加载优化
- 关键技术问题解决
- JS 沙箱隔离:启用 Proxy 沙箱,解决全局变量污染,支持多实例运行
- CSS 样式隔离:采用 experimentalStyleIsolation + scoped 组合方案
- 路由劫持与同步:实现主子应用路由无缝切换,支持浏览器前进后退
- 静态资源路径:通过 publicPath 动态配置解决资源 404 问题
- 全局状态共享:基于 initGlobalState 实现主子应用数据通信
- 子应用改造指导
- 制定生命周期规范(bootstrap/mount/unmount)
- 规范 webpack 配置(library/libraryTarget/publicPath)
- 实现子应用独立运行和微前端运行双模式
- 工程化建设
- 开发脚手架工具,快速创建符合规范的子应用
- 建立 CI/CD 流程,实现子应用独立部署
- 实现监控系统,监控子应用加载性能、错误率
技术栈: qiankun、single-spa、Vue3、Webpack、import-html-entry、Proxy
项目成果
- 构建时间从 15 分钟降至单应用 3 分钟,效率提升 80%
- 各业务线独立开发部署,团队协作效率提升 50%
- 发布频率从月发布提升到周发布,迭代速度提升 4 倍
- 故障影响面降低 80%,单个子应用故障不影响其他模块
- 支持技术栈逐步升级,已有 3 个子应用从 Vue2 升级到 Vue3
- 新人上手时间从 2 周缩短到 3 天,培训成本降低 70%
4.2 SOP 标准回答话术
面试官:详细说说 qiankun 的沙箱隔离原理
回答话术: "好的。qiankun 的沙箱隔离是它的核心能力之一,主要解决不同子应用的 JS 执行环境隔离问题。
qiankun 提供了三种沙箱方案:
第一种是 ProxySandbox,这是默认方案也是最推荐的。它的原理是用 Proxy 代理 window 对象。具体来说,qiankun 会为每个子应用创建一个 fakeWindow 对象,然后用 Proxy 代理这个对象。当子应用访问 window.xxx 时,实际访问的是代理对象。如果这个属性是子应用自己设置的,就从 fakeWindow 取;如果不是,就从真实 window 取。这样子应用的全局变量都记录在 fakeWindow 里,不会污染真实 window,实现了完全隔离。而且这个方案支持多实例,性能也很好。
第二种是 SnapshotSandbox,快照沙箱。它的原理比较简单:应用激活时,遍历 window 对象做一个快照;应用失活时,对比快照和当前 window,把变化的属性恢复回去。这个方案的优势是兼容性好,支持 IE11,但缺点是性能差(需要遍历 window)、不支持多实例、而且会暂时污染全局 window。
第三种是 LegacySandbox,遗留沙箱,是 Proxy 和快照的结合。它用 Proxy 记录所有变更,同时还是会修改真实 window,只不过在失活时会还原。这个方案主要是为了兼容一些特殊场景。
我们项目用的是 ProxySandbox,因为我们不需要兼容 IE,而且需要支持多个子应用同时运行的场景。实测下来隔离效果很好,没有出现全局变量污染的问题。
不过沙箱也不是万能的,比如一些直接操作 DOM 的第三方库,或者往 document 上绑定事件的,沙箱是管不到的。这种情况需要在子应用 unmount 时手动清理。"
面试官:qiankun 的样式隔离是如何实现的?
回答话术: "qiankun 提供了两种样式隔离方案:strictStyleIsolation 和 experimentalStyleIsolation。
strictStyleIsolation 是严格样式隔离,用的是 Shadow DOM。原理是把子应用的内容挂载到一个 Shadow Root 里面,Shadow DOM 有天然的样式隔离能力,里面的样式不会泄露出来,外面的样式也进不去。这是最彻底的隔离方案。
但 Shadow DOM 也有缺点。最大的问题是某些组件库不支持,比如 antd 的 Modal、Tooltip 这些弹窗组件,默认会挂载到 document.body,在 Shadow DOM 里就找不到样式了。解决办法是修改组件的 getContainer,让它挂载到子应用容器里面。但这需要改很多代码,比较麻烦。
所以我们项目用的是 experimentalStyleIsolation,实验性样式隔离。它的原理是给子应用的所有样式选择器加一个属性前缀。比如原来是 .title { color: red },处理后变成 div[data-qiankun-app1] .title { color: red }。同时给子应用的容器加上这个属性 data-qiankun-app1。这样样式就只对子应用内的元素生效了。
这个方案兼容性好,性能也好,大部分场景都够用。但它不是完全隔离,如果子应用写了很宽泛的选择器,比如 * { margin: 0 },还是可能影响主应用。
所以我们的最佳实践是:
- 开启 experimentalStyleIsolation
- 子应用使用 scoped 或 CSS Modules
- 公共样式放主应用
- 弹窗组件指定挂载容器
这样组合使用,既保证了隔离性,又有很好的兼容性。"
4.3 难点与亮点分析
难点1:子应用间通信
问题描述: 两个子应用之间需要通信,比如子应用 A 创建了一个订单,子应用 B 需要刷新订单列表。
解决方案
// 方案1: 通过主应用中转
// 主应用
const actions = initGlobalState({ events: {} })
actions.onGlobalStateChange((state, prev) => {
if (state.events.orderCreated) {
console.log('订单创建事件', state.events.orderCreated)
// 可以在这里做统一的事件分发
}
})
// 子应用 A 触发事件
actions.setGlobalState({
events: {
orderCreated: { orderId: '123', timestamp: Date.now() }
}
})
// 子应用 B 监听事件
actions.onGlobalStateChange((state) => {
if (state.events.orderCreated) {
refreshOrderList()
}
})
// 方案2: 使用 EventBus
// 主应用提供 EventBus
class EventBus {
constructor() {
this.events = {}
}
on(event, handler) {
if (!this.events[event]) {
this.events[event] = []
}
this.events[event].push(handler)
}
emit(event, data) {
if (this.events[event]) {
this.events[event].forEach(handler => handler(data))
}
}
off(event, handler) {
if (this.events[event]) {
this.events[event] = this.events[event].filter(h => h !== handler)
}
}
}
window.__EVENT_BUS__ = new EventBus()
// 子应用 A
window.__EVENT_BUS__.emit('orderCreated', { orderId: '123' })
// 子应用 B
window.__EVENT_BUS__.on('orderCreated', (data) => {
console.log('订单创建', data)
refreshOrderList()
})
难点2:公共依赖处理
问题描述: Vue、Vue Router、Axios 等公共库,每个子应用都打包会很大。
解决方案
// 主应用 index.html 引入公共依赖
<script src="https://cdn.jsdelivr.net/npm/vue@3"></script>
<script src="https://cdn.jsdelivr.net/npm/vue-router@4"></script>
<script src="https://cdn.jsdelivr.net/npm/axios@1"></script>
// 子应用 webpack 配置
configureWebpack: {
externals: {
vue: 'Vue',
'vue-router': 'VueRouter',
axios: 'axios'
}
}
// 版本管理
// 建立公共依赖版本表
{
"vue": "3.3.4",
"vue-router": "4.2.4",
"axios": "1.5.0"
}
// 子应用必须遵守版本约定
难点3:首屏加载优化
问题描述: 微前端架构下,首屏需要加载主应用 + 子应用,时间长。
解决方案
// 1. 预加载
start({
prefetch: true, // 自动预加载
// 或自定义预加载策略
prefetch: [
{ name: 'app1', force: true }, // 强制预加载
'app2' // 空闲时预加载
]
})
// 2. 手动预加载高频子应用
import { prefetchApps } from 'qiankun'
prefetchApps([
{ name: 'app1', entry: '//localhost:8081' }
])
// 3. 缓存机制
// qiankun 默认会缓存子应用资源
// 可以通过 import-html-entry 的 cache 参数控制
// 4. 代码分割
// 子应用使用路由懒加载
const routes = [
{
path: '/list',
component: () => import('./views/List.vue')
}
]
// 5. 关键路径优化
// 主应用只加载必要的代码
// 非关键功能延迟加载
亮点1:渐进式改造
不是一次性重构,而是渐进式迁移:
- 先抽离最独立的模块作为子应用
- 主应用保留核心功能继续运行
- 验证方案可行性后逐步迁移
- 最后清理旧代码
这种策略风险可控,业务不中断。
亮点2:完整的监控体系
// 子应用加载监控
registerMicroApps(apps, {
beforeLoad: [(app) => {
console.time(`${app.name}-load`)
return Promise.resolve()
}],
afterMount: [(app) => {
console.timeEnd(`${app.name}-load`)
// 上报加载时间
reportMetrics({
appName: app.name,
loadTime: performance.now()
})
return Promise.resolve()
}]
})
// 错误捕获
window.addEventListener('error', (event) => {
if (event.filename.includes('app1')) {
console.error('子应用1错误', event)
reportError({
app: 'app1',
error: event.message
})
}
})
亮点3:开发工具链
# 脚手架快速创建子应用
npm create micro-app my-app
# 自动配置 webpack
# 自动添加生命周期
# 自动配置 publicPath
# 一键启动所有应用
npm run dev:all
# 一键部署
npm run deploy:app1
4.4 完整简历示例
【qiankun 微前端架构落地实践】2024.06 - 2024.12
项目背景:
公司核心业务系统代码量 30 万行,涉及 6 个业务线,3 个团队协作。存在构建慢、协作难、技术债重、发布风险高等问题。
技术选型:
对比 qiankun、Wujie、Micro-App 后选择 qiankun,原因:社区最活跃、生态最完善、适合企业级长期项目。
核心工作:
1. 技术方案设计
- 深入研究 qiankun 核心原理:single-spa 生命周期、import-html-entry 加载机制、Proxy 沙箱隔离
- 设计主应用架构:应用注册、路由管理、状态共享、权限控制
- 制定拆分策略:按业务域拆分为 1 主应用 + 8 子应用
2. 关键技术攻坚
- JS 沙箱隔离:启用 ProxySandbox,支持多实例,完全隔离全局变量
- CSS 样式隔离:experimentalStyleIsolation + scoped 组合方案,兼容 antd 等组件库
- 路由劫持同步:实现主子应用路由无缝切换,支持浏览器前进后退
- 全局状态共享:基于 initGlobalState 实现主子应用通信,开发 EventBus 增强通信能力
- 静态资源路径:通过动态 publicPath 解决部署后资源 404 问题
3. 工程化建设
- 开发脚手架工具,快速创建符合规范的子应用,降低 80% 接入成本
- 建立 CI/CD 流程,实现子应用独立构建部署
- 实现监控系统:子应用加载性能、错误捕获、用户行为分析
4. 团队赋能
- 编写技术文档和最佳实践指南
- 组织技术分享和培训
- Code Review 保证代码质量
技术栈:
qiankun、single-spa、Vue3、Webpack、import-html-entry、Proxy
项目成果:
- 构建时间:从 15 分钟降至单应用 3 分钟(提升 80%)
- 协作效率:各业务线独立开发部署,协作效率提升 50%,代码冲突减少 90%
- 迭代速度:发布频率从月发布提升到周发布(提升 4 倍)
- 故障影响:单个子应用故障不影响其他模块(故障面降低 80%)
- 技术升级:支持渐进式升级,已有 3 个子应用从 Vue2 升级到 Vue3
- 培训成本:新人上手时间从 2 周缩短到 3 天(降低 70%)
- 系统稳定性:线上故障率下降 60%,平均修复时间缩短 70%
五、总结
qiankun 是目前最成熟的微前端解决方案,核心优势:
- 基于 single-spa,生态完善
- 完善的沙箱隔离机制
- 灵活的样式隔离方案
- 强大的全局状态管理
- 活跃的社区支持
关键要点:
- 理解核心原理(single-spa + import-html-entry + 沙箱)
- 解决关键问题(路由、状态、样式、资源)
- 制定开发规范
- 建立监控体系
- 持续优化改进