21 KiB
Vue 2 子应用接入 micro-app 微前端 — 面试问答手册
主应用配置:
@micro-zoe/micro-app,baseroute 为/child-app,routerMode 为native,iframe 为false,keepAlive 为true。 子应用技术栈:Vue 2.7 + Vite 7 + Vue Router 3(history 模式)。
一、CORS 跨域配置
Q1: 子应用为什么需要配置 CORS?不配会怎样?
答: 主应用通过 fetch 请求子应用的 HTML 入口文件(如 http://localhost:5173/),浏览器同源策略会阻止跨域请求。如果不配置 CORS,主应用无法获取子应用的 HTML/JS/CSS 资源,导致子应用加载失败(控制台报跨域错误)。
Q2: Vite 项目中如何配置 CORS?
答: 在 vite.config.js 的 server 配置中:
server: {
cors: true, // 开启 CORS
headers: {
'Access-Control-Allow-Origin': '*', // 允许任意来源
},
}
追问: cors: true 和手动设置 headers 有什么区别?
答: cors: true 是 Vite 内置的语法糖,等价于设置 Access-Control-Allow-Origin: *,同时 Vite 还会自动处理预检请求(OPTIONS)。手动加 headers 是双保险,确保所有响应都带上跨域头。
Q3: 生产环境怎么处理 CORS?
答: 三种方式:
- Nginx 反向代理:把主应用和子应用部署到同一域名下,从根本上消除跨域。
- Nginx 配置 CORS 头:
add_header Access-Control-Allow-Origin *; - 子应用服务器自行返回 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',子应用内部的路由跳转会丢失这个前缀,导致:
- 路由跳转到错误路径(如跳到
/about而非/child-app/about) - 主应用无法正确识别子应用的路由变化
- 浏览器刷新后 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:5173),window.__MICRO_APP_BASE_ROUTE__ 是 undefined。|| '/' 确保独立运行模式下路由正常工作,不需要每次调试都要通过主应用加载。
追问: 这个写法有什么潜在风险?
答: 在一个空字符串情况下 '' || '/' 的结果是 '/',但 micro-app 不会传空字符串,这个风险可以忽略。真实的坑是路由 base 以 / 开头但不以 / 结尾时的行为差异(Vue Router 会自动处理,一般不需要担心)。
三、生命周期管理(keepAlive)
Q7: 为什么要实现 window.mount 和 window.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 判断?
答: 两个原因:
- 微前端沙箱初始化和跨应用切换时,
mount可能被调用多次(初始化一次 + 切回时一次),不加判断会导致多个 Vue 实例同时挂载到#app。 - 防御性编程——即使主应用行为变更也不会导致子应用崩溃。
Q9: app.$destroy() 做了什么?够不够?
答: Vue 2 的 $destroy() 会:
- 触发
beforeDestroy/destroyed生命周期钩子 - 解除 Vue 实例与 DOM 的绑定
- 移除所有 Watcher(数据响应式)
- 解绑组件内的事件监听
但它不会做:
- 清除
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?
答:
- 子应用使用了一些不兼容 Proxy 的库(直接操作
window.top等) - 子应用需要完全独立的
document - 使用
v-html渲染第三方内容(XSS 风险) - 出现白屏且排查不到其他原因时
六、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 顺序:
- 控制台是否有 CORS 报错? → 检查子应用 CORS 配置 + 子应用服务是否启动
- Network 中 JS/CSS 是否加载成功? → 检查 URL 是否正确、资源路径是否正确
- Vue DevTools 是否显示组件树? → 有树但页面空白 → CSS 样式问题;无树 → JS 执行报错
- 是否 mount 了多个 Vue 实例? → 检查
mount()是否有重复挂载保护 routerMode和路由模式是否匹配? →native模式必须配 history 路由- 第三方库是否兼容 Proxy 沙箱? → 尝试
iframe: true
Q16: 子应用路由跳转后主应用 URL 没变化?
答: 通常是 base 没有正确设置。检查:
- 路由 base 是否为
window.__MICRO_APP_BASE_ROUTE__ - 主应用
baseroute配置是否正确 routerMode是否为native
Q17: 子应用独立运行正常,通过主应用加载就报错,为什么?
答: 最常见原因:
- CORS 未配置——独立运行时不存在跨域,主应用加载时才出现
- 沙箱兼容性——子应用的某段代码依赖原生
window,Proxy 沙箱下行为不一致 - 路径错误——独立运行时路径
/about正确,微前端下应该是/child-app/about
十、架构设计面试题
Q18: 如果让你设计一个微前端框架,子应用接入需要做哪些事?
答: 考察对微前端原理的理解,可以按以下层次回答:
- 资源加载层:fetch HTML → 解析 script/link → 提取 CSS → 执行 JS
- 沙箱隔离层:JS 隔离(Proxy/iframe) + 样式隔离(Scoped CSS/Shadow DOM)
- 路由同步层:主应用路由 ↔ 子应用路由双向同步
- 生命周期层:bootstrap → mount → unmount → destroy
- 通信层:父子应用数据传递(发布订阅/全局状态)
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',
}
]
追问: 改造后如何验证?
答: 分两步验证:
- 独立运行验证:直接访问
http://localhost:5173/,确认子应用自身功能正常 - 微前端集成验证:通过主应用访问
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 进程仍在后台占用端口。
排查 2:Vite 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 沙箱兼容问题。此时有两种选择:
- 保持
iframe: true(兼容性最好,代价是额外渲染进程开销) - 使用子应用生产构建(
npm run build后的产物无 ESM,兼容 Proxy 沙箱)+ Nginx 部署
排查 3:CORS 响应头是否生效
症状:控制台报 Access-Control-Allow-Origin 相关错误。
验证:用 curl 或浏览器直接请求子应用,检查响应头:
curl -I http://localhost:5173/
# 响应应包含:
# Access-Control-Allow-Origin: *
如果响应头缺失,检查:
vite.config.js中server.cors: true是否正确配置- 是否重启了 Vite dev server(修改 config 后需要重启)
排查 4:router 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: false(Proxy 沙箱性能更好)
子应用是否依赖 window.top / window.parent?
├── 是 → 必须 iframe: true(Proxy 沙箱无法模拟)
└── 否 → 优先 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 + history,URL 正确 |
// 改造前
new VueRouter({ mode: 'history', routes })
// 改造后
new VueRouter({ mode: 'history', base: window.__MICRO_APP_BASE_ROUTE__ || '/', routes })