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

586 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Vue 2 子应用接入 micro-app 微前端 — 面试问答手册
> 主应用配置:`@micro-zoe/micro-app`baseroute 为 `/child-app`routerMode 为 `native`iframe 为 `false`keepAlive 为 `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.js``server` 配置中:
```js
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`)。
源码实现相当于:
```js
// 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 中的生命周期实现细节?
**答:** 核心代码:
```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` 钩子中手动清理:
```js
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
**子应用获取主应用数据:**
```js
// 监听主应用下发的数据
window.microApp.addDataListener((data) => {
console.log('收到主应用数据:', data)
})
```
**子应用向主应用发送数据:**
```js
window.microApp.dispatch({ type: 'navigate', path: '/other-app' })
```
**追问:** `keepAlive: true` 时,`addDataListener` 需要注意什么?
**答:** `addDataListener` 在组件 `destroyed` 时不会自动解绑。需要在 `beforeDestroy` 中手动解绑:
```js
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. **沙箱兼容性**——子应用的某段代码依赖原生 `window`Proxy 沙箱下行为不一致
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**
```js
// 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**
```js
// src/router/index.js
const router = new VueRouter({
mode: 'history',
// 微前端环境下使用主应用注入的 base route独立运行时兜底 '/'
base: window.__MICRO_APP_BASE_ROUTE__ || '/',
routes,
})
```
**第三步src/main.js — 生命周期适配**
```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
}
```
**第四步:主应用配置**
```js
// 主应用 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。
```bash
# 检查端口占用
netstat -ano | grep 5173
```
Vite 在默认端口被占用时会**自动跳到下一个端口**5173 → 5174 → 5175...),但主应用配置的 `url` 是固定的 `http://localhost:5173/`,端口不一致导致主应用请求落空。
根因与修复:
```bash
# 杀掉占用 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`
```js
{
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 或浏览器直接请求子应用,检查响应头:
```bash
curl -I http://localhost:5173/
# 响应应包含:
# Access-Control-Allow-Origin: *
```
如果响应头缺失,检查:
- `vite.config.js``server.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` 分支保留 |
```js
// 改造前:只管自己运行
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 正确 |
```js
// 改造前
new VueRouter({ mode: 'history', routes })
// 改造后
new VueRouter({ mode: 'history', base: window.__MICRO_APP_BASE_ROUTE__ || '/', routes })
```