Files
child-vue2-interview/docs/micro-app-adaptation.md
2026-06-21 00:05:19 +08:00

21 KiB
Raw Permalink Blame History

Vue 2 子应用接入 micro-app 微前端 — 面试问答手册

主应用配置:@micro-zoe/micro-appbaseroute 为 /child-approuterMode 为 nativeiframe 为 falsekeepAlive 为 true。 子应用技术栈Vue 2.7 + Vite 7 + Vue Router 3history 模式)。


一、CORS 跨域配置

Q1: 子应用为什么需要配置 CORS不配会怎样

答: 主应用通过 fetch 请求子应用的 HTML 入口文件(如 http://localhost:5173/),浏览器同源策略会阻止跨域请求。如果不配置 CORS主应用无法获取子应用的 HTML/JS/CSS 资源,导致子应用加载失败(控制台报跨域错误)。

Q2: Vite 项目中如何配置 CORS

答:vite.config.jsserver 配置中:

server: {
    cors: true,                          // 开启 CORS
    headers: {
        'Access-Control-Allow-Origin': '*',  // 允许任意来源
    },
}

追问: cors: true 和手动设置 headers 有什么区别?

答: cors: true 是 Vite 内置的语法糖,等价于设置 Access-Control-Allow-Origin: *,同时 Vite 还会自动处理预检请求OPTIONS。手动加 headers 是双保险,确保所有响应都带上跨域头。

Q3: 生产环境怎么处理 CORS

答: 三种方式:

  1. Nginx 反向代理:把主应用和子应用部署到同一域名下,从根本上消除跨域。
  2. Nginx 配置 CORS 头add_header Access-Control-Allow-Origin *;
  3. 子应用服务器自行返回 CORS 头Node/Java/Go 等服务端中间件处理。

追问: 为什么不推荐生产环境用 *

答: Access-Control-Allow-Origin: * 不允许携带 Cookie 和 Authorization 头。如果子应用需要携带用户凭证,必须指定具体域名并配合 Access-Control-Allow-Credentials: true


二、路由 base 对齐

Q4: 子应用路由为什么要设置 base?不设会怎样?

答: 主应用的 baseroute/child-app,意味着主应用会把 /child-app/* 的路径分配给该子应用。如果子应用路由不设置 base: '/child-app',子应用内部的路由跳转会丢失这个前缀,导致:

  1. 路由跳转到错误路径(如跳到 /about 而非 /child-app/about
  2. 主应用无法正确识别子应用的路由变化
  3. 浏览器刷新后 404

Q5: base: window.__MICRO_APP_BASE_ROUTE__ 是什么原理?

答: window.__MICRO_APP_BASE_ROUTE__ 是 micro-app 在加载子应用时注入到沙箱 window 上的全局变量,其值就是主应用配置中该子应用的 baseroute(即 /child-app)。

源码实现相当于:

// micro-app 源码(简化)
sandbox.proxyWindow.__MICRO_APP_BASE_ROUTE__ = appConfig.baseroute // '/child-app'

Q6: 为什么需要 || '/' 兜底?

答: 子应用在独立运行时(开发人员直接在浏览器访问 http://localhost:5173window.__MICRO_APP_BASE_ROUTE__undefined|| '/' 确保独立运行模式下路由正常工作,不需要每次调试都要通过主应用加载。

追问: 这个写法有什么潜在风险?

答: 在一个空字符串情况下 '' || '/' 的结果是 '/',但 micro-app 不会传空字符串,这个风险可以忽略。真实的坑是路由 base 以 / 开头但不以 / 结尾时的行为差异Vue Router 会自动处理,一般不需要担心)。


三、生命周期管理keepAlive

Q7: 为什么要实现 window.mountwindow.unmount

答: 主应用配置了 keepAlive: true,子应用在路由切换时不会被销毁重建,而是走 挂载/卸载 生命周期:

  • 用户离开子应用页面 → 主应用调用 window.unmount(),子应用自行销毁 Vue 实例、清理事件监听、释放内存
  • 用户回到子应用页面 → 主应用调用 window.mount(),子应用重新创建 Vue 实例并挂载

不实现这两个钩子的后果:

  • 内存泄漏:离开子应用后 Vue 实例仍在占用内存
  • 事件监听残留:定时器、全局事件等继续运行
  • 状态错乱:回到子应用时可能出现重复挂载或状态异常

Q8: main.js 中的生命周期实现细节?

答: 核心代码:

let app = null  // 闭包持有 Vue 实例引用

function mount() {
    if (app) return  // ⚠️ 关键:防止重复挂载
    app = new Vue({ router, render: h => h(App) }).$mount('#app')
}

function unmount() {
    if (app) {
        app.$destroy()  // Vue 2 官方销毁方法
        app = null      // 释放引用,让 GC 回收
    }
}

追问: 为什么 mount 里要加 if (app) return 判断?

答: 两个原因:

  1. 微前端沙箱初始化和跨应用切换时,mount 可能被调用多次(初始化一次 + 切回时一次),不加判断会导致多个 Vue 实例同时挂载到 #app
  2. 防御性编程——即使主应用行为变更也不会导致子应用崩溃。

Q9: app.$destroy() 做了什么?够不够?

答: Vue 2 的 $destroy() 会:

  1. 触发 beforeDestroy / destroyed 生命周期钩子
  2. 解除 Vue 实例与 DOM 的绑定
  3. 移除所有 Watcher数据响应式
  4. 解绑组件内的事件监听

但它不会做:

  • 清除 setInterval / setTimeout
  • 清除手动 addEventListener 绑定的全局事件
  • 清除第三方库创建的 DOM 节点

追问: 那这些“漏网之鱼”怎么处理?

答: 在 Vue 组件的 beforeDestroy 钩子中手动清理:

export default {
    data() { return { timer: null } },
    created() { this.timer = setInterval(() => {...}, 1000) },
    beforeDestroy() { clearInterval(this.timer) }  // 手动清理
}

四、环境判断

Q10: window.__MICRO_APP_ENVIRONMENT__ 是谁设置的?值为 true 意味着什么?

答: micro-app 在初始化子应用沙箱时设置。值为 true 表示当前 JS 代码在 micro-app 的沙箱proxy window中执行而非浏览器的原生 window 环境。

追问: 除了环境判断micro-app 还注入了哪些全局变量?

答:

变量 含义
window.__MICRO_APP_ENVIRONMENT__ 是否在沙箱中运行
window.__MICRO_APP_NAME__ 当前子应用名称(这里是 'vue2-app'
window.__MICRO_APP_BASE_ROUTE__ 主应用给子应用分配的路由前缀
window.__MICRO_APP_PUBLIC_PATH__ 子应用的静态资源路径
window.microApp micro-app 实例,用于父子通信
window.rawWindow 原生 window 对象(绕过沙箱)
window.rawDocument 原生 document 对象(绕过沙箱)

五、沙箱与隔离

Q11: iframe: false 意味着什么?用什么机制隔离?

答: iframe: false 表示使用 JS Proxy 沙箱 而非 iframe 隔离。micro-app 通过 new Proxy(window, {...}) 创建一个代理 window 对象,拦截子应用对全局变量/DOM 的读写操作,将其限制在子应用的作用域内。

追问: Proxy 沙箱 vs iframe 的优缺点?

答:

维度 Proxy 沙箱 iframe
性能 高(无额外渲染进程) 低(每个 iframe 独立进程)
样式隔离 需要额外处理CSS Scope 天然隔离
JS 隔离 基本隔离 完全隔离
弹窗/通知 ⚠️ 可能穿透 完全隔离
三方库兼容 ⚠️ 部分库可能有问题 兼容性好

追问: 什么时候必须改用 iframe: true

答:

  1. 子应用使用了一些不兼容 Proxy 的库(直接操作 window.top 等)
  2. 子应用需要完全独立的 document
  3. 使用 v-html 渲染第三方内容XSS 风险)
  4. 出现白屏且排查不到其他原因时

六、routerMode 配置

Q12: routerMode: 'native' 是什么意思?和 search 模式有什么区别?

答:

模式 路由体现 子应用路由要求
native URL path/child-app/about 子应用必须使用 history 模式base 设为 /child-app
search URL query/child-app?router=/about 子应用可用 hash 或 history 模式
pure 不变化 单页面应用(不需要路由同步)

当前配置 native 模式 + history 路由是最佳实践——URL 干净、SEO 友好、用户可直接通过 URL 访问子页面。


七、Vite base 与构建部署

Q13: vite.config.js 里的 base 什么时候需要改成 /child-app/

答: 生产构建时base 决定了 Vite 打包后 HTML 中静态资源的引用路径:

  • base: '/'<script src="/assets/app-xxx.js">(需要部署在域名根路径)
  • base: '/child-app/'<script src="/child-app/assets/app-xxx.js">(部署在 /child-app/ 子路径)

如果主应用通过 Nginx 把子应用部署在 /child-app/ 路径下,子应用的 base 必须对应。

追问: 开发环境这个注释掉的 base 会影响吗?

答: 不会。开发环境 Vite 使用原生 ESM<script type="module" src="/src/main.js"> 直接由 Vite Dev Server 处理,不走 base。这也是为什么开发环境保持 base 为默认 / 即可。


八、父子通信

Q14: 父子应用之间如何通信?

答: 通过 window.microApp 提供的 API

子应用获取主应用数据:

// 监听主应用下发的数据
window.microApp.addDataListener((data) => {
    console.log('收到主应用数据:', data)
})

子应用向主应用发送数据:

window.microApp.dispatch({ type: 'navigate', path: '/other-app' })

追问: keepAlive: true 时,addDataListener 需要注意什么?

答: addDataListener 在组件 destroyed 时不会自动解绑。需要在 beforeDestroy 中手动解绑:

dataListener = window.microApp.addDataListener(handler)
// 卸载时
dataListener()  // 调用返回的函数即可解绑

九、常见问题排查

Q15: 子应用白屏怎么排查?

答: 排障 checklist 顺序:

  1. 控制台是否有 CORS 报错? → 检查子应用 CORS 配置 + 子应用服务是否启动
  2. Network 中 JS/CSS 是否加载成功? → 检查 URL 是否正确、资源路径是否正确
  3. Vue DevTools 是否显示组件树? → 有树但页面空白 → CSS 样式问题;无树 → JS 执行报错
  4. 是否 mount 了多个 Vue 实例? → 检查 mount() 是否有重复挂载保护
  5. routerMode 和路由模式是否匹配?native 模式必须配 history 路由
  6. 第三方库是否兼容 Proxy 沙箱? → 尝试 iframe: true

Q16: 子应用路由跳转后主应用 URL 没变化?

答: 通常是 base 没有正确设置。检查:

  1. 路由 base 是否为 window.__MICRO_APP_BASE_ROUTE__
  2. 主应用 baseroute 配置是否正确
  3. routerMode 是否为 native

Q17: 子应用独立运行正常,通过主应用加载就报错,为什么?

答: 最常见原因:

  1. CORS 未配置——独立运行时不存在跨域,主应用加载时才出现
  2. 沙箱兼容性——子应用的某段代码依赖原生 windowProxy 沙箱下行为不一致
  3. 路径错误——独立运行时路径 /about 正确,微前端下应该是 /child-app/about

十、架构设计面试题

Q18: 如果让你设计一个微前端框架,子应用接入需要做哪些事?

答: 考察对微前端原理的理解,可以按以下层次回答:

  1. 资源加载层fetch HTML → 解析 script/link → 提取 CSS → 执行 JS
  2. 沙箱隔离层JS 隔离Proxy/iframe + 样式隔离Scoped CSS/Shadow DOM
  3. 路由同步层:主应用路由 ↔ 子应用路由双向同步
  4. 生命周期层bootstrap → mount → unmount → destroy
  5. 通信层:父子应用数据传递(发布订阅/全局状态)

Q19: 微前端架构的优缺点?什么时候该用什么时候不该用?

答:

该用:

  • 多个团队独立开发、独立部署(如不同业务线)
  • 老旧系统技术栈迁移(增量升级,不用整体重写)
  • 巨石应用拆分解耦

不该用:

  • 单一团队的小型应用(过度设计)
  • 对性能极致要求iframe/沙箱有额外开销)
  • SEO 依赖严重的 C 端页面SSR 困难)
  • 子应用间强耦合的场景(共享状态复杂)

十一、实战改造Vue 2 + Vite 项目完整接入流程

Q20: 把一个 Vue 2 + Vite 项目改造为 micro-app 子应用,完整步骤是什么?

答: 共 4 步,涉及 3 个文件的修改。

第一步vite.config.js — 开启 CORS

// vite.config.js
export default defineConfig({
    // ... 原有配置
    server: {
        port: 5173,
        cors: true,                               // 开启 CORS
        headers: {
            'Access-Control-Allow-Origin': '*',   // 允许跨域
        },
    },
    // 生产构建时 base 对齐主应用 baseroute需要时取消注释
    // base: '/child-app/',
})

第二步src/router/index.js — 动态路由 base

// src/router/index.js
const router = new VueRouter({
    mode: 'history',
    // 微前端环境下使用主应用注入的 base route独立运行时兜底 '/'
    base: window.__MICRO_APP_BASE_ROUTE__ || '/',
    routes,
})

第三步src/main.js — 生命周期适配

// src/main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'

let app = null

function mount() {
    if (app) return                     // 防止重复挂载
    app = new Vue({
        router,
        render: h => h(App)
    }).$mount('#app')
}

function unmount() {
    if (app) {
        app.$destroy()
        app = null
    }
}

if (window.__MICRO_APP_ENVIRONMENT__) {
    window.mount = mount      // 注册生命周期:主应用 keepAlive 切换时调用
    window.unmount = unmount
    mount()                   // 首次加载主动挂载
} else {
    mount()                   // 独立运行模式(直接访问 localhost:5173
}

第四步:主应用配置

// 主应用 subApps 配置
export const subApps = [
    {
        name: 'vue2-app',
        url: 'http://localhost:5173/',
        baseroute: '/child-app',
        iframe: false,          // 优先尝试 Proxy 沙箱(性能好)
        keepAlive: true,
        routerMode: 'native',
    }
]

追问: 改造后如何验证?

答: 分两步验证:

  1. 独立运行验证:直接访问 http://localhost:5173/,确认子应用自身功能正常
  2. 微前端集成验证:通过主应用访问 http://localhost:8080/child-app,确认能正常加载

Q21: 改造后访问主应用页面空白,怎么排查?(实操案例)

答: 这是我们实际改造中遇到的真实问题,按以下 checklist 逐项排查:

排查 1子应用服务是否在正确的端口运行

症状主应用页面空白Network 面板显示对子应用的请求 pending 或 404。

# 检查端口占用
netstat -ano | grep 5173

Vite 在默认端口被占用时会自动跳到下一个端口5173 → 5174 → 5175...),但主应用配置的 url 是固定的 http://localhost:5173/,端口不一致导致主应用请求落空。

根因与修复:

# 杀掉占用 5173 的残留进程,重新启动
taskkill /PID <pid> /F
npm run dev

追问: 为什么会有残留进程?

答: 上一次次 npm run dev 没有正常终止Ctrl+C 杀的是终端不是进程,或 IDE 崩溃Vite 进程仍在后台占用端口。


排查 2Vite ESM 模块与 micro-app Proxy 沙箱兼容性

症状:子应用独立运行正常(http://localhost:5173/ 有内容),但通过主应用加载就是空白,控制台无 CORS 报错Network 面板显示 HTML 请求成功但 JS 未执行。

根因Vite 开发模式使用 <script type="module" src="/src/main.js">,浏览器以 ESM 方式加载。micro-app 的 Proxy 沙箱对 ESM 模块脚本的拦截和执行可能存在兼容性问题,导致 Vue 实例未能创建。

快速验证方法:在主应用配置中临时将 iframe 改为 true

{
    name: 'vue2-app',
    url: 'http://localhost:5173/',
    baseroute: '/child-app',
    iframe: true,  // 改成 true用 iframe 隔离绕过 Proxy 沙箱
    keepAlive: true,
    routerMode: 'native',
}

如果 iframe: true 后页面正常,则确认是 Proxy 沙箱兼容问题。此时有两种选择:

  1. 保持 iframe: true(兼容性最好,代价是额外渲染进程开销)
  2. 使用子应用生产构建(npm run build 后的产物无 ESM兼容 Proxy 沙箱)+ Nginx 部署

排查 3CORS 响应头是否生效

症状:控制台报 Access-Control-Allow-Origin 相关错误。

验证:用 curl 或浏览器直接请求子应用,检查响应头:

curl -I http://localhost:5173/
# 响应应包含:
# Access-Control-Allow-Origin: *

如果响应头缺失,检查:

  • vite.config.jsserver.cors: true 是否正确配置
  • 是否重启了 Vite dev server修改 config 后需要重启)

排查 4router base 不匹配导致空白

症状HTML/JS 加载成功Vue 实例已创建,但 <router-view> 中无内容。

根因:主应用的 baseroute: '/child-app' 与子应用路由 base 不匹配。

  • 用户访问 http://localhost:8080/child-app
  • micro-app 注入 window.__MICRO_APP_BASE_ROUTE__ = '/child-app'
  • 子应用路由 base: '/child-app'path: '/' 匹配 → 应显示 Home 组件
  • 如果忘记设置 base,路由 path: '/' 只匹配 /,不匹配 /child-app,导致 <router-view> 为空

修复:确认 src/router/index.js 中设置了 base: window.__MICRO_APP_BASE_ROUTE__ || '/'


排查 5重复挂载或 $mount 目标不存在

症状:控制台报 [Vue warn]: Cannot find element: #app 或出现多个 Vue 实例。

  • #app 不存在:检查子应用 index.html 是否有 <div id="app"></div>
  • 重复挂载:mount() 函数中缺少 if (app) return 保护

综合排查流程图:

空白页面
  ├── Network 有子应用 HTML 请求?
  │     ├── 无 → 子应用未启动 / 端口不对 → 检查 dev server
  │     └── 有但状态非 200 → CORS 报错?→ 检查 CORS 配置
  ├── Network 有 JS 请求且返回 200
  │     └── 无 → HTML 中 script 路径错误 / CORS 拦截
  ├── Console 有 Vue warn/error
  │     ├── "Cannot find element" → index.html 缺少 #app
  │     ├── "already mounted" → 重复挂载
  │     └── 无报错但空白 → Proxy 沙箱兼容 → 改成 iframe: true 验证
  └── Vue DevTools 有组件树但页面空白?
        ├── 是 → 样式问题CSS 未加载或被隔离)
        └── 否 → router-view 无匹配路由 → 检查 base 配置

Q22: iframe: true vs iframe: false,实际项目中怎么选?

答: 决策流程:

子应用是否用 Vite/Webpack ESM 开发模式?
  ├── 是 → 先用 iframe: true 快速跑通
  │       上线时用构建产物 + iframe: false构建产物无 ESM 问题)
  └── 否 → 直接用 iframe: falseProxy 沙箱性能更好)

子应用是否依赖 window.top / window.parent
  ├── 是 → 必须 iframe: trueProxy 沙箱无法模拟)
  └── 否 → 优先 iframe: false

子应用是否有大量第三方 DOM 操作库(如 ECharts 地图、富文本编辑器)?
  ├── 是 → iframe: true 更稳定
  └── 否 → iframe: false

实际项目经验:

  • 开发阶段:iframe: true 省心,兼容性问题最少
  • 生产阶段:iframe: false + 构建产物,性能和体验最优
  • 如果子应用数量多5+iframe: true 会导致浏览器内存压力显著增大,必须用 iframe: false

十二、改造前后对比

Q23: 改造前后 main.js 的核心区别是什么?

答:

维度 改造前 改造后
挂载方式 立即执行 $mount('#app') 封装为 mount() 函数,按需调用
卸载能力 无(只能关闭页面) unmount()$destroy() 清理
环境感知 通过 __MICRO_APP_ENVIRONMENT__ 判断
重复挂载 无保护 if (app) return 防重复
生命周期 曝露 window.mount / window.unmount
独立运行 通过 else 分支保留
// 改造前:只管自己运行
new Vue({ router, render: h => h(App) }).$mount('#app')

// 改造后:同时支持独立运行和被主应用管控
let app = null
function mount()  { if (!app) app = new Vue({...}).$mount('#app') }
function unmount(){ if (app) { app.$destroy(); app = null } }
window.__MICRO_APP_ENVIRONMENT__ ? (window.mount = mount, window.unmount = unmount, mount()) : mount()

Q24: 改造前后 router 配置的核心区别是什么?

答:

维度 改造前 改造后
base 无(默认 / window.__MICRO_APP_BASE_ROUTE__ || '/'
独立运行 / + history '/' 兜底,行为一致
微前端运行 路由错乱 /child-app + historyURL 正确
// 改造前
new VueRouter({ mode: 'history', routes })

// 改造后
new VueRouter({ mode: 'history', base: window.__MICRO_APP_BASE_ROUTE__ || '/', routes })