返回笔记首页

Wujie 无界微前端(1)- 原理

主题配置

一、Wujie 核心原理

1.1 技术架构

Wujie 采用了创新的 iframe + Web Component 方案:

plain
Wujie 架构
├── iframe 沙箱
│   ├── 天然 JS 隔离
│   ├── 天然 CSS 隔离
│   └── 完整 DOM 环境
│
├── Web Component 容器
│   ├── 承载子应用 DOM
│   ├── 劫持 document 操作
│   └── 同域通信
│
└── 核心机制
    ├── document 劫持
    ├── location 劫持
    ├── 事件系统
    └── 资源加载

1.2 核心创新点

1. iframe 做沙箱,Web Component 做容器

plain
传统 iframe 方案:
┌─────────────┐
│   主应用    │
│             │
│ ┌─────────┐│
│ │ iframe  ││ ← 子应用在 iframe 内,通信困难
│ │         ││
│ └─────────┘│
└─────────────┘

Wujie 方案:
┌─────────────┐
│   主应用    │
│             │
│ ┌─────────┐│
│ │ Web     ││ ← 子应用 DOM 在 Web Component 内
│ │Component││
│ └─────────┘│
│             │
│ [iframe]   │ ← iframe 隐藏,只提供沙箱环境
└─────────────┘
2. 劫持 iframe 的 document 到主应用
javascript
// 简化版原理

// 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 主应用配置

安装

bash
npm install wujie-vue3
# or
yarn add wujie-vue3

完整代码示例

vue
<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 的优势:子应用零改造

javascript
// 子应用 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 会自动处理一切

如果需要与主应用通信

vue
<!-- 子应用组件 -->
<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 高级配置

预加载配置

javascript
// 主应用 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 // 超时时间
})

保活配置

vue
<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>

路由同步配置

vue
<template>
  <!-- sync=true 开启路由同步 -->
  <WujieVue
    name="app1"
    :url="app1Url"
    :sync="true"
  />
</template>

<script setup>
// 开启后,子应用路由变化会同步到主应用 URL
// 主应用 URL:/#/app1
// 子应用路由:/list
// 实际 URL:/#/app1/list
</script>

2.4 完整的生命周期示例

vue
<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>

三、性能优化(后续章节)

由于篇幅限制,性能优化、简历撰写等内容将继续补充...

待完成内容

  • 性能优化策略
  • 通信机制详解
  • 适用场景分析
  • 简历撰写指南
  • 面试应答话术
  • 难点亮点分析