一、qiankun 核心原理
1.1 整体架构
qiankun 架构层级
├── single-spa(基础层)
│ ├── 应用注册管理
│ ├── 生命周期调度
│ └── 路由监听
│
├── import-html-entry(加载层)
│ ├── HTML 解析
│ ├── JS 执行
│ └── CSS 注入
│
├── 沙箱隔离(隔离层)
│ ├── Proxy 沙箱(默认)
│ ├── Snapshot 快照沙箱
│ └── LegacySandbox 遗留沙箱
│
└── 样式隔离(样式层)
├── Shadow DOM(严格)
├── Scoped CSS(宽松)
└── Dynamic Stylesheet(动态)
1.2 single-spa 应用注册与生命周期
核心概念: single-spa 是 qiankun 的基础,负责管理多个应用的注册、加载、卸载。
生命周期钩子
// 子应用必须导出三个生命周期函数
// 1. bootstrap - 应用首次加载时调用,只调用一次
export async function bootstrap() {
console.log('子应用首次加载')
// 初始化工作,比如创建 Vue 实例需要的根节点
}
// 2. mount - 应用每次激活时调用
export async function mount(props) {
console.log('子应用挂载', props)
// 创建 Vue 实例,挂载 DOM
render(props)
}
// 3. unmount - 应用每次失活时调用
export async function unmount() {
console.log('子应用卸载')
// 销毁 Vue 实例,清理副作用
instance.$destroy()
}
// 可选:update - 主应用传递的 props 更新时调用
export async function update(props) {
console.log('子应用更新', props)
}
注册应用
import { registerMicroApps, start } from 'qiankun'
registerMicroApps([
{
name: 'app1', // 应用名称
entry: '//localhost:8081', // 应用入口
container: '#subapp-container', // 容器节点
activeRule: '/app1', // 激活路由
props: { // 传递给子应用的数据
data: { user: 'admin' }
}
}
])
start() // 启动 qiankun
1.3 import-html-entry 动态加载
工作原理
- 获取子应用的 HTML 文本
- 解析 HTML,提取 JS 和 CSS
- 创建沙箱环境
- 执行 JS,注入 CSS
- 返回生命周期函数
核心代码逻辑
// 简化版实现原理
async function importHTML(url) {
// 1. 获取 HTML
const html = await fetch(url).then(res => res.text())
// 2. 解析 HTML
const { template, scripts, styles } = parseHTML(html)
// 3. 加载并执行 scripts
const jsCode = await Promise.all(
scripts.map(src => fetch(src).then(res => res.text()))
)
// 4. 创建沙箱并执行
const sandbox = new ProxySandbox()
const exports = sandbox.execScripts(jsCode)
// 5. 注入样式
styles.forEach(style => {
const styleNode = document.createElement('style')
styleNode.innerHTML = style
document.head.appendChild(styleNode)
})
return {
template, // 处理后的 HTML 模板
assetPublicPath, // 资源公共路径
getExternalScripts, // 获取外部脚本
getExternalStyleSheets, // 获取外部样式
execScripts // 执行脚本
}
}
1.4 JS 沙箱隔离
三种沙箱方案
1.4.1 ProxySandbox(推荐)
原理: 使用 Proxy 代理 window 对象
class ProxySandbox {
constructor() {
this.running = false
const fakeWindow = Object.create(null)
this.proxyWindow = new Proxy(fakeWindow, {
get(target, prop) {
// 优先从沙箱内取值
if (prop in target) {
return target[prop]
}
// 否则从真实 window 取值
return window[prop]
},
set(target, prop, value) {
// 所有设置都记录在沙箱内
target[prop] = value
return true
}
})
}
active() {
this.running = true
}
inactive() {
this.running = false
}
}
// 使用示例
const sandbox = new ProxySandbox()
sandbox.active()
// 子应用的代码运行在沙箱内
with(sandbox.proxyWindow) {
var count = 1 // 这个 count 只在沙箱内
console.log(window.count) // undefined,不会污染主应用
}
sandbox.inactive()
优势
- 完全隔离,互不影响
- 支持多实例
- 性能好
劣势
- 不支持 IE11
- 需要 Proxy 支持
1.4.2 SnapshotSandbox(快照沙箱)
原理: 激活时记录 window 快照,失活时恢复
class SnapshotSandbox {
constructor() {
this.modifyPropsMap = {} // 记录被修改的属性
}
active() {
// 记录当前 window 的快照
this.windowSnapshot = {}
for (const prop in window) {
this.windowSnapshot[prop] = window[prop]
}
// 恢复上次的修改
Object.keys(this.modifyPropsMap).forEach(prop => {
window[prop] = this.modifyPropsMap[prop]
})
}
inactive() {
// 记录被修改的属性
for (const prop in window) {
if (window[prop] !== this.windowSnapshot[prop]) {
this.modifyPropsMap[prop] = window[prop]
// 恢复原始值
window[prop] = this.windowSnapshot[prop]
}
}
}
}
// 使用示例
const sandbox = new SnapshotSandbox()
sandbox.active()
window.count = 1 // 修改 window
sandbox.inactive() // 恢复 window
console.log(window.count) // undefined
优势
- 兼容性好,支持 IE11
- 实现简单
劣势
- 性能差(遍历 window)
- 不支持多实例
- 会污染全局 window
1.4.3 LegacySandbox(遗留沙箱)
原理: Proxy + 快照的结合
class LegacySandbox {
constructor() {
this.addedPropsMap = {} // 新增属性
this.modifiedPropsOriginalValueMap = {} // 修改的原始值
this.currentUpdatedPropsValueMap = {} // 当前修改的值
const { addedPropsMap, modifiedPropsOriginalValueMap, currentUpdatedPropsValueMap } = this
const fakeWindow = Object.create(null)
this.proxyWindow = new Proxy(fakeWindow, {
set(target, prop, value) {
if (!window.hasOwnProperty(prop)) {
addedPropsMap[prop] = value
} else if (!modifiedPropsOriginalValueMap.hasOwnProperty(prop)) {
modifiedPropsOriginalValueMap[prop] = window[prop]
}
currentUpdatedPropsValueMap[prop] = value
window[prop] = value
return true
},
get(target, prop) {
return window[prop]
}
})
}
active() {
// 恢复环境
Object.keys(this.currentUpdatedPropsValueMap).forEach(prop => {
window[prop] = this.currentUpdatedPropsValueMap[prop]
})
}
inactive() {
// 还原环境
Object.keys(this.addedPropsMap).forEach(prop => {
delete window[prop]
})
Object.keys(this.modifiedPropsOriginalValueMap).forEach(prop => {
window[prop] = this.modifiedPropsOriginalValueMap[prop]
})
}
}
优势
- 性能较好
- 记录了所有变更
劣势
- 仍会污染 window
- 不支持多实例
1.5 CSS 样式隔离
三种隔离方案
1.5.1 Shadow DOM(严格隔离)
// qiankun 配置
start({
sandbox: {
strictStyleIsolation: true // 开启严格样式隔离
}
})
// 实现原理
const container = document.querySelector('#subapp-container')
const shadowRoot = container.attachShadow({ mode: 'open' })
// 子应用内容挂载到 Shadow DOM
shadowRoot.innerHTML = `
<style>
.title { color: red; }
</style>
<div class="title">子应用标题</div>
`
// 样式完全隔离,不会影响主应用
优势
- 完全隔离,最彻底
- 子应用样式不会泄露
- 主应用样式不会影响子应用
劣势
- 某些组件库不支持(如 antd 的弹窗)
- 调试困难
- 性能略差
1.5.2 Scoped CSS(宽松隔离)
// qiankun 配置
start({
sandbox: {
experimentalStyleIsolation: true // 开启实验性样式隔离
}
})
// 实现原理:给子应用所有样式加前缀
// 原始样式
.title { color: red; }
// 处理后
div[data-qiankun="app1"] .title { color: red; }
// 容器添加属性
<div id="subapp-container" data-qiankun="app1">
<div class="title">子应用标题</div>
</div>
优势
- 兼容性好
- 性能好
- 可以使用主应用的全局样式
劣势
- 隔离不彻底
- 可能有样式泄露
- 选择器权重问题
1.5.3 Dynamic Stylesheet(动态样式)
// 实现原理:应用切换时动态加载/卸载样式
class DynamicStylesheet {
constructor() {
this.styles = []
}
mount(styleNodes) {
styleNodes.forEach(node => {
document.head.appendChild(node)
this.styles.push(node)
})
}
unmount() {
this.styles.forEach(node => {
document.head.removeChild(node)
})
this.styles = []
}
}
// 子应用激活时加载样式
app.mount = async () => {
dynamicStylesheet.mount(styleNodes)
}
// 子应用失活时卸载样式
app.unmount = async () => {
dynamicStylesheet.unmount()
}
优势
- 简单有效
- 性能好
- 兼容性好
劣势
- 切换时有闪烁
- 不适合频繁切换
二、qiankun 实战配置
2.1 主应用配置
完整代码示例
<template>
<div id="main-app">
<!-- 主应用导航 -->
<nav class="main-nav">
<div class="logo">微前端主应用</div>
<ul class="menu">
<li @click="goto('/')">首页</li>
<li @click="goto('/app1')">子应用1</li>
<li @click="goto('/app2')">子应用2</li>
<li @click="goto('/app3')">子应用3</li>
</ul>
<div class="user-info">
<span>用户:{{ username }}</span>
<button @click="logout">退出</button>
</div>
</nav>
<!-- 主应用内容区 -->
<div class="main-content">
<!-- 主应用路由 -->
<router-view v-if="!isSubApp"></router-view>
<!-- 子应用容器 -->
<div id="subapp-container" v-show="isSubApp"></div>
</div>
<!-- 加载提示 -->
<div v-if="loading" class="loading-mask">
<div class="loading-spinner"></div>
<div class="loading-text">加载中...</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, computed } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { registerMicroApps, start, initGlobalState, setDefaultMountApp } from 'qiankun'
const router = useRouter()
const route = useRoute()
const username = ref('Admin')
const loading = ref(false)
// 判断当前是否在子应用路由下
const isSubApp = computed(() => {
return route.path.startsWith('/app1') ||
route.path.startsWith('/app2') ||
route.path.startsWith('/app3')
})
// 初始化全局状态
const actions = initGlobalState({
user: {
name: 'Admin',
role: 'admin'
},
token: 'xxxx-xxxx-xxxx'
})
// 监听状态变化
actions.onGlobalStateChange((state, prev) => {
console.log('全局状态改变', state, prev)
})
// 注册子应用
const microApps = [
{
name: 'app1',
entry: '//localhost:8081',
container: '#subapp-container',
activeRule: '/app1',
props: {
// 传递给子应用的数据
routerBase: '/app1',
getGlobalState: actions.getGlobalState,
setGlobalState: actions.setGlobalState
}
},
{
name: 'app2',
entry: '//localhost:8082',
container: '#subapp-container',
activeRule: '/app2',
props: {
routerBase: '/app2',
getGlobalState: actions.getGlobalState,
setGlobalState: actions.setGlobalState
}
},
{
name: 'app3',
entry: '//localhost:8083',
container: '#subapp-container',
activeRule: '/app3',
props: {
routerBase: '/app3',
getGlobalState: actions.getGlobalState,
setGlobalState: actions.setGlobalState
}
}
]
onMounted(() => {
// 注册微应用
registerMicroApps(microApps, {
beforeLoad: [
app => {
console.log('before load', app.name)
loading.value = true
return Promise.resolve()
}
],
beforeMount: [
app => {
console.log('before mount', app.name)
return Promise.resolve()
}
],
afterMount: [
app => {
console.log('after mount', app.name)
loading.value = false
return Promise.resolve()
}
],
beforeUnmount: [
app => {
console.log('before unmount', app.name)
return Promise.resolve()
}
],
afterUnmount: [
app => {
console.log('after unmount', app.name)
return Promise.resolve()
}
]
})
// 设置默认子应用
// setDefaultMountApp('/app1')
// 启动 qiankun
start({
prefetch: true, // 预加载
sandbox: {
strictStyleIsolation: false, // 严格样式隔离
experimentalStyleIsolation: true // 实验性样式隔离
},
singular: false // 是否单实例场景
})
})
const goto = (path) => {
router.push(path)
}
const logout = () => {
console.log('退出登录')
actions.setGlobalState({ user: null, token: null })
}
</script>
<style scoped>
#main-app {
width: 100%;
height: 100vh;
display: flex;
flex-direction: column;
}
.main-nav {
display: flex;
align-items: center;
padding: 0 24px;
height: 64px;
background: #001529;
color: white;
}
.logo {
font-size: 18px;
font-weight: bold;
margin-right: 50px;
}
.menu {
display: flex;
list-style: none;
margin: 0;
padding: 0;
flex: 1;
}
.menu li {
padding: 0 20px;
cursor: pointer;
transition: all 0.3s;
}
.menu li:hover {
color: #1890ff;
}
.user-info {
display: flex;
align-items: center;
gap: 16px;
}
.user-info button {
padding: 6px 16px;
background: #1890ff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.main-content {
flex: 1;
overflow: auto;
background: #f0f2f5;
}
#subapp-container {
width: 100%;
height: 100%;
}
.loading-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 9999;
}
.loading-spinner {
width: 50px;
height: 50px;
border: 4px solid #f3f3f3;
border-top: 4px solid #1890ff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading-text {
margin-top: 20px;
color: white;
font-size: 16px;
}
</style>
主应用路由配置
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
const routes = [
{
path: '/',
name: 'Home',
component: () => import('../views/Home.vue')
},
// 子应用路由通配
{
path: '/app1/:pathMatch(.*)*',
name: 'App1',
component: () => import('../views/SubApp.vue')
},
{
path: '/app2/:pathMatch(.*)*',
name: 'App2',
component: () => import('../views/SubApp.vue')
},
{
path: '/app3/:pathMatch(.*)*',
name: 'App3',
component: () => import('../views/SubApp.vue')
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
2.2 子应用配置
完整代码示例
// main.js
import { createApp } from 'vue'
import { createRouter, createWebHistory } from 'vue-router'
import App from './App.vue'
import routes from './router'
let instance = null
let router = null
// 渲染函数
function render(props = {}) {
const { container, routerBase } = props
// 创建路由
router = createRouter({
history: createWebHistory(routerBase || '/app1'),
routes
})
// 创建 Vue 实例
instance = createApp(App)
instance.use(router)
// 挂载
const containerEl = container
? container.querySelector('#app')
: document.querySelector('#app')
instance.mount(containerEl)
}
// 判断是否在 qiankun 环境
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)
// 注册全局状态监听
props.onGlobalStateChange?.((state, prev) => {
console.log('[app1] 全局状态变化', state, prev)
})
// 获取全局状态
const globalState = props.getGlobalState?.()
console.log('[app1] 全局状态', globalState)
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)
}
子应用 Webpack 配置
// vue.config.js
const { name } = require('./package.json')
module.exports = {
devServer: {
port: 8081,
headers: {
'Access-Control-Allow-Origin': '*' // 允许跨域
}
},
configureWebpack: {
output: {
library: `${name}-[name]`,
libraryTarget: 'umd', // 把微应用打包成 umd 库格式
jsonpFunction: `webpackJsonp_${name}`
}
}
}
子应用 publicPath 配置
// public-path.js
if (window.__POWERED_BY_QIANKUN__) {
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__
}
// main.js 顶部引入
import './public-path'
2.3 完整运行示例
目录结构
微前端项目
├── main-app/ # 主应用
│ ├── src/
│ │ ├── main.js
│ │ ├── App.vue
│ │ └── router/
│ ├── package.json
│ └── vue.config.js
│
├── sub-app1/ # 子应用1
│ ├── src/
│ │ ├── main.js
│ │ ├── App.vue
│ │ ├── public-path.js
│ │ └── router/
│ ├── package.json
│ └── vue.config.js
│
├── sub-app2/ # 子应用2
└── sub-app3/ # 子应用3
启动命令
# 主应用
cd main-app
npm install
npm run serve # http://localhost:8080
# 子应用1
cd sub-app1
npm install
npm run serve # http://localhost:8081
# 子应用2
cd sub-app2
npm install
npm run serve # http://localhost:8082
# 子应用3
cd sub-app3
npm install
npm run serve # http://localhost:8083
访问 http://localhost:8080 即可看到微前端效果。
三、关键问题解决(下一篇)
由于篇幅限制,关键问题解决、简历撰写等内容将在下一部分详细说明。
包含内容
- 路由劫持与同步
- 全局状态共享
- 样式隔离方案
- 静态资源路径问题
- 子应用独立运行配置
- 简历描述模板
- SOP 标准回答
- 难点与亮点分析