vue2项目接入完成

This commit is contained in:
2026-06-21 00:05:19 +08:00
parent c76852f949
commit 7df8308ea8
4 changed files with 635 additions and 5 deletions

View File

@@ -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 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 })
```

View File

@@ -2,7 +2,40 @@ import Vue from 'vue'
import App from './App.vue'
import router from './router'
new Vue({
let app = null
/**
* 挂载 Vue 实例
* 微前端环境下由主应用通过 window.mount() 调用
* 独立运行时直接调用
*/
function mount() {
if (app) return // 防止重复挂载
app = new Vue({
router,
render: h => h(App)
}).$mount('#app')
}).$mount('#app')
}
/**
* 卸载 Vue 实例
* 微前端 keepAlive 切换时由主应用通过 window.unmount() 调用
*/
function unmount() {
if (app) {
app.$destroy()
app = null
}
}
// 判断是否运行在 micro-app 沙箱中
if (window.__MICRO_APP_ENVIRONMENT__) {
// 注册生命周期钩子,供主应用 keepAlive 切换路由时调用
window.mount = mount
window.unmount = unmount
// 首次加载时主动挂载
mount()
} else {
// 独立运行模式(如本地开发直接访问 http://localhost:5173
mount()
}

View File

@@ -26,6 +26,8 @@ const routes = [
const router = new VueRouter({
mode: 'history', // 去掉 URL 中的 #,需要后端配合兜底
// 微前端环境下使用主应用传入的 base route独立运行时默认 '/'
base: window.__MICRO_APP_BASE_ROUTE__ || '/',
routes
})

View File

@@ -15,5 +15,15 @@ export default defineConfig({
'vue$': 'vue/dist/vue.esm.js'
},
extensions: ['.js', '.vue', '.json']
}
},
server: {
port: 5173,
// micro-app 子应用跨域配置:允许主应用 fetch 子应用资源
cors: true,
headers: {
'Access-Control-Allow-Origin': '*',
},
},
// 生产环境 base 应与主应用 baseroute 对齐,需要时取消注释
// base: '/child-app/',
})