diff --git a/docs/micro-app-adaptation.md b/docs/micro-app-adaptation.md new file mode 100644 index 0000000..9d794b8 --- /dev/null +++ b/docs/micro-app-adaptation.md @@ -0,0 +1,585 @@ +# 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` 配置中: + +```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: '/'` → `