Files
microapp-main-interview/docs/micro-app面试题.md
2026-06-21 20:19:21 +08:00

38 KiB
Raw Blame History

@micro-zoe/micro-app 微前端面试题

基于 Vue 3 + Vite 主应用搭建实战经验整理,涵盖从项目初始化、配置、到 bug 排查的全流程。


一、基础概念

Q1什么是微前端micro-app 相比其他方案有什么优势?

答: 微前端是一种将多个独立的前端应用组合成一个统一应用的架构模式。每个子应用可以独立开发、独立部署、使用不同的技术栈。

@micro-zoe/micro-app(京东出品)的核心优势:

  • 零依赖:不依赖 single-spa完全自研
  • 类 WebComponent:使用 <micro-app> 标签加载子应用,对开发者透明
  • 双沙箱模式:同时支持 with 沙箱和 iframe 沙箱
  • 不限制技术栈主应用和子应用可以任意组合Vue 2/3、React、原生 HTML 等)
  • 兼容 Vite:通过 iframe 模式完美支持 Vite 构建的子应用
对比维度 micro-app qiankun
接入成本 低(类 WebComponent 中(需改造子应用入口)
Vite 支持 原生支持iframe 模式) 需额外配置
沙箱方案 with + iframe 双模式 基于 Proxy 的沙箱
包体积 ~300KB ~100KB

Q2micro-app 主应用和子应用之间是如何隔离的?

答: micro-app 提供三层隔离体系,各管各的维度,可以组合使用:

┌──────────────────────────────────────────────────────────────┐
│                    micro-app 隔离体系                          │
│                                                              │
│  ① CSS 样式隔离默认开启disableScopecss: false            │
│     └── 方式CSS 前缀改写,不是 Shadow DOM                   │
│          子应用写了 h2 { color: red }                          │
│          micro-app 提取 CSS 后自动改成                          │
│          micro-app[name=xxx] h2 { color: red }                │
│                                                              │
│  ② JS 沙箱默认开启disableSandbox: false                  │
│     ├── with 沙箱new Function() + Proxy(window)             │
│     └── iframe 沙箱:浏览器原生 iframe 隔离                    │
│                                                              │
│  ③ 元素隔离默认关闭shadowDOM: false                      │
│     └── 浏览器原生 Shadow DOMDOM 树级别隔离                  │
│                                                              │
└──────────────────────────────────────────────────────────────┘

一、CSS 样式隔离 — CSS 前缀改写(默认方案)

工作原理:

/* 子应用源码 */
h2 { color: orange; }

/* micro-app 自动改写为 ↓ */
micro-app[name=mock-app] h2 { color: orange; }

micro-app 在加载子应用时,会提取所有 <style> 标签内容,给每个 CSS 规则自动加上 micro-app[name=xxx] 属性选择器前缀,限制样式只作用于子应用容器内部。

⚠️ 关键局限:只能阻止「子→主」,不能阻止「主→子」

子应用样式泄漏到主应用(子→主):✅ CSS 前缀改写可以阻止
  ──────────────────────────────────────────────
  子应用: h2 { color: orange }
  → 改写后: micro-app[name=mock-app] h2 { color: orange }
  → 主应用的 h2 没有 micro-app[name=mock-app] 父级 → 不匹配 ✅

主应用样式泄漏到子应用(主→子):❌ CSS 前缀改写无法阻止
  ──────────────────────────────────────────────
  主应用: .app-main h2 { border-bottom: 2px solid purple; }
  → 子应用 h2 也在 .app-main 内部 → 匹配 ❌
  → 需要额外手段::not(.child-app-wrapper) / iframe / shadowDOM

这就是我们在 App.vue 中遇到的问题 — 加了 .app-main 前缀还不够,因为子应用 DOM 就嵌在 .app-main 里面。


二、JS 沙箱 — with 沙箱 vs iframe 沙箱

维度 with 沙箱 (iframe: false) iframe 沙箱 (iframe: true)
原理 with(window.proxy) + new Function() 浏览器原生 iframe
子应用 window Proxy 代理的 microWindow 真实的 iframe.contentWindow
ES Module 不支持 import/export 原生支持
适用 Webpack UMD 子应用 Vite 子应用
性能 较快 略慢(多一层 iframe 渲染)
共享内存 同一 document可传引用 跨 document必须 postMessage 序列化

with 沙箱的执行过程:

// micro-app 内部简化逻辑
const code = await fetchJs()  // 拉取子应用 JS 文本

const fn = new Function('window', `
  with(window) {
    ${code}   // 子应用代码拼接在这里
  }
`)
fn(microWindow)  // microWindow = new Proxy(realWindow, handler)

子应用代码里的 window.xxx / document.xxx 等全局操作,都会被 with(microWindow) 劫持到 Proxy 代理上,实现对全局变量的拦截和隔离。

为什么 Vite 子应用必须用 iframe Vite 开发环境输出 <script type="module"> 格式的 ES Moduleimport/exportnew Function() 无法执行模块语法(import 只能出现在模块静态顶层)。详见 Q8-1。


三、元素隔离 — Shadow DOM可选功能

⚠️ 注意Shadow DOM 不是默认的样式隔离方式!默认方式是 CSS 前缀改写。

<micro-app name="mock-app" :shadowDOM="true" />

开启后,子应用的 DOM 被包裹在 Shadow Root 里:

<micro-app name="mock-app">
  #shadow-root (open)          ← Shadow DOM 边界
    <style>h2 { color: orange; }</style>
    <h2>子应用标题</h2>          ← 样式天然隔离,内外互不穿透
</micro-app>

四、三种隔离方案对比CSS 前缀 vs Shadow DOM vs iframe

CSS 前缀改写 Shadow DOM iframe 沙箱
配置 默认,无需配置 shadowDOM: true iframe: true
样式隔离方向 子→主 / 主→子 双向 双向
JS 隔离 依赖沙箱配置 依赖沙箱配置 原生隔离
ES Module 不支持with 沙箱) 不支持with 沙箱) 支持
DOM 树 共享 独立 Shadow Tree 独立 Document
通信 同 window直接引用 同 window直接引用 postMessage序列化
兼容性 最好 部分 UI 库不兼容 最好
全局样式污染 会(主→子) 不会 不会
适用场景 Webpack 子应用 纯样式隔离需求 Vite 子应用、完整隔离
隔离强度递增 →

 CSS 前缀改写          Shadow DOM            iframe 沙箱
 ┌──────────┐      ┌──────────────┐      ┌──────────────┐
 │ 子→主 ✅  │      │ 子→主 ✅      │      │ 子→主 ✅      │
 │ 主→子 ❌  │      │ 主→子 ✅      │      │ 主→子 ✅      │
 │ JS 部分   │      │ JS 部分       │      │ JS 完全 ✅   │
 │ DOM 共享  │      │ DOM 隔离      │      │ 全部隔离     │
 └──────────┘      └──────────────┘      └──────────────┘

选择决策树:

子应用是 Vite 构建?
  ├── 是 → iframe: true必须ES Module 限制)
  └── 否Webpack
       ├── 只需样式隔离 → shadowDOM: true可选
       ├── 需完整 JS 隔离 → iframe: true
       └── 隔离要求不高 → 默认配置CSS 前缀改写)
            └── 注意主→子穿透!主应用样式需用 :not() 排除子应用容器

Q2-1为什么 CSS 前缀改写只能阻止「子→主」而不能阻止「主→子」?

答:

这是 CSS 前缀改写的单向性决定的:

子→主(子应用样式泄漏到主应用):
  子应用 CSS: h2 { color: orange }
        ↓ micro-app 改写
  改写后: micro-app[name=mock-app] h2 { color: orange }
        ↓
  主应用的 h2 的父级链中没有 micro-app[name=mock-app] → 不匹配 ✅

主→子(主应用样式泄漏到子应用):
  主应用 CSS: .app-main h2 { border-bottom: 2px solid purple; }
        ↓ 没有任何改写!原样生效
  子应用 h2 的祖先链中有 .app-main → 匹配 ❌

核心原因micro-app 只改写子应用的 CSS,不改写主应用的 CSS。主应用的样式规则在全 document 范围内照常匹配,而 iframe: false 模式下子应用 DOM 就在这个 document 里。

三种解决思路

  1. 主应用 CSS 加 :not(.child-app-wrapper) 排除子应用容器(本项目方案)
  2. 开启 shadowDOM: true — Shadow DOM 边界双向阻断
  3. 开启 iframe: true — 独立 document双向天然隔离

二、项目搭建

Q3使用 Vue 3 + Vite 搭建 micro-app 主应用需要哪些步骤?

答: 核心 4 步:

// 1. 安装依赖
npm install @micro-zoe/micro-app vue-router@4

// 2. vite.config.ts — 将 <micro-app> 注册为自定义元素
export default defineConfig({
  plugins: [
    vue({
      template: {
        compilerOptions: {
          isCustomElement: (tag) => /^micro-app/.test(tag)
        }
      }
    })
  ],
  server: { port: 8080 }
})

// 3. main.ts — 在 Vue 实例化前启动 micro-app
import microApp from '@micro-zoe/micro-app'
microApp.start({ /* 全局配置 */ })
createApp(App).use(router).mount('#app')

// 4. 页面中使用 <micro-app> 标签加载子应用
<micro-app name="my-app" url="http://localhost:3000/" baseroute="/my-app" />

Q4为什么要在 vite.config.ts 中配置 isCustomElement

答: <micro-app> 是一个自定义 HTML 标签,不是 Vue 组件。Vue 模板编译器默认会把所有非标准 HTML 标签当作 Vue 组件去寻找,找不到就会报警告:

[Vue warn]: Failed to resolve component: micro-app

通过 isCustomElement: (tag) => /^micro-app/.test(tag) 告诉编译器:以 micro-app 开头的标签是自定义元素,跳过 Vue 组件解析。

延伸:如果子应用中也使用了 <micro-app>(嵌套微前端),同样需要配置。


Q5主应用路由如何设计才能正确匹配子应用

答: 使用 Vue Router 的通配符 :page* 匹配子应用所有内部路由:

const routes = [
  { path: '/', redirect: '/home' },
  { path: '/home', component: () => import('@/views/Home.vue') },
  // 关键::page* 匹配 /child-app 及其下所有子路由
  { path: '/child-app/:page*', component: () => import('@/views/ChildApp.vue') }
]

为什么用 :page* 而不是 *

  • * 只在 Vue Router 3 中可用
  • Vue Router 4 中必须使用 :page*(或 :pathMatch(.*)*)来匹配多级路径
  • 这样 /child-app/page1/child-app/a/b/c 都能被正确捕获

三、沙箱机制(重点 Bug 来源)

Q6micro-app 的两种沙箱模式有什么区别?如何选择?

答:

特性 with 沙箱 iframe 沙箱
配置 iframe: false(默认) iframe: true
原理 with(window.proxy) + new Function() 浏览器原生 iframe
ES Module 不支持 import/export 原生支持
性能 较快(无额外渲染) 略慢(需创建 iframe
隔离性 中等Proxy 代理) 强(浏览器原生隔离)
调试体验 较好(同 document 较差(跨 iframe
适用场景 Webpack 构建的 UMD 应用 Vite 构建的应用

选择原则:

  • Vite 子应用 → 必须 iframe: true
  • Webpack UMD 子应用 → iframe: false 即可
  • 对样式/JS 隔离要求极高的场景 → iframe: true

Q7遇到 Cannot use import statement outside a module 报错,是什么原因?如何解决?

答: 这是 micro-app 主应用对接 Vite 子应用时必踩的坑

报错堆栈:

[micro-app] app vue2-app: {
  error: SyntaxError: Cannot use import statement outside a module
  at new Function (<anonymous>)
}

根因分析:

  1. Vite 开发服务器输出的 JS 是 <script type="module"> 格式的 ES Module
  2. micro-app 默认的 with 沙箱 使用 new Function() 来执行子应用的 JS 代码
  3. new Function() 创建的函数运行在「非模块」作用域,无法解析 import/export 语法

解决方案:

// src/config/subApps.ts
{
  name: 'vue2-app',
  url: 'http://localhost:5173/',
  baseroute: '/child-app',
  iframe: true,  // ← 关键:开启 iframe 沙箱
}

为什么 iframe 能解决? iframe 是浏览器原生的独立文档环境Vite 的 <script type="module"> 在 iframe 中可以正常执行。


Q8new Function()eval() 有什么区别?为什么 micro-app 用 new Function()

答:

特性 eval() new Function()
作用域 访问当前局部作用域 只能访问全局作用域
性能 慢(影响局部变量优化) 较快(引擎可优化)
安全性 较低(泄漏局部变量) 较高(作用域隔离)

micro-app 选择 new Function() 的原因:

  • 它创建的函数只在全局作用域执行,配合 with(window.proxy) 可以精确控制子应用对全局变量的访问
  • eval() 会渗透到调用方的局部变量,无法做到完全隔离

但局限性也很明显: new Function() 无法处理 ES Module 语法,这就是为什么 Vite 子应用必须使用 iframe 模式。


Q8-1为什么 Vite 子应用必须用 iframe: true,而 Webpack 子应用不需要?

答:

核心原因在于 Vite 和 Webpack 输出的 JS 代码格式不同,而 micro-app 的 with 沙箱只能执行某种格式。

一、开发环境Dev Server的差异

Vite Dev Server Webpack Dev Server
输出的 JS 格式 <script type="module"> ES Module <script> IIFE / UMD bundle
import/export
文件数量 按需加载,可能几百个 一个 bundle或少量 chunk
<!-- Vite 开发环境输出的 HTML -->
<script type="module" src="/src/main.ts"></script>
<!-- main.ts 内部还有上百个 import -->
<!-- import { createApp } from 'vue'           ← ES Module 语法 -->
<!-- import router from './router'             ← ES Module 语法 -->

<!-- Webpack 开发环境输出的 HTML -->
<script src="/js/app.js"></script>
<!-- app.js 是一个巨大的 IIFE 包裹的 bundle -->
<!-- (function(modules) { ... })({...})        ← 没有 import/export -->

二、with 沙箱的执行过程

micro-app 用 new Function() 执行子应用的 JS

// micro-app 内部简化逻辑with 沙箱模式)
const code = await fetchJsFromChildApp()  // 拉取子应用的 JS 文本

// ⚠️ 这里执行 JS
const fn = new Function('window', `
  with(window) {
    ${code}    // ← 子应用的代码直接拼接在这里
  }
`)
fn(microWindow)  // microWindow 是 Proxy 代理的隔离 window

问题来了: 如果 code 包含 import/export

// 子应用代码
import { createApp } from 'vue'  // ❌ SyntaxError!
//     ↑ import 只能出现在模块顶层,不能出现在 new Function() 内部

浏览器的模块系统要求 import 必须出现在 <script type="module"> 或 ES Module 文件的静态顶层,不能被 new Function() / eval() 动态执行。

三、为什么 Webpack 可以不用 iframe

Webpack 构建流程:
  import { ref } from 'vue'    webpack打包    __webpack_require__('./vue')
  import router from './router'  ──────────→  __webpack_require__('./router')
  ↑ ES Module 语法                              ↑ 普通函数调用

Vite 开发环境:
  import { ref } from 'vue'    不打包!      import { ref } from 'vue'
  import router from './router' ──────────→  import router from './router'
  ↑ ES Module 语法                             ↑ 仍然是 ES Module 语法
  • Webpack 在构建时就把 import 转成了 __webpack_require__() 这种普通函数调用,输出的是 IIFE 格式,new Function() 可以正常执行
  • Vite 开发模式 不做打包,直接输出浏览器原生的 import/export,只能通过 iframe 的原生模块环境执行

四、一张图总结

                    子应用 JS 包含 import/export
                           │
                    ┌──────┴──────┐
                    │             │
                   否             是
              (Webpack)       (Vite)
                    │             │
                    ▼             ▼
           new Function()    iframe 沙箱
           可直接执行 ✅     唯一可行方案 ✅

五、面试追问Vite 生产环境构建后还需要 iframe 吗?

生产环境 Vite 用 Rollup 打包,输出的是普通 JSimport),理论上用 with 沙箱也能执行。但仍然推荐 iframe: true,原因:

  1. 一致性:开发/生产行为一致,避免"开发没问题,上线炸了"
  2. 隔离性iframe 提供更彻底的样式/JS 隔离
  3. 资源路径iframe 中的相对路径自动指向子应用 origin不用额外处理

四、TypeScript 类型问题

Q9micro-app 的 lifeCycles 回调中,为什么 e.name 会报 TS 类型错误?

答: 这是 TypeScript 类型定义与实际使用的差异问题。

错误代码:

lifeCycles: {
  created(e) {
    console.log(e.name) // TS2339: Property 'name' does not exist on type 'CustomEvent<any>'
  }
}

根因: 根据 @micro-zoe/micro-app 的类型定义文件 global.d.ts

interface lifeCyclesType {
  created?(e: CustomEvent, appName: string): void
  beforemount?(e: CustomEvent, appName: string): void
  // ...
}

生命周期回调接收两个参数:第一个是 CustomEvent 事件对象,第二个才是 appName 字符串

正确写法:

lifeCycles: {
  created(_e, appName) {
    console.log(`子应用 ${appName} 被创建`)
  }
}

Q10vue-tsc --noEmit 在 CI/CD 中有什么作用?我们遇到了什么类型错误?

答: vue-tsc --noEmit 是 Vue 项目的类型检查命令:

  • 只做类型检查,不输出编译产物
  • 相当于 tsc --noEmit,但额外支持 .vue 文件

我们在项目中遇到了两类类型错误:

  1. lifeCycles 回调参数类型错误e.name 不存在于 CustomEvent 类型 → 改用第二个参数 appName
  2. 未使用的导入getSubAppConfigChildApp.vue 中导入但未使用 → 移除无用导入

配置建议:package.json 中将类型检查加入构建流程:

{
  "scripts": {
    "build": "vue-tsc --noEmit && vite build"
  }
}

五、子应用容器与通信

Q11<micro-app> 标签有哪些核心属性?各有什么作用?

答:

属性 类型 说明
name string 必填,子应用唯一标识,字母开头
url string 必填子应用入口地址HTML 路径)
baseroute string 路由前缀,主应用分配给子应用的路由基路径
iframe boolean 是否使用 iframe 沙箱Vite 子应用必须开启)
keep-alive boolean 保活子应用,切换路由时不销毁
router-mode string 路由模式:native(原生同步)/ native-scope(带作用域)
disable-scopecss boolean 禁用样式隔离
disable-sandbox boolean 禁用 JS 沙箱(调试时可用)
data object 向子应用传递的初始数据
shadowDOM boolean 是否使用 Shadow DOM

Q12主应用如何向子应用传递数据有哪几种方式

答: 三种方式:

方式一:通过 data 属性(初始化传参)

<micro-app name="app1" url="..." :data="{ token: 'abc', userId: 1 }" />
// 子应用接收
const data = window.microApp?.getData()

方式二:通过 microApp.setData() API动态传参

import microApp from '@micro-zoe/micro-app'
microApp.setData('app1', { path: '/detail' })
// 子应用监听
window.microApp?.addDataListener((data) => {
  console.log('收到数据:', data)
  if (data.path) router.push(data.path)
}, true) // autoTrigger = true初始化时立即触发一次

方式三:通过 EventCenterForMicroApp(双向通信)

// 主应用
import { EventCenterForMicroApp } from '@micro-zoe/micro-app'
const eventCenter = new EventCenterForMicroApp('app1')
eventCenter.dispatch({ type: '通知', payload: {} })
// 子应用
window.microApp?.dispatch({ type: '回复', payload: {} })

Q12-1iframe: true 模式下,通信的底层原理是什么?和 iframe: false 有什么不同?

答:

API 层面完全一致 — 无论 iframe: true 还是 iframe: false,主应用和子应用都用同一套 APIsetDatagetDatadispatchaddDataListener)。框架在底层封装了通信差异。

底层机制对比:

iframe: falsewith 沙箱)              iframe: true浏览器原生隔离
┌──────────────────────┐              ┌──────────────────────────┐
│ 主应用 document        │              │ 主应用 document            │
│  window (共享)         │              │  window                    │
│  ├── micro-app         │              │  ├── micro-app             │
│  │   └── 子应用 DOM    │              │  │   └── <iframe>          │
│  │       window ✅ 同一个│              │  │       └── 子应用 DOM   │
│  │       内存引用传递   │              │  │            window (独立) │
│  └── ...               │              │  └── ...                  │
│  直接读写 JS 对象       │              │  跨域边界,必须序列化       │
└──────────────────────┘              └──────────────────────────┘

iframe 模式下的通信链路:

主应用                           iframe 子应用
  │                                  │
  │ microApp.setData('app', data)    │
  │   ↓ JSON.stringify(data)         │
  │   ↓ iframe.contentWindow         │
  │     .postMessage(data, '*')       │
  │ ───────── postMessage ────────→  │
  │                                  │ window.addEventListener
  │                                  │   ('message', handler)
  │                                  │   ↓ JSON.parse
  │                                  │   ↓ 触发 dataListener
  │                                  │
  │              window.parent       │
  │ ←──────── postMessage ────────  │
  │ addEventListener('message')      │ microApp.dispatch(msg)
  │   ↓ JSON.parse                   │
  │   ↓ 触发 dataListener            │

核心步骤:

  1. 主→子microApp.setData() → 框架 JSON.stringify(data)iframe.contentWindow.postMessage(serialized, '*') → 子应用 window.addEventListener('message')JSON.parse → 触发 dataListener

  2. 子→主window.microApp.dispatch(msg) → 框架 JSON.stringify(msg)window.parent.postMessage(serialized, '*') → 主应用 window.addEventListener('message')JSON.parse → 触发 dataListener

关键限制postMessage 的"结构化克隆"

✅ 可以传:                  ❌ 不能传:
  - string/number/boolean     - 函数 / class
  - 普通 Object / Array       - DOM 节点 / Event
  - Date / RegExp             - Symbol
  - Map / Set                 - WeakMap / WeakSet
  - ArrayBuffer / Blob        - Proxy 对象

这和 iframe: false 模式有本质区别 — with 沙箱下主应用和子应用共享同一个 window,可以传内存引用(对象改了,两边都能看到),而 iframe 模式走的是序列化拷贝(传完之后各是各的副本)。

实战中的坑:

// ❌ iframe 模式下这样传会丢失数据
microApp.setData('app', {
  callback: () => {},       // 函数 → 序列化后丢失
  element: document.body,   // DOM 节点 → 序列化后丢失
})

// ✅ 正确做法:只传纯数据
microApp.setData('app', {
  token: 'abc123',
  userId: 42,
  theme: 'dark',
})

对比总结:

维度 iframe: false iframe: true
通信机制 直接内存引用 postMessage 序列化
传引用 对象共享 序列化拷贝
传函数 可以 丢失
性能 高(无序列化开销) JSON 序列化)
数据大小 不受限 受 postMessage 限制
API 一致性 完全相同 完全相同
调试 同 console 需切换 iframe context

Q13keep-alive 保活机制的原理是什么?有什么注意事项?

答: 原理:当 <micro-app> 元素从 DOM 中移除时(如路由切换),不执行 unmount(),而是调用 hiddenKeepAliveApp() 将子应用隐藏。当元素重新插入 DOM 时,调用 showKeepAliveApp() 恢复显示。

优点: 避免重复加载资源,保持子应用状态

注意事项:

  • 保活的子应用仍然占用内存,需注意内存泄漏
  • 子应用的定时器、事件监听需要在 unmount 生命周期中手动清理
  • microApp.getActiveApps({ excludeHiddenApp: false }) 可以查看所有活跃应用(包括隐藏的保活应用)

六、生命周期

Q14micro-app 子应用加载经历了哪些生命周期?顺序是什么?

答:

主应用生命周期(全局 lifeCycles
created → beforemount → (beforeshow) → mounted → (afterhidden) → unmount

子应用内部生命周期window 上的钩子):
window.mount() → window.unmount()

完整流程:

阶段 全局回调 说明
资源加载完成 created HTML 和静态资源已下载
挂载前 beforemount 沙箱准备就绪,即将渲染
挂载前keep-alive 恢复) beforeshow 保活应用恢复显示前触发
渲染完成 mounted 子应用已渲染到容器中
保活隐藏后 afterhidden 保活应用被隐藏后触发
卸载 unmount 子应用被销毁
报错 error 加载或执行出错时触发

Q15全局 lifeCycles 和子应用内部的 mount/unmount 有什么区别?

答:

维度 全局 lifeCycles 子应用 mount/unmount
定义位置 主应用 microApp.start() 子应用 window['micro-app-xxx']
关注点 框架层面的加载状态 子应用业务层面的渲染状态
参数 (e: CustomEvent, appName: string) 无参数(或接收主应用传递的配置)
典型用途 全局 loading 控制、埋点 初始化 Vue Router、挂载根组件
// 子应用Vue 2入口 — 导出生命周期
if (window.__MICRO_APP_ENVIRONMENT__) {
  window[`micro-app-${window.__MICRO_APP_NAME__}`] = {
    mount() {
      instance = new Vue({ router, render: h => h(App) }).$mount('#child-app-root')
    },
    unmount() {
      instance.$destroy()
      instance.$el.innerHTML = ''
    }
  }
} else {
  // 独立运行时直接渲染
  new Vue({ router, render: h => h(App) }).$mount('#app')
}

七、Vue 2 子应用对接

Q16Vue 2 Webpack 子应用需要做哪些改造才能接入 micro-app

答: 需要 4 处改造:

1. webpack 配置 — UMD 输出

// vue.config.js 或 webpack.config.js
module.exports = {
  output: {
    libraryTarget: 'umd',
    library: 'vue2App'
  }
}

2. devServer 配置 — 跨域

module.exports = {
  devServer: {
    port: 3000,
    headers: {
      'Access-Control-Allow-Origin': '*'
    }
  }
}

3. 入口文件 — 导出生命周期

let instance = null

function mount() {
  instance = new Vue({ router, render: h => h(App) }).$mount('#child-app-root')
}

function unmount() {
  instance.$destroy()
  instance.$el.innerHTML = ''
}

if (window.__MICRO_APP_ENVIRONMENT__) {
  window[`micro-app-${window.__MICRO_APP_NAME__}`] = { mount, unmount }
} else {
  mount()
}

4. 路由 base — 动态适配

const router = new VueRouter({
  mode: 'history',
  base: window.__MICRO_APP_BASE_ROUTE__ || '/',
  routes
})

Q17子应用挂载的 DOM 元素 id 为什么不能用 #app

答: 因为主应用已经使用了 <div id="app"></div>。如果子应用也挂载到 #app,会与主应用的根节点冲突,导致:

  • 子应用覆盖主应用内容
  • 样式污染
  • 路由混乱

正确做法: 子应用使用独立的 id

<!-- 子应用的 index.html -->
<div id="child-app-root"></div>
// 子应用入口
new Vue({ ... }).$mount('#child-app-root')

八、排错与调试

Q18子应用加载白屏如何排查

答: 按以下顺序排查:

  1. 检查跨域:浏览器 Network 面板看子应用资源是否被 CORS 阻止 → 确认子应用 devServer 配置了 Access-Control-Allow-Origin: *

  2. 检查控制台错误

    • Cannot use import statement outside a module → 子应用是 Vite 项目,需配置 iframe: true
    • Failed to resolve component: micro-app → Vite 未配置 isCustomElement
    • Unexpected token '<' → 子应用静态资源路径错误,检查 url 配置
  3. 检查沙箱模式:临时设置 disable-sandbox: true 确认是否是沙箱引起的问题

  4. 检查子应用生命周期:在 created/mounted/error 回调中打印日志,确认子应用加载到哪一步

  5. 查看 micro-app 日志:启用 __DEV__ 模式可看到详细日志


Q19为什么 Vite 子应用的静态资源会 404

答: 两个常见原因:

1. 资源路径使用绝对路径:

<!-- 子应用中这样写会 404因为主应用在 localhost:8080 -->
<script src="/assets/index.js"></script>
<!-- 实际应该请求子应用 localhost:5173/assets/index.js -->

解决:子应用 Vite 配置 base 或使用相对路径

2. baseroutebase 不匹配:

// 主应用
<micro-app baseroute="/child-app" />

// 子应用 vite.config.ts
export default defineConfig({
  base: '/child-app/'  // 必须与 baseroute 一致
})

九、实战总结

Q20这套微前端架构在生产环境中部署需要注意什么

答:

  1. 子应用独立部署:每个子应用有自己独立的 CI/CD 流水线,版本号独立管理

  2. 统一资源路径管理:通过环境变量管理各子应用的 URL

    export const subApps = [{
      name: 'vue2-app',
      url: import.meta.env.VITE_CHILD_APP_URL || 'http://localhost:5173/',
      baseroute: '/child-app',
    }]
    
  3. Nginx 配置

    • 所有子应用必须配置 CORS 头
    • 主应用和子应用建议部署在同源下(同协议、同域名、同端口),可避免大量跨域问题
    • 如果异构部署,需要配置反向代理统一转发
  4. 公共依赖提取:将 Vue、Vue Router 等公共依赖配置为 external避免重复加载

    // vite.config.ts
    build: {
      rollupOptions: {
        external: ['vue', 'vue-router']
      }
    }
    
  5. 灰度发布:利用 microApp.start()plugins.global 机制,可以在加载子应用 HTML 时做 A/B 测试控制

  6. 错误兜底:配置全局 error 回调 + 子应用容器的 @error 事件,展示友好的降级页面


附录:快速检查清单

检查项 正确配置
Vite isCustomElement (tag) => /^micro-app/.test(tag)
Vite 子应用沙箱 iframe: true(必须)
Webpack 子应用沙箱 iframe: false 即可
子应用跨域 Access-Control-Allow-Origin: *
子应用挂载 id 不能用 #app,需独立命名
路由通配符Vue Router 4 :page*,不是 *
lifeCycles 回调参数 (e: CustomEvent, appName: string)
子应用路由 base window.__MICRO_APP_BASE_ROUTE__ || '/'

十、样式穿透实战

Q21子应用 iframe: false 模式下,主应用的全局样式为什么会穿透到子应用?如何彻底解决?

答:

这是微前端 iframe: false 模式下最常见的样式泄漏问题。即使使用了 .app-main h2 这样的"作用域选择器",样式仍然会穿透。


一、问题现象

http://localhost:8080/mock-app 页面中,子应用的 <h2> 元素出现了主应用的紫色 border-bottom 样式,而不是子应用自己定义的样式。


二、根因分析DOM 层级重叠

主应用的 App.vue 布局结构:

<!-- App.vue 模板 -->
<div id="main-app">
  <header class="app-header">...</header>
  <main class="app-main">
    <router-view />   <!-- ← 这里渲染所有页面 -->
  </main>
</div>

当访问 /mock-app 时,<router-view> 渲染的是 ChildApp.vue

<!-- ChildApp.vue 模板 -->
<div class="child-app-wrapper">
  <micro-app name="mock-app" :iframe="false">
    <!-- 子应用的完整 DOM 在这里!包括 <h2>、<button>、<p> 等 -->
  </micro-app>
</div>

最终 DOM 树:

main.app-main                        ← 主应用的样式容器
  ├── div.home-page                  ← 访问 /home 时
  │     └── h2 (主应用的 h2)
  │
  └── div.child-app-wrapper          ← 访问 /mock-app 时
        └── <micro-app name="mock-app">
              └── h2 (子应用的 h2)   ← ⚠️ 也在 .app-main 内部!

关键点:子应用的 DOM 被渲染在 .app-main 内部,所以 .app-main h2 这个后代选择器会匹配到子应用的 <h2>


三、常见的错误认知

很多开发者以为加上父级 class 就能隔离:

/* ❌ 错误:这样写仍然会穿透! */
.app-main h2 {
  border-bottom: 2px solid #667eea !important;
}

为什么不行? 因为 .app-main h2后代选择器(任意层级),它会匹配 .app-main 下面所有层级的 h2,包括嵌套在 micro-app 内部的子应用 h2

/* ❌ 更差:这样写 100% 泄漏 */
h2 {
  border-bottom: 2px solid purple;
}

四、正确解法

方案 1子选择器 + :not() 排除子应用容器(本项目采用)

/* ✅ 正确:只匹配主应用页面内的 h2排除子应用容器 */
.app-main > :not(.child-app-wrapper) h2 {
  border-bottom: 2px solid #667eea !important;
}

.app-main > :not(.child-app-wrapper) button { ... }
.app-main > :not(.child-app-wrapper) p { ... }
.app-main > :not(.child-app-wrapper) table { ... }
.app-main > :not(.child-app-wrapper) code { ... }

原理:

  • > 子选择器:只匹配 .app-main直接子元素
  • :not(.child-app-wrapper):排除类名为 .child-app-wrapper 的直接子元素
  • 子应用的容器 .child-app-wrapper 被排除 → 其内部所有元素都不受主样式影响
.app-main
  ├── div.home-page            ← :not(.child-app-wrapper) → ✅ 样式生效
  └── div.child-app-wrapper    ← 被 :not() 排除 → ❌ 样式不生效
        └── h2 (安全!)

方案 2开启 iframe 沙箱(最彻底)

{
  name: 'mock-app',
  iframe: true,  // ← 浏览器原生隔离,样式 100% 不互通
}

优点:彻底的样式/JS 隔离
缺点:性能略差、调试不便、通信成本增加

方案 3使用 Shadow DOM激进方案

<micro-app name="mock-app" :shadowDOM="true" />

缺点Shadow DOM 边界会阻断很多 CSS 特性(如全局字体),兼容性成本高。

方案 4CSS @scope未来方案

@scope (.app-main) to (micro-app) {
  h2 { border-bottom: 2px solid #667eea; }
}

@scopeto 边界可以精确控制样式不穿透到 micro-app 内部。Chrome 118+、Safari 17.4+ 已支持,但目前还不够普及。


五、各方案对比

方案 隔离程度 性能 复杂度 适用场景
:not(.child-app-wrapper) 中(单向阻断) 无影响 已知子应用容器结构的场景
iframe 沙箱 高(完全隔离) 略低 低(配置即可) Vite 子应用、高隔离要求
Shadow DOM DOM 隔离) 高(兼容问题多) 对隔离有极致要求的场景
CSS @scope 中高 无影响 未来主流方案(等浏览器普及)

六、核心教训

iframe: false 模式下,子应用和主应用共享同一个 document任何后代选择器都会穿透到子应用内部。给选择器加父级 class.app-main)只在父级和子应用不在同一容器时才有效。真正有效的做法是用子选择器限制层级深度,同时用 :not() 排除子应用容器。