一、Wujie 核心原理
1.1 技术架构
Wujie 采用了创新的 iframe + Web Component 方案:
Wujie 架构
├── iframe 沙箱
│ ├── 天然 JS 隔离
│ ├── 天然 CSS 隔离
│ └── 完整 DOM 环境
│
├── Web Component 容器
│ ├── 承载子应用 DOM
│ ├── 劫持 document 操作
│ └── 同域通信
│
└── 核心机制
├── document 劫持
├── location 劫持
├── 事件系统
└── 资源加载
1.2 核心创新点
1. iframe 做沙箱,Web Component 做容器
传统 iframe 方案:
┌─────────────┐
│ 主应用 │
│ │
│ ┌─────────┐│
│ │ iframe ││ ← 子应用在 iframe 内,通信困难
│ │ ││
│ └─────────┘│
└─────────────┘
Wujie 方案:
┌─────────────┐
│ 主应用 │
│ │
│ ┌─────────┐│
│ │ Web ││ ← 子应用 DOM 在 Web Component 内
│ │Component││
│ └─────────┘│
│ │
│ [iframe] │ ← iframe 隐藏,只提供沙箱环境
└─────────────┘
2. 劫持 iframe 的 document 到主应用
// 简化版原理
// 1. 创建 iframe(不显示)
const iframe = document.createElement('iframe')
iframe.style.display = 'none'
document.body.appendChild(iframe)
// 2. 创建 Web Component 容器
const shadowRoot = container.attachShadow({ mode: 'open' })
// 3. 劫持 iframe 的 document
const iframeWindow = iframe.contentWindow
const iframeDocument = iframe.contentDocument
// 劫持 document.querySelector 等方法,指向 shadowRoot
Object.defineProperty(iframeWindow, 'document', {
get() {
return new Proxy(iframeDocument, {
get(target, prop) {
if (prop === 'querySelector') {
return (selector) => shadowRoot.querySelector(selector)
}
if (prop === 'getElementById') {
return (id) => shadowRoot.getElementById(id)
}
return target[prop]
}
})
}
})
// 4. 在 iframe 中执行子应用 JS
iframeWindow.eval(subAppJsCode)
// 5. 子应用的 document 操作实际作用在 shadowRoot 上
// 子应用:document.body.appendChild(el)
// 实际:shadowRoot.appendChild(el)
1.3 与 qiankun 的对比
| 特性 | Wujie | qiankun |
|---|---|---|
| JS 隔离 | iframe 天然隔离 | Proxy 沙箱 |
| CSS 隔离 | iframe + Shadow DOM | Shadow DOM / Scoped |
| 子应用改造 | 零改造 | 需要导出生命周期 |
| 通信方式 | props + EventBus | initGlobalState |
| 保活能力 | 支持 | 不支持 |
| 性能 | 略逊(iframe 开销) | 更好 |
| 兼容性 | 好 | 需要 Proxy |
二、Wujie 实战配置
2.1 主应用配置
安装
npm install wujie-vue3
# or
yarn add wujie-vue3
完整代码示例
<template>
<div id="main-app">
<!-- 导航菜单 -->
<nav class="main-nav">
<div class="logo">Wujie 微前端</div>
<ul class="menu">
<li @click="switchApp('home')" :class="{ active: currentApp === 'home' }">
首页
</li>
<li @click="switchApp('app1')" :class="{ active: currentApp === 'app1' }">
子应用1
</li>
<li @click="switchApp('app2')" :class="{ active: currentApp === 'app2' }">
子应用2
</li>
<li @click="switchApp('app3')" :class="{ active: currentApp === 'app3' }">
子应用3
</li>
</ul>
<div class="user-info">
<span>用户:{{ userData.name }}</span>
<button @click="logout">退出</button>
</div>
</nav>
<!-- 内容区 -->
<div class="main-content">
<!-- 主应用页面 -->
<div v-if="currentApp === 'home'" class="home-page">
<h1>欢迎使用 Wujie 微前端</h1>
<p>当前时间:{{ currentTime }}</p>
<div class="stats">
<div class="stat-card">
<h3>子应用数量</h3>
<p class="number">3</p>
</div>
<div class="stat-card">
<h3>加载次数</h3>
<p class="number">{{ loadCount }}</p>
</div>
<div class="stat-card">
<h3>活跃应用</h3>
<p class="number">{{ aliveApps.length }}</p>
</div>
</div>
<div class="actions">
<button @click="preloadApps" class="btn-primary">
预加载所有子应用
</button>
<button @click="destroyApps" class="btn-danger">
销毁所有子应用
</button>
</div>
</div>
<!-- 子应用容器 -->
<WujieVue
v-if="currentApp === 'app1'"
name="app1"
:url="app1Url"
:alive="true"
:props="appProps"
:sync="true"
@mounted="onAppMounted"
@beforeLoad="onBeforeLoad"
@activated="onActivated"
@deactivated="onDeactivated"
/>
<WujieVue
v-if="currentApp === 'app2'"
name="app2"
:url="app2Url"
:alive="false"
:props="appProps"
@mounted="onAppMounted"
/>
<WujieVue
v-if="currentApp === 'app3'"
name="app3"
:url="app3Url"
:alive="true"
:props="appProps"
/>
</div>
<!-- 加载提示 -->
<div v-if="loading" class="loading-mask">
<div class="loading-spinner"></div>
<div class="loading-text">{{ loadingText }}</div>
</div>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, onUnmounted, computed } from 'vue'
import WujieVue from 'wujie-vue3'
import { bus, preloadApp, destroyApp } from 'wujie'
const currentApp = ref('home')
const loading = ref(false)
const loadingText = ref('加载中...')
const loadCount = ref(0)
const aliveApps = ref(['app1', 'app3'])
const currentTime = ref(new Date().toLocaleString())
// 用户数据
const userData = reactive({
name: 'Admin',
role: 'admin',
token: 'xxxx-xxxx-xxxx'
})
// 子应用 URL
const app1Url = import.meta.env.MODE === 'production'
? 'https://cdn.example.com/app1/'
: '//localhost:8081'
const app2Url = import.meta.env.MODE === 'production'
? 'https://cdn.example.com/app2/'
: '//localhost:8082'
const app3Url = import.meta.env.MODE === 'production'
? 'https://cdn.example.com/app3/'
: '//localhost:8083'
// 传递给子应用的数据
const appProps = computed(() => ({
user: userData,
env: import.meta.env.MODE,
basePath: currentApp.value,
// 传递方法
logout: logout,
updateUser: updateUser,
navigate: switchApp
}))
// 切换应用
const switchApp = (app) => {
currentApp.value = app
if (app !== 'home') {
loadCount.value++
}
}
// 子应用生命周期回调
const onBeforeLoad = (appName) => {
console.log('beforeLoad', appName)
loading.value = true
loadingText.value = `正在加载${appName}...`
}
const onAppMounted = (appName) => {
console.log('mounted', appName)
loading.value = false
// 向子应用发送消息
bus.$emit(`${appName}-mounted`, {
timestamp: Date.now(),
message: '主应用通知:挂载完成'
})
}
const onActivated = (appName) => {
console.log('activated', appName)
}
const onDeactivated = (appName) => {
console.log('deactivated', appName)
}
// 预加载所有子应用
const preloadApps = async () => {
loading.value = true
loadingText.value = '预加载中...'
try {
await Promise.all([
preloadApp({ name: 'app1', url: app1Url }),
preloadApp({ name: 'app2', url: app2Url }),
preloadApp({ name: 'app3', url: app3Url })
])
alert('预加载完成!')
} catch (error) {
console.error('预加载失败', error)
alert('预加载失败')
} finally {
loading.value = false
}
}
// 销毁所有子应用
const destroyApps = () => {
if (confirm('确定要销毁所有子应用吗?')) {
destroyApp('app1')
destroyApp('app2')
destroyApp('app3')
currentApp.value = 'home'
alert('已销毁所有子应用')
}
}
// 更新用户信息
const updateUser = (newUser) => {
Object.assign(userData, newUser)
// 通知所有子应用
bus.$emit('user-updated', userData)
}
// 退出登录
const logout = () => {
if (confirm('确定要退出吗?')) {
userData.name = ''
userData.token = ''
// 通知所有子应用
bus.$emit('user-logout')
currentApp.value = 'home'
}
}
// 监听子应用发来的消息
onMounted(() => {
// 监听子应用请求用户信息
bus.$on('request-user-info', (callback) => {
callback(userData)
})
// 监听子应用通知
bus.$on('child-notify', (data) => {
console.log('收到子应用通知', data)
})
// 更新时间
const timer = setInterval(() => {
currentTime.value = new Date().toLocaleString()
}, 1000)
onUnmounted(() => {
clearInterval(timer)
// 清理事件监听
bus.$off('request-user-info')
bus.$off('child-notify')
})
})
</script>
<style scoped>
#main-app {
width: 100%;
height: 100vh;
display: flex;
flex-direction: column;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}
.main-nav {
display: flex;
align-items: center;
padding: 0 24px;
height: 64px;
background: #001529;
color: white;
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
}
.logo {
font-size: 20px;
font-weight: bold;
margin-right: 50px;
}
.menu {
display: flex;
list-style: none;
margin: 0;
padding: 0;
flex: 1;
gap: 8px;
}
.menu li {
padding: 8px 20px;
cursor: pointer;
border-radius: 4px;
transition: all 0.3s;
}
.menu li:hover {
background: rgba(255, 255, 255, 0.1);
}
.menu li.active {
background: #1890ff;
}
.user-info {
display: flex;
align-items: center;
gap: 16px;
}
.user-info button {
padding: 8px 20px;
background: #1890ff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s;
}
.user-info button:hover {
background: #40a9ff;
transform: translateY(-2px);
}
.main-content {
flex: 1;
overflow: auto;
background: #f0f2f5;
}
.home-page {
padding: 40px;
max-width: 1200px;
margin: 0 auto;
}
.home-page h1 {
font-size: 32px;
color: #001529;
margin-bottom: 20px;
}
.stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 24px;
margin: 40px 0;
}
.stat-card {
background: white;
padding: 24px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
text-align: center;
}
.stat-card h3 {
font-size: 14px;
color: #666;
margin-bottom: 12px;
}
.stat-card .number {
font-size: 36px;
color: #1890ff;
font-weight: bold;
margin: 0;
}
.actions {
display: flex;
gap: 16px;
}
.btn-primary,
.btn-danger {
padding: 12px 32px;
border: none;
border-radius: 6px;
font-size: 16px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s;
}
.btn-primary {
background: #1890ff;
color: white;
}
.btn-primary:hover {
background: #40a9ff;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.4);
}
.btn-danger {
background: #ff4d4f;
color: white;
}
.btn-danger:hover {
background: #ff7875;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(255, 77, 79, 0.4);
}
.loading-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 9999;
}
.loading-spinner {
width: 60px;
height: 60px;
border: 6px solid #f3f3f3;
border-top: 6px 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: 18px;
font-weight: 500;
}
</style>
2.2 子应用配置
Wujie 的优势:子应用零改造
// 子应用 main.js - 无需任何改造!
import { createApp } from 'vue'
import { createRouter, createWebHistory } from 'vue-router'
import App from './App.vue'
import routes from './router'
// 正常的 Vue 应用代码
const router = createRouter({
history: createWebHistory('/'),
routes
})
const app = createApp(App)
app.use(router)
app.mount('#app')
// 就这么简单!不需要导出生命周期!
// Wujie 会自动处理一切
如果需要与主应用通信
<!-- 子应用组件 -->
<template>
<div class="sub-app">
<h2>子应用1</h2>
<div class="user-panel">
<h3>主应用用户信息:</h3>
<p>姓名:{{ mainAppUser?.name }}</p>
<p>角色:{{ mainAppUser?.role }}</p>
</div>
<div class="actions">
<button @click="notifyMain">通知主应用</button>
<button @click="requestUserInfo">请求用户信息</button>
<button @click="updateMainUser">更新主应用用户</button>
</div>
<div class="message-log">
<h3>消息日志:</h3>
<div v-for="(msg, index) in messages" :key="index" class="message-item">
{{ msg }}
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
const mainAppUser = ref(null)
const messages = ref([])
// 方式1: 通过 props 获取主应用数据
onMounted(() => {
// Wujie 会把主应用传递的 props 挂载到 window.$wujie.props
if (window.$wujie) {
mainAppUser.value = window.$wujie.props.user
addMessage('从 props 获取到用户信息')
}
})
// 方式2: 使用 EventBus 通信
const notifyMain = () => {
if (window.$wujie) {
window.$wujie.bus.$emit('child-notify', {
from: 'app1',
message: '子应用发来的消息',
timestamp: Date.now()
})
addMessage('已发送消息给主应用')
}
}
const requestUserInfo = () => {
if (window.$wujie) {
window.$wujie.bus.$emit('request-user-info', (user) => {
mainAppUser.value = user
addMessage('收到主应用用户信息')
})
}
}
const updateMainUser = () => {
if (window.$wujie?.props?.updateUser) {
window.$wujie.props.updateUser({
name: 'Updated from Child',
role: 'super-admin'
})
addMessage('已更新主应用用户信息')
}
}
// 监听主应用消息
onMounted(() => {
if (window.$wujie) {
// 监听用户更新
window.$wujie.bus.$on('user-updated', (user) => {
mainAppUser.value = user
addMessage('主应用用户信息已更新')
})
// 监听用户登出
window.$wujie.bus.$on('user-logout', () => {
mainAppUser.value = null
addMessage('用户已登出')
})
// 监听挂载完成消息
window.$wujie.bus.$on('app1-mounted', (data) => {
addMessage(`收到主应用消息:${data.message}`)
})
}
onUnmounted(() => {
if (window.$wujie) {
window.$wujie.bus.$off('user-updated')
window.$wujie.bus.$off('user-logout')
window.$wujie.bus.$off('app1-mounted')
}
})
})
const addMessage = (msg) => {
messages.value.unshift(`[${new Date().toLocaleTimeString()}] ${msg}`)
if (messages.value.length > 10) {
messages.value.pop()
}
}
</script>
<style scoped>
.sub-app {
padding: 24px;
}
h2 {
color: #001529;
margin-bottom: 24px;
}
.user-panel,
.message-log {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
margin-bottom: 20px;
}
.user-panel h3,
.message-log h3 {
font-size: 16px;
color: #333;
margin-bottom: 12px;
}
.user-panel p {
margin: 8px 0;
color: #666;
}
.actions {
margin: 20px 0;
display: flex;
gap: 12px;
}
.actions button {
padding: 10px 24px;
background: #1890ff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s;
}
.actions button:hover {
background: #40a9ff;
transform: translateY(-2px);
}
.message-item {
padding: 8px;
background: #f0f2f5;
border-radius: 4px;
margin-bottom: 8px;
font-size: 13px;
color: #666;
}
</style>
2.3 高级配置
预加载配置
// 主应用 main.js
import { setupApp, preloadApp } from 'wujie'
// 全局配置
setupApp({
// 生命周期
beforeLoad: (appWindow) => console.log('beforeLoad', appWindow),
beforeMount: (appWindow) => console.log('beforeMount', appWindow),
afterMount: (appWindow) => console.log('afterMount', appWindow),
beforeUnmount: (appWindow) => console.log('beforeUnmount', appWindow),
afterUnmount: (appWindow) => console.log('afterUnmount', appWindow),
activated: (appWindow) => console.log('activated', appWindow),
deactivated: (appWindow) => console.log('deactivated', appWindow),
// 全局 props
props: {
env: 'production'
},
// 插件
plugins: [
{
// CSS 排除规则
cssExcludes: ['https://cdn.example.com/ignore.css'],
// JS 排除规则
jsExcludes: ['https://cdn.example.com/ignore.js'],
// HTML loader
htmlLoader: (code) => {
// 可以在这里修改 HTML
return code.replace('old', 'new')
},
// CSS loader
cssLoader: (code) => {
// 可以在这里修改 CSS
return code
},
// JS loader
jsLoader: (code) => {
// 可以在这里修改 JS
return code
}
}
]
})
// 预加载子应用
preloadApp({
name: 'app1',
url: '//localhost:8081',
// 预加载时机
exec: true, // 是否执行 JS
fetch: (url) => fetch(url), // 自定义 fetch
timeout: 10000 // 超时时间
})
保活配置
<template>
<!-- alive=true 开启保活 -->
<WujieVue
name="app1"
:url="app1Url"
:alive="true"
@activated="onActivated"
@deactivated="onDeactivated"
/>
</template>
<script setup>
// 保活模式下,切换应用不会销毁,状态会保留
const onActivated = () => {
console.log('应用激活,状态保留')
}
const onDeactivated = () => {
console.log('应用失活,状态保留')
}
</script>
路由同步配置
<template>
<!-- sync=true 开启路由同步 -->
<WujieVue
name="app1"
:url="app1Url"
:sync="true"
/>
</template>
<script setup>
// 开启后,子应用路由变化会同步到主应用 URL
// 主应用 URL:/#/app1
// 子应用路由:/list
// 实际 URL:/#/app1/list
</script>
2.4 完整的生命周期示例
<template>
<div class="lifecycle-demo">
<h2>Wujie 生命周期演示</h2>
<div class="lifecycle-log">
<div
v-for="(log, index) in lifecycleLogs"
:key="index"
class="log-item"
:class="log.type"
>
<span class="log-time">{{ log.time }}</span>
<span class="log-phase">{{ log.phase }}</span>
<span class="log-message">{{ log.message }}</span>
</div>
</div>
<div class="controls">
<button @click="loadApp">加载应用</button>
<button @click="destroyApp">销毁应用</button>
<button @click="clearLogs">清空日志</button>
</div>
<WujieVue
v-if="showApp"
name="lifecycle-demo"
:url="appUrl"
:alive="alive"
@beforeLoad="onBeforeLoad"
@beforeMount="onBeforeMount"
@afterMount="onAfterMount"
@beforeUnmount="onBeforeUnmount"
@afterUnmount="onAfterUnmount"
@activated="onActivated"
@deactivated="onDeactivated"
@loadError="onLoadError"
/>
</div>
</template>
<script setup>
import { ref } from 'vue'
import WujieVue from 'wujie-vue3'
import { destroyApp as wujieDestroyApp } from 'wujie'
const showApp = ref(false)
const alive = ref(false)
const appUrl = '//localhost:8081'
const lifecycleLogs = ref([])
const addLog = (phase, message, type = 'info') => {
lifecycleLogs.value.unshift({
time: new Date().toLocaleTimeString(),
phase,
message,
type
})
}
const onBeforeLoad = (appWindow) => {
addLog('beforeLoad', '开始加载子应用', 'info')
console.log('beforeLoad', appWindow)
}
const onBeforeMount = (appWindow) => {
addLog('beforeMount', '开始挂载子应用 DOM', 'info')
console.log('beforeMount', appWindow)
}
const onAfterMount = (appWindow) => {
addLog('afterMount', '子应用挂载完成', 'success')
console.log('afterMount', appWindow)
}
const onBeforeUnmount = (appWindow) => {
addLog('beforeUnmount', '开始卸载子应用', 'warning')
console.log('beforeUnmount', appWindow)
}
const onAfterUnmount = (appWindow) => {
addLog('afterUnmount', '子应用卸载完成', 'warning')
console.log('afterUnmount', appWindow)
}
const onActivated = (appWindow) => {
addLog('activated', '子应用激活(保活模式)', 'success')
console.log('activated', appWindow)
}
const onDeactivated = (appWindow) => {
addLog('deactivated', '子应用失活(保活模式)', 'warning')
console.log('deactivated', appWindow)
}
const onLoadError = (url, error) => {
addLog('loadError', `加载失败:${error.message}`, 'error')
console.error('loadError', url, error)
}
const loadApp = () => {
showApp.value = true
addLog('user-action', '用户点击加载应用', 'info')
}
const destroyApp = () => {
wujieDestroyApp('lifecycle-demo')
showApp.value = false
addLog('user-action', '用户销毁应用', 'warning')
}
const clearLogs = () => {
lifecycleLogs.value = []
}
</script>
<style scoped>
.lifecycle-demo {
padding: 24px;
}
.lifecycle-log {
background: #1e1e1e;
padding: 16px;
border-radius: 8px;
max-height: 400px;
overflow-y: auto;
margin: 20px 0;
font-family: 'Consolas', 'Monaco', monospace;
}
.log-item {
padding: 8px;
margin-bottom: 4px;
border-radius: 4px;
font-size: 13px;
display: flex;
gap: 12px;
}
.log-item.info {
background: rgba(24, 144, 255, 0.1);
color: #1890ff;
}
.log-item.success {
background: rgba(82, 196, 26, 0.1);
color: #52c41a;
}
.log-item.warning {
background: rgba(250, 173, 20, 0.1);
color: #faad14;
}
.log-item.error {
background: rgba(255, 77, 79, 0.1);
color: #ff4d4f;
}
.log-time {
color: #888;
min-width: 80px;
}
.log-phase {
font-weight: bold;
min-width: 120px;
}
.log-message {
flex: 1;
}
.controls {
display: flex;
gap: 12px;
margin: 20px 0;
}
.controls button {
padding: 10px 24px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s;
}
.controls button:nth-child(1) {
background: #52c41a;
color: white;
}
.controls button:nth-child(2) {
background: #ff4d4f;
color: white;
}
.controls button:nth-child(3) {
background: #666;
color: white;
}
.controls button:hover {
opacity: 0.8;
transform: translateY(-2px);
}
</style>
三、性能优化(后续章节)
由于篇幅限制,性能优化、简历撰写等内容将继续补充...
待完成内容
- 性能优化策略
- 通信机制详解
- 适用场景分析
- 简历撰写指南
- 面试应答话术
- 难点亮点分析