vue2项目接入完成
This commit is contained in:
585
docs/micro-app-adaptation.md
Normal file
585
docs/micro-app-adaptation.md
Normal 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 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: '/'` → `<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 进程仍在后台占用端口。
|
||||
|
||||
---
|
||||
|
||||
**排查 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`:
|
||||
|
||||
```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 部署
|
||||
|
||||
---
|
||||
|
||||
**排查 3:CORS 响应头是否生效**
|
||||
|
||||
症状:控制台报 `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 后需要重启)
|
||||
|
||||
---
|
||||
|
||||
**排查 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` 分支保留 |
|
||||
|
||||
```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` + history,URL 正确 |
|
||||
|
||||
```js
|
||||
// 改造前
|
||||
new VueRouter({ mode: 'history', routes })
|
||||
|
||||
// 改造后
|
||||
new VueRouter({ mode: 'history', base: window.__MICRO_APP_BASE_ROUTE__ || '/', routes })
|
||||
```
|
||||
35
src/main.js
35
src/main.js
@@ -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')
|
||||
}
|
||||
|
||||
/**
|
||||
* 卸载 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()
|
||||
}
|
||||
@@ -26,6 +26,8 @@ const routes = [
|
||||
|
||||
const router = new VueRouter({
|
||||
mode: 'history', // 去掉 URL 中的 #,需要后端配合兜底
|
||||
// 微前端环境下使用主应用传入的 base route,独立运行时默认 '/'
|
||||
base: window.__MICRO_APP_BASE_ROUTE__ || '/',
|
||||
routes
|
||||
})
|
||||
|
||||
|
||||
@@ -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/',
|
||||
})
|
||||
Reference in New Issue
Block a user