Files
microapp-main-interview/docs/micro-app多子应用接入实战.md
2026-06-21 10:57:32 +08:00

1248 lines
43 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.
# micro-app 多子应用接入实战面试题
> 基于在已有主应用中接入第二个 Vue 3 + Vite 子应用(端口 3001的完整实战经验整理。
---
## 一、多子应用注册与路由
### Q1如何在已有 micro-app 主应用中新增一个子应用?需要改哪些文件?
**答:**
只需要改 **3 个文件**,遵循「配置 → 路由 → 导航」的顺序:
**1. `src/config/subApps.ts` — 添加子应用配置**
```ts
export const subApps: SubAppConfig[] = [
{
name: 'vue2-app',
url: 'http://localhost:5173/',
baseroute: '/child-app',
iframe: true,
keepAlive: true,
routerMode: 'native'
},
// ✅ 新增
{
name: 'vue3-app',
url: 'http://localhost:3001/',
baseroute: '/vue3-app',
iframe: true,
keepAlive: true,
routerMode: 'native'
}
]
```
**2. `src/router/index.ts` — 添加通配路由**
```ts
const routes = [
// ... 其他路由
{
path: '/vue3-app/:page*', // ✅ 新增:通配符匹配子应用所有内部路由
name: 'vue3App',
component: () => import('@/views/ChildApp.vue')
}
]
```
**3. `src/App.vue` — 导航栏添加入口**
```html
<router-link to="/vue3-app">Vue3 子应用</router-link>
```
**不需要改的文件:**
- `ChildApp.vue` — 容器组件是通用的,根据 URL 自动匹配配置
- `main.ts``microApp.start()` 是框架级初始化,不绑定具体子应用
- `vite.config.ts``isCustomElement` 配置对所有 `<micro-app>` 标签生效
---
### Q2为什么要设计成"3 个文件改动"的模式?能否进一步减少?
**答:**
这是 micro-app 架构设计中最简洁的扩展模式:
| 改动 | 为什么必须 |
|------|-----------|
| `subApps.ts` | 子应用的元信息name、url、baseroute必须有地方存储 |
| `router/index.ts` | Vue Router 需要知道哪些 URL 前缀交给子应用处理 |
| `App.vue` | 用户需要入口点击进入子应用 |
**能否合并?**
可以。如果子应用数量很多,可以优化为**自动注册**模式:
```ts
// router/index.ts — 自动为所有子应用生成路由
import { subApps } from '@/config/subApps'
const subAppRoutes = subApps.map(app => ({
path: `${app.baseroute}/:page*`,
name: app.name,
component: () => import('@/views/ChildApp.vue')
}))
const routes = [
{ path: '/', redirect: '/home' },
{ path: '/home', component: () => import('@/views/Home.vue') },
...subAppRoutes // 自动展开
]
```
导航栏同理,用 `v-for` 遍历 `subApps` 自动生成。这样新增子应用只需要改 `subApps.ts` 一个文件。
---
### Q3`ChildApp.vue` 组件是如何做到"一个组件服务所有子应用"的?
**答:**
核心原理是通过 **路由路径匹配配置**
```ts
// ChildApp.vue 的核心逻辑
const currentApp = computed(() => {
const matched = subApps.find((app) =>
route.path.startsWith(app.baseroute)
)
return matched || subApps[0]
})
```
```html
<micro-app
:key="currentApp.url"
:name="currentApp.name"
:url="currentApp.url"
:baseroute="currentApp.baseroute"
<!-- 所有属性都从 currentApp 动态绑定 -->
/>
```
**工作流程:**
1. 用户访问 `/vue3-app/dashboard`
2. Vue Router 匹配到通配路由 → 渲染 `ChildApp.vue`
3. `ChildApp.vue` 内的 `computed` 检查 `route.path` 以哪个 `baseroute` 开头
4. 找到 `vue3-app` 配置 → 绑定到 `<micro-app>` 标签
5. micro-app 框架根据 `name` + `url` 加载对应的子应用
**关键设计点:**
- `:key="currentApp.url"` — 当切换到不同子应用时Vue 会销毁旧的 `<micro-app>` 重建新的
- `keepAlive: true` — 切换回来时不销毁,保持应用状态
- 未匹配到任何 `baseroute` 时的兜底逻辑:`|| subApps[0]`
---
## 二、路由冲突与优先级
### Q4如果主应用有一个 `/about` 路由,子应用也有一个 `/about` 路由,且子应用没有设置 router base访问时会发生什么
**答:**
**结论:不会冲突,展示哪个取决于 URL。**
| 访问路径 | 展示内容 | 原因 |
|----------|----------|------|
| `/about` | 主应用的 About | 主应用路由直接匹配,不经过子应用 |
| `/vue3-app/about` | 子应用的 About | 主应用路由匹配 `/vue3-app/:page*`,交给子应用处理 |
**完整流程图:**
```
浏览器输入: http://localhost:8080/about
主应用 Vue Router 开始匹配
┌───────┴───────┐
│ │
主应用有 /about 主应用没有 /about
│ │
▼ ▼
展示主应用 About 404 / fallback
(子应用完全不会
被加载)
```
```
浏览器输入: http://localhost:8080/vue3-app/about
主应用 Vue Router 匹配
命中 /vue3-app/:page* 通配路由
渲染 ChildApp.vue → 加载 <micro-app>
micro-app 剥离 baseroute(/vue3-app)
传给子应用内部路由: /about
子应用 Vue Router 匹配 /about
展示子应用 About 页面
```
---
### Q5子应用不设置 router base 会有什么隐患?
**答:**
虽然 micro-app 的 `baseroute` 机制能拦截 pushState 并自动拼接前缀,让大部分场景"看起来正常",但仍有 **4 个隐患**
| 隐患 | 说明 | 风险等级 |
|------|------|----------|
| `window.location` 误用 | 代码中直接读 `location.pathname` 会得到完整路径 `/vue3-app/about`,而不是期望的 `/about` | ⭐⭐⭐ 高 |
| 静态资源 404 | `<img src="/assets/logo.png">` 会请求主应用的 8080 端口而非子应用的 3001 端口 | ⭐⭐⭐ 高 |
| 独立运行失败 | 不经过主应用直接访问子应用时base 为空字符串,路由跳转可能出错 | ⭐⭐ 中 |
| `history.go()` 异常 | 直接操作浏览器历史时不经过 micro-app 拦截,可能跳出微前端上下文 | ⭐ 低 |
**最佳实践:**
```ts
// 子应用 router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(
// 动态适配:微前端环境用 baseroute独立运行用 '/'
window.__MICRO_APP_BASE_ROUTE__ || '/'
),
routes: [
{ path: '/about', component: () => import('@/views/About.vue') }
]
})
```
`window.__MICRO_APP_BASE_ROUTE__` 是 micro-app 注入到子应用 window 上的全局变量,值就是主应用中配置的 `baseroute`
---
### Q6micro-app 是如何实现"主应用路由和子应用路由互不干扰"的?
**答:**
通过 **URL 命名空间隔离** 机制:
```
主应用路由空间Vue Router 管理)
├── / → 重定向到 /home
├── /home → Home.vue
├── /about → About.vue (如果定义了)
├── /child-app/:page* → 子应用 vue2-app 的领地
└── /vue3-app/:page* → 子应用 vue3-app 的领地
子应用 vue2-app 路由空间(内部 Router 管理,剥离 /child-app 前缀后)
├── / → 首页
├── /about → 关于页
└── /user/:id → 用户详情
子应用 vue3-app 路由空间(内部 Router 管理,剥离 /vue3-app 前缀后)
├── / → 首页
├── /about → 关于页
└── /dashboard → 控制台
```
**三层隔离保证:**
1. **Vue Router 层级** — 主应用的 `/about` 和通配路由 `/vue3-app/:page*` 互斥匹配,不会同时命中
2. **baseroute 剥离** — micro-app 在将 URL 传给子应用前,自动去除 baseroute 前缀
3. **iframe 沙箱** — Vite 子应用运行在独立 iframe 中,`window.location` 指向自己的 origin
---
## 三、SubAppConfig 接口设计
### Q7`SubAppConfig` 接口各字段的作用和设计考量是什么?
**答:**
```ts
export interface SubAppConfig {
name: string // 全局唯一标识,字母开头
url: string // 子应用 devServer / 生产地址
baseroute: string // 主应用分配给子应用的 URL 命名空间
iframe?: boolean // 是否使用 iframe 沙箱
keepAlive?: boolean // 切换时是否保活
routerMode?: 'native' | 'native-scope'
disableScopecss?: boolean // 禁用样式隔离
disableSandbox?: boolean // 禁用 JS 沙箱
}
```
| 字段 | 为什么可选/必填 | 设计考量 |
|------|----------------|----------|
| `name` | 必填 | micro-app 框架需要唯一标识来管理子应用生命周期 |
| `url` | 必填 | 不知道地址就无法加载;生产环境应通过环境变量注入 |
| `baseroute` | 必填 | 决定 URL 命名空间,也影响子应用的资源路径解析 |
| `iframe` | 可选,默认 false | Vite 子应用必须设为 trueES Module 兼容性) |
| `keepAlive` | 可选 | 高频切换场景开启可提升体验,但占用内存 |
| `routerMode` | 可选 | `native-scope` 提供额外的作用域隔离 |
| `disableScopecss` | 可选 | 调试时临时关闭样式隔离,排查样式问题 |
| `disableSandbox` | 可选 | 调试时临时关闭沙箱,确认是否是沙箱引起的 bug |
---
### Q8为什么 Vite 子应用的 `iframe` 必须设为 `true`
**答:**
见 [基础面试题 Q7](./micro-app面试题.md#q7遇到-cannot-use-import-statement-outside-a-module-报错是什么原因如何解决),核心原因:
```
Vite devServer 输出 ES Module (type="module")
with 沙箱用 new Function() 执行 JS
new Function() 无法解析 import/export 语法
❌ SyntaxError: Cannot use import statement outside a module
```
**解决:** `iframe: true` → 浏览器原生 iframe 天然支持 `<script type="module">`
---
## 四、扩展与自动化
### Q9子应用数量从 1 个增长到 10 个后,路由和导航如何自动管理?
**答:**
将硬编码改为**配置驱动**
**路由自动生成:**
```ts
// router/index.ts
import { subApps } from '@/config/subApps'
const routes = [
{ path: '/', redirect: '/home' },
{ path: '/home', name: 'home', component: () => import('@/views/Home.vue') },
// 自动为每个子应用生成通配路由
...subApps.map(app => ({
path: `${app.baseroute}/:page*`,
name: app.name,
component: () => import('@/views/ChildApp.vue')
}))
]
```
**导航自动生成:**
```html
<!-- App.vue -->
<router-link
v-for="app in subApps"
:key="app.name"
:to="app.baseroute"
>
{{ app.name }}
</router-link>
```
这样新增子应用只需要在 `subApps` 数组中加一行配置,路由和导航自动生效。
---
### Q10如果有两个子应用的 `baseroute` 配置成了同一个值,会发生什么?
**答:**
会出现**不确定行为**
1. **路由层面**Vue Router 按数组顺序匹配,第一个通配路由先命中,后面的永远不会被访问
2. **`ChildApp.vue`**`subApps.find(app => route.path.startsWith(app.baseroute))` 返回数组中第一个匹配的配置
3. **结果**:第二个子应用永远加载不到,且不会有任何报错提示
**防御措施:**
```ts
// subApps.ts — 启动时校验
if (import.meta.env.DEV) {
const routes = subApps.map(a => a.baseroute)
const duplicates = routes.filter((r, i) => routes.indexOf(r) !== i)
if (duplicates.length) {
console.error(`[SubApp] 重复的 baseroute: ${duplicates.join(', ')}`)
}
const names = subApps.map(a => a.name)
const dupNames = names.filter((n, i) => names.indexOf(n) !== i)
if (dupNames.length) {
console.error(`[SubApp] 重复的 name: ${dupNames.join(', ')}`)
}
}
```
---
## 五、环境管理与部署
### Q11开发环境和生产环境子应用 URL 不同,如何管理?
**答:**
使用环境变量:
```ts
// src/config/subApps.ts
export const subApps: SubAppConfig[] = [
{
name: 'vue3-app',
// 开发环境用 localhost生产环境用实际部署地址
url: import.meta.env.VITE_VUE3_APP_URL || 'http://localhost:3001/',
baseroute: '/vue3-app',
iframe: true,
keepAlive: true,
routerMode: 'native'
}
]
```
```bash
# .env.development
VITE_VUE3_APP_URL=http://localhost:3001/
# .env.production
VITE_VUE3_APP_URL=https://vue3-app.your-domain.com/
```
这样开发和部署环境自动切换,无需修改代码。
---
### Q12这套架构中如何保证子应用独立部署时不影响其他子应用
**答:**
三个关键点:
1. **独立端口/域名**:每个子应用有自己的 devServer3001、5173和生产域名
2. **独立的 CI/CD**:每个子应用独立构建、独立发版,不耦合
3. **主应用只做路由分发**:主应用不包含子应用的业务逻辑,子应用挂了只影响自己的路由
**故障隔离效果:**
| 场景 | 影响范围 |
|------|----------|
| 子应用 vue2-app 挂了 | 只有 `/child-app/*` 不可用,主应用和 vue3-app 正常 |
| 主应用挂了 | 所有子应用都不可访问(主应用是入口) |
| 子应用 JS 报错 | iframe 沙箱内错误不污染主应用 |
---
## 六、Vue 3 子应用工程化创建(实战完整流程)
### Q13从零创建一个 Vue 3 + Vite 子应用,需要哪几步?
**答:**
完整流程分 **4 个阶段,共 8 步**
| 阶段 | 步骤 | 操作 |
|------|------|------|
| 脚手架 | 1 | `npm create vite@latest microapp-vue3 -- --template vue-ts` |
| 脚手架 | 2 | `npm install && npm install vue-router@4 @micro-zoe/micro-app` |
| 配置改造 | 3 | 改造 `vite.config.ts`(端口/base/CORS/alias/isCustomElement |
| 配置改造 | 4 | 改造 `tsconfig.app.json``@/*` 路径别名) |
| 入口改造 | 5 | 创建 `vite-env.d.ts`micro-app 全局类型声明) |
| 入口改造 | 6 | 改造 `index.html`(独立挂载点 id |
| 入口改造 | 7 | 改造 `main.ts`mount/unmount 生命周期 + 双模式) |
| 业务代码 | 8 | 创建 router + views完成路由通配和页面 |
下面逐一拆解每个阶段的细节和踩坑点。
---
### Q14子应用的 `vite.config.ts` 需要配置哪些关键项?为什么?
**答:**
完整的 Vite 子应用配置文件如下,每个配置项都有明确的原因:
```ts
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
// ① 路径别名 — 与 tsconfig 的 paths 配合,支持 @/ 导入
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
},
plugins: [
vue({
template: {
compilerOptions: {
// ② 自定义元素注册 — 避免 Vue 对 <micro-app> 标签报警告
isCustomElement: (tag) => /^micro-app/.test(tag)
}
}
})
],
// ③ base 路径 — 必须与主应用的 baseroute 一致
base: '/vue3-app/',
server: {
// ④ 端口 — 子应用独立端口,避免与主应用(8080)或其他子应用冲突
port: 3001,
// ⑤ CORS 头 — 主应用跨域加载子应用资源必须配置
headers: {
'Access-Control-Allow-Origin': '*'
}
}
})
```
| 配置项 | 必须? | 不配置的后果 |
|--------|--------|-------------|
| `resolve.alias` | 推荐 | `import xxx from '@/views/Home.vue'` 无法解析 |
| `isCustomElement` | 推荐 | Vue 警告 `Failed to resolve component: micro-app` |
| `base` | **必须** | 静态资源 404路由跳转路径错乱 |
| `port` | 推荐 | 默认 5173可能与已有子应用冲突 |
| `CORS` | **必须** | 主应用无法加载子应用资源(跨域被浏览器拦截) |
---
### Q15为什么子应用的 `index.html` 挂载点不能用 `#app`
**答:**
这是一个**两个项目共用同一个 DOM 根节点**的经典问题。
```
主应用 index.html <div id="app"></div>
子应用 index.html <div id="app"></div> ← ❌ 冲突!
```
在 iframe 沙箱模式下,子应用运行在独立 iframe 中,理论上 id 重复不会直接冲突。但有两个隐患:
1. **语义混乱** — 维护者无法从 id 区分是主应用还是子应用的根节点
2. **非 iframe 模式风险** — 如果未来改用 with 沙箱(禁用 iframe两个 `#app` 同时存在于同一个 document`mount('#app')` 会挂载到错误的节点,导致页面错乱
**正确做法:** 使用语义化的独立命名:
```html
<!-- 子应用 index.html -->
<div id="microapp-vue3-root"></div>
```
```ts
// 子应用 main.ts — 挂载点保持一致
app.mount('#microapp-vue3-root')
// 卸载时清理
function unmount() {
const root = document.getElementById('microapp-vue3-root')
if (root) {
root.innerHTML = ''
}
}
```
**命名建议:** `<项目名>-root`,如 `microapp-vue3-root``portal-root``dashboard-root`
---
### Q16子应用的 `main.ts` 中 mount/unmount 生命周期是如何设计的?
**答:**
核心模式是 **双模式入口**:微前端环境导出生命周期,独立运行直接挂载。
```ts
import { createApp } from 'vue'
import type { App as VueApp } from 'vue' // ① type-only import
import App from './App.vue'
import router from './router'
let app: VueApp | null = null
function mount(): void {
app = createApp(App)
app.use(router)
app.mount('#microapp-vue3-root') // ② 独立挂载点
}
function unmount(): void {
if (app) {
app.unmount() // ③ Vue 3 销毁
const root = document.getElementById('microapp-vue3-root')
if (root) root.innerHTML = '' // ④ 清空 DOM 残留
app = null
}
}
// ⑤ 环境判断 — 决定使用哪种模式
if (window.__MICRO_APP_ENVIRONMENT__) {
// 微前端环境:导出生命周期给 micro-app 框架调用
window[`micro-app-${window.__MICRO_APP_NAME__}`] = { mount, unmount }
} else {
// 独立运行:直接挂载,方便开发调试
mount()
}
```
**设计要点:**
| 要点 | 说明 |
|------|------|
| `type-only import` | `App` 类型从 `vue` 导入必须用 `import type`,否则生产构建报 `MISSING_EXPORT` 错误 |
| `app: VueApp \| null` | 用闭包变量持有实例引用mount 创建、unmount 销毁 |
| `unmount()` 清理 | Vue 3 的 `app.unmount()` 不会清除 DOM需手动 `innerHTML = ''` |
| 微前端环境判断 | `window.__MICRO_APP_ENVIRONMENT__` 是 micro-app 注入的,框架级保证 |
| 生命周期导出 | `window['micro-app-' + name]` 是 micro-app 约定的命名规则 |
---
### Q17为什么 `import { App } from 'vue'` 在生产构建时会报错?如何修复?
**答:**
这是 Vite 8 / Rolldown 构建时的 **类型导出与运行时分包** 问题。
**报错信息:**
```
[MISSING_EXPORT] "App" is not exported by "node_modules/vue/dist/vue.runtime.esm-bundler.js"
```
**根因分析:**
Vue 3 的 `App` 是一个 **TypeScript 接口**不是运行时值。在生产构建中Vue 的 `esm-bundler` 版本只导出运行时 API`createApp``ref``computed` 等),不导出类型。
```ts
// ❌ 错误写法App 既是值又是类型,构建工具无法区分
import { createApp, App as VueApp } from 'vue'
// ✅ 正确写法:用 import type 明确告知这仅用于类型标注
import { createApp } from 'vue'
import type { App as VueApp } from 'vue'
```
**为什么会混淆?**
- `vue-tsc --noEmit` 类型检查能通过TypeScript 知道 `App` 是类型)
- `vite build` 构建失败Rolldown 试图在运行时模块中找 `App` 的值导出)
**核心原则:** 凡是只用于 TypeScript 类型标注的导入,一律使用 `import type`
```ts
import type { App } from 'vue' // ✅ Vue 实例类型
import type { Router } from 'vue-router' // ✅ 路由实例类型
import type { DefineComponent } from 'vue' // ✅ 组件类型
```
---
### Q18`vite-env.d.ts` 中需要声明哪些 micro-app 相关的全局类型?
**答:**
micro-app 框架会在子应用的 `window` 上注入 4 个关键全局变量,必须显式声明类型:
```ts
/// <reference types="vite/client" />
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<object, object, unknown>
export default component
}
// micro-app 全局类型声明
interface Window {
/** 是否运行在微前端环境中 */
__MICRO_APP_ENVIRONMENT__?: boolean
/** 应用名称(等于主应用中配置的 name */
__MICRO_APP_NAME__?: string
/** 基础路由(等于主应用中配置的 baseroute */
__MICRO_APP_BASE_ROUTE__?: string
/** 公共路径(用于拼接静态资源路径) */
__MICRO_APP_PUBLIC_PATH__?: string
}
```
| 全局变量 | 谁注入 | 典型用途 | 示例值 |
|----------|--------|----------|--------|
| `__MICRO_APP_ENVIRONMENT__` | micro-app | 判断是否在微前端环境,决定导出生命周期还是直接挂载 | `true` / `undefined` |
| `__MICRO_APP_NAME__` | micro-app | 拼接生命周期导出 key`window['micro-app-' + name]` | `'vue3-app'` |
| `__MICRO_APP_BASE_ROUTE__` | micro-app | Vue Router 的 `base` 参数 | `'/vue3-app'` |
| `__MICRO_APP_PUBLIC_PATH__` | micro-app | 拼接静态资源 URL | `'http://localhost:3001/'` |
**面试延伸:** 为什么要声明为 `?:`(可选)而不是必填?
> 因为子应用需要同时支持**独立运行**(直接 `npm run dev` 访问 3001 端口)和**微前端环境**(通过主应用 8080 端口加载)。独立运行时这些变量不存在,所以必须标记为可选。
---
### Q19`tsconfig.app.json` 中 `@/*` 路径别名和 Vite 的 `resolve.alias` 有什么关系?为什么需要两处配置?
**答:**
这对应了**类型系统**和**构建系统**两条独立管线:
```
tsconfig.app.json 的 paths vite.config.ts 的 resolve.alias
│ │
▼ ▼
TypeScript 编译器 Vite/Rolldown 打包器
(类型检查阶段) (构建阶段)
│ │
└───────────┬───────────────────────────────┘
开发者写 import xxx from '@/views/Home.vue'
TypeScript 能找对类型 ✓
Vite 打包能找到文件 ✓
```
**配置对比:**
| 位置 | 配置 | 作用 |
|------|------|------|
| `tsconfig.app.json` | `"paths": { "@/*": ["./src/*"] }` + `"baseUrl": "."` | 让 TS 理解 `@/` 指向 `src/` |
| `vite.config.ts` | `resolve: { alias: { "@": fileURLToPath(...) } }` | 让 Vite 构建时正确解析路径 |
**常见错误:**
| 现象 | 原因 | 修复 |
|------|------|------|
| TS 报红波浪线但能跑 | 只配了 Vite alias没配 tsconfig paths | 补充 `tsconfig.app.json` 的 paths |
| `vue-tsc --noEmit` 报错但能构建 | 同上tsconfig 缺 paths | 同上 |
| 构建报错找不到模块 | 只配了 tsconfig paths没配 Vite alias | 补充 `vite.config.ts` 的 resolve.alias |
---
### Q20子应用开发完成后如何验证它作为 micro-app 子应用的可用性?
**答:**
三层验证策略:
**第一层:类型检查**
```bash
npx vue-tsc --noEmit
```
- 验证所有类型标注正确
- 检查 micro-app 全局变量的类型使用
- 确保 `import type` 用在正确的位置
**第二层:独立构建**
```bash
npm run build
```
- 验证 Vite 生产构建能通过(暴露 `import type` 问题)
- 输出 `dist/` 产物,确认资源路径基于 `/vue3-app/` 正确生成
- 典型坑:`"App" is not exported` → 修复 `import type`
**第三层:联调验证**
```bash
# 终端 1 — 启动子应用(独立模式)
cd microapp-vue3 && npm run dev
# 访问 http://localhost:3001 — 确认页面正常、路由跳转正常
# 终端 2 — 启动主应用
cd microapp-main && npm run dev
# 访问 http://localhost:8080/vue3-app — 确认通过主应用加载正常
```
**验证清单:**
| 检查项 | 通过标准 |
|--------|----------|
| 独立运行首页 | `localhost:3001` 展示 Home 页面,路由跳转正常 |
| 独立运行 about | `localhost:3001/about` 展示 About 页面 |
| 主应用加载 | `localhost:8080/vue3-app` 通过主应用正常渲染子应用 |
| 子应用内路由 | `localhost:8080/vue3-app/about` 展示子应用的 About 页 |
| 环境检测 | Home 页面显示"🟢 微前端环境"而非"🔵 独立运行" |
| 控制台无报错 | 无 CORS 错误、无 MIME type 错误、无 Vue 警告 |
---
### Q21Vite 8 项目中 `erasableSyntaxOnly` 和 `noUnusedLocals` 配置对 micro-app 子应用开发有什么影响?
**答:**
Vite 8 的默认 `tsconfig.app.json` 引入了更严格的 TypeScript 检查:
```json
{
"compilerOptions": {
"noUnusedLocals": true, // 未使用的局部变量报错
"noUnusedParameters": true, // 未使用的参数报错
"erasableSyntaxOnly": true // 仅允许可擦除的 TS 语法
}
}
```
| 选项 | 含义 | 对子应用开发的影响 |
|------|------|-------------------|
| `erasableSyntaxOnly` | 只允许不产生运行时痕迹的 TS 语法 | **强制使用 `import type`** — 因为普通的 `import { App }` 会在 JS 输出中留下导入语句,如果模块不存在该导出,会运行时/构建时出错 |
| `noUnusedLocals` | 未使用的局部变量报错 | 生命周期回调参数如果未使用,需用 `_` 前缀(如 `_e` |
| `noUnusedParameters` | 未使用的函数参数报错 | 同上,回调参数必须消费或以 `_` 开头 |
**`erasableSyntaxOnly` 与 micro-app 的关系:**
这个选项恰好帮我们避免了 Q17 中的构建错误 — 如果开启了 `erasableSyntaxOnly`,你写的 `import { App } from 'vue'` 会在类型检查阶段就报错(因为 `App` 是类型,应使用 `import type`),从而**在开发阶段就暴露了生产构建才会出现的问题**。
这是一个很好的 **"左移"工程实践** — 类型检查期间就捕获构建期错误。
---
### Q22子应用路由中使用 `window.__MICRO_APP_BASE_ROUTE__ || '/'` 的兜底逻辑,在什么场景下会用到 `'/'`
**答:**
`'/'` 作为 fallback 值的设计覆盖了 **两个关键场景**
**场景一:独立开发调试**
```bash
npm run dev
# 子应用独立运行在 localhost:3001
# window.__MICRO_APP_BASE_ROUTE__ === undefined
# router base → '/' ← 走兜底
# 访问 /、/about 正常工作
```
**场景二:单元测试 / E2E 测试**
```ts
// 测试文件中直接创建 router
const router = createRouter({
history: createWebHistory('/'),
routes
})
// 不需要 mock __MICRO_APP_BASE_ROUTE__
```
**如果不写兜底会怎样?**
```ts
// ❌ 没有兜底
const router = createRouter({
history: createWebHistory(window.__MICRO_APP_BASE_ROUTE__)
// 独立运行时createWebHistory(undefined) → 可能报错或行为异常
})
```
**设计哲学:** 子应用不应该依赖微前端环境才能运行。`'/'` 兜底体现了 **"独立可运行"** 的工程原则 — 子应用脱离主应用后依然是一个完整的、可访问的网站。
---
### Q23在子应用中展示"当前运行环境"指示器有什么实用价值?
**答:**
`Home.vue` 中我们设计了环境检测展示:
```vue
<p v-if="isMicroApp">🟢 当前运行在 <strong>micro-app 微前端环境</strong> </p>
<p v-else>🔵 当前<strong>独立运行</strong>模式</p>
```
这个看似简单的小功能有 4 个实用价值:
| 价值 | 说明 |
|------|------|
| **调试可视化** | 一眼看出当前是通过主应用加载还是独立访问,快速定位"为什么行为不同"的问题 |
| **新人友好** | 新加入的开发者不需要理解整个架构就能知道当前运行模式 |
| **排查跨环境 Bug** | "在独立运行正常、主应用里出问题" → 有了指示器更容易复现和对比 |
| **演示/汇报** | 向 Leader 或面试官展示时,直观证明微前端架构确实在工作 |
**延伸设计:** 可以在指示器中进一步展示:
```ts
// 环境信息面板
const envInfo = computed(() => ({
运行模式: isMicroApp ? '微前端环境' : '独立运行',
baseroute: window.__MICRO_APP_BASE_ROUTE__ || '/',
应用名称: window.__MICRO_APP_NAME__ || '独立运行',
实际路径: window.location.pathname
}))
```
---
### Q24这次 Vue3 子应用创建过程中遇到了什么实际 bug是如何修复的
**答:**
实际遇到了**一个构建错误**
**Bug 表现:**
```bash
npm run build
# ❌ Build failed with 1 error:
# [MISSING_EXPORT] "App" is not exported by
# "node_modules/vue/dist/vue.runtime.esm-bundler.js"
# at src/main.ts:1:21
```
**Bug 现场代码:**
```ts
// src/main.ts — 第 1 行
import { createApp, App as VueApp } from 'vue'
// ^^^ 这是类型,不是值
```
**排查过程:**
| 排查步骤 | 结果 | 判断 |
|----------|------|------|
| `vue-tsc --noEmit` | ✅ 零错误 | 类型检查通过,排除了类型标注问题 |
| 检查 `node_modules/vue/dist/` | `vue.runtime.esm-bundler.js` 存在 | 排除安装问题 |
| 在 `vue` 包中搜索 `App` 导出 | 只找到 `interface App`,无运行时导出 | **确认:`App` 是类型,不是值** |
| 检查 `tsconfig.app.json` | `"erasableSyntaxOnly": true` 但未触发报错 | TS 认为用法正确(作为类型的别名) |
**根因:** Vite 8 的 Rolldown 打包器在生产构建时使用 Vue 的 `esm-bundler` 版本,该版本只导出运行时 API。`App` 是 TypeScript 接口,在 JS 运行时不存在。`import { App }` 让 Rolldown 尝试从运行时模块中找 `App`,找不到就报 `MISSING_EXPORT`
**修复:** 将值导入和类型导入分开:
```ts
import { createApp } from 'vue' // 值导入
import type { App as VueApp } from 'vue' // 类型导入 — Rolldown 会删除此行
```
**修复后验证:**
```bash
npx vue-tsc --noEmit # ✅ 类型检查通过
npx vite build # ✅ 构建成功,产物正常
```
**教训总结:**
| 经验 | 说明 |
|------|------|
| `vue-tsc` 通过 ≠ 构建通过 | 类型检查器不模拟打包器的模块解析 |
| `import type` 不是可选的 | 在 Vite 8 项目中使用 Vue 类型,必须用 type-only import |
| `erasableSyntaxOnly` 是好配置 | 理论上能提前发现此类问题(但别名场景未覆盖) |
---
## 七、加载状态与沙箱机制
### Q25`<micro-app>` 标签加载子应用时的 Loading 效果是如何实现的?
**答:**
利用了 micro-app 自定义元素的 **默认插槽slot机制** 配合框架的生命周期,是一个零配置的内置能力。
**核心代码:**
```html
<micro-app
name="vue3-app"
url="http://localhost:3001/"
baseroute="/vue3-app"
@created="onCreated"
@mounted="onMounted"
>
<!-- 🔑 这里的内容就是 Loading 占位 -->
<div class="loading-placeholder">
<div class="spinner"></div>
<p>正在加载子应用 {{ currentApp.name }}...</p>
<p class="loading-hint">
请确保子应用已启动:<code>{{ currentApp.url }}</code>
</p>
</div>
</micro-app>
```
**工作原理(时间线):**
```
时间 →
──────────────────────────────────────────────────────────────
① micro-app 标签插入 DOM
│ 此时 slot 内容立即渲染:
│ ┌─────────────────────────┐
│ │ ⟳ 正在加载子应用... │
│ │ 请确保子应用已启动 │
│ └─────────────────────────┘
② fetch(url) — 请求子应用 HTML
│ [Loading 持续展示]
③ @created 触发 — HTML 已下载,资源正在加载
│ [Loading 持续展示]
④ @beforemount 触发 — 沙箱准备就绪
│ [Loading 持续展示]
⑤ 子应用 JS 执行完毕,根组件挂载完成
│ micro-app 框架做 DOM 替换:
│ 移除 slot 内容 → 插入子应用渲染结果
⑥ @mounted 触发 — 子应用完全渲染
┌─────────────────────────┐
│ 子应用的实际页面内容 │
└─────────────────────────┘
```
| 阶段 | DOM 状态 | 用户看到 |
|------|----------|----------|
| `<micro-app>` 插入 DOM | 框架初始化 | 插槽内容Loading |
| `fetch` 请求中 | 插槽内容 | Loading + spinner 动画 |
| `created` 触发 | 插槽内容 | Loading |
| `beforemount` 触发 | 插槽内容 | Loading |
| 子应用挂载完成 → `mounted` | **插槽被替换为子应用内容** | 子应用页面 |
**为什么不需要手动控制 Loading 的显示/隐藏?**
这是 `<micro-app>` 作为 Web Component 的**原生插槽机制**
```
<micro-app> 的 Shadow DOM 内部逻辑:
if (子应用未就绪) {
渲染 <slot></slot> ← 显示用户传入的 Loading 内容
} else {
渲染子应用的 DOM ← 替换掉 slot
}
```
`mounted` 生命周期触发时micro-app 框架自动用子应用的实际 DOM 替换插槽内容。开发者**不需要手动写 `v-if` / `v-show`** 来控制 loading 和子应用之间的切换。
**Loading 设计的最佳实践:**
| 要素 | 示例 | 作用 |
|------|------|------|
| **加载动画** | CSS spinner 旋转圈 | 视觉反馈,安抚用户等待 |
| **应用标识** | `{{ currentApp.name }}` | 多个子应用时,用户知道正在加载哪个 |
| **调试信息** | `{{ currentApp.url }}` | 开发阶段帮助排查"子应用是否已启动" |
| **错误兜底** | `@error="onError"` | 加载失败时在回调中做降级处理 |
**常见优化:**
```html
<micro-app ... @error="onError">
<!-- 正常加载 -->
<div class="loading-placeholder">
<div class="spinner"></div>
<p>正在加载子应用...</p>
</div>
<!-- 加载失败(可在 error 回调中切换状态来展示) -->
<!-- 这里需要用 Vue 响应式变量控制,因为 slot 的可见性由 micro-app 框架管理 -->
</micro-app>
```
```ts
// error 回调中可以做重试或降级
function onError(error: Error) {
console.error(`[ChildApp] ${currentApp.value.name} 加载失败:`, error)
// 可在此处设置 UI 状态提示用户刷新
}
```
---
### Q26为什么 Vite 子应用必须用 `iframe: true`,而 Webpack 子应用可以用 `iframe: false`
**答:**
根因是两种构建工具产出的 **JS 格式** 不同,导致 micro-app 的 with 沙箱对 Vite 输出的 ES Module 无法执行。
---
#### Webpack 子应用with 沙箱可执行
**Webpack 的代码输出:**
```html
<!-- Webpack dev/build 输出的 HTML -->
<script src="/js/chunk-vendors.js"></script> <!-- 普通脚本 -->
<script src="/js/app.js"></script> <!-- 普通脚本 -->
```
```js
// Webpack 打包后的 JS 格式UMD / IIFE
(function(module, exports, __webpack_require__) {
// 所有模块代码都在一个函数闭包里
// 没有 import/export 语句!
var Vue = __webpack_require__('vue')
// ...
})
```
**关键特征:** Webpack 打包产物是**普通脚本Regular Script**,不包含 `import`/`export` 语句。
**with 沙箱的工作方式:**
```
micro-app with 沙箱执行流程:
1. 获取子应用的 JS 代码(字符串)
2. 用 with(window.proxy) 创建一个作用域链
3. 通过 new Function(code) 执行 JS
伪代码:
const sandboxCode = `
with (this.proxyWindow) {
${子应用的JS代码} ← 普通 JS可以执行 ✅
}
`
const fn = new Function('window', 'document', sandboxCode)
fn(proxyWindow, proxyDocument)
```
因为 Webpack 产出的 JS 是普通的 IIFE/UMD 代码,`new Function()` 完全可以执行它。Proxy 劫持了 `window.document` 等全局变量的访问,实现了沙箱隔离。
---
#### Vite 子应用with 沙箱不可执行(必须用 iframe
**Vite devServer 的代码输出:**
```html
<!-- Vite devServer 返回的 HTML -->
<script type="module" src="/src/main.ts"></script>
```
```js
// Vite 不对源码打包,直接输出 ES Module
import { createApp } from '/node_modules/.vite/deps/vue.js?v=abc123'
import App from '/src/App.vue'
import router from '/src/router/index.ts'
// ↑ 全是 import/export 语句!
```
**关键特征:** Vite devServer 输出的是 **ES Module`<script type="module">`**,包含大量 `import`/`export` 语句。
**with 沙箱为什么失败:**
```
micro-app with 沙箱执行流程:
1. 获取子应用的 JS 代码(字符串)
2. 用 with(window.proxy) 创建作用域
3. 通过 new Function(code) 执行 JS
伪代码:
const sandboxCode = `
with (this.proxyWindow) {
import { createApp } from '...' ← ❌ SyntaxError!
// ↑
// import 语句只能在 Module 上下文中使用
// new Function() 创建的是 Script 上下文,不是 Module 上下文
}
`
const fn = new Function('window', 'document', sandboxCode)
fn(proxyWindow, proxyDocument)
```
**报错本质:**
```
浏览器 JS 规范中有两种脚本类型:
┌─────────────────────────────────────────────────────────┐
│ Script普通脚本 │ ModuleES 模块) │
├───────────────────────────────┼─────────────────────────┤
│ <script src="..."> │ <script type="module"> │
│ 不能用 import/export │ 可以用 import/export │
│ 顶层 this = window │ 顶层 this = undefined │
│ new Function() 创建的 │ ❌ 没有 new Module() │
│ 就是这个类型 │ 这个 API 不存在! │
└───────────────────────────────┴─────────────────────────┘
```
**核心矛盾:** JavaScript 规范里**没有 `new Module()` 这样的 API**。`new Function()` 创建的函数永远运行在 Script 上下文,永远无法解析 `import`/`export`。这是 ECMAScript 规范层面的限制,不是 micro-app 的 bug。
---
#### iframe 沙箱为什么能解决
```
┌─────────────────────────────────────────────┐
│ 主应用页面 (8080) │
│ │
│ <micro-app iframe="true" url="...3001"> │
│ │ │
│ │ 创建 iframe │
│ ▼ │
│ ┌──────────────────────────────────────┐ │
│ │ iframe (沙箱) │ │
│ │ │ │
│ │ <html> │ │
│ │ <script type="module" src="..."> │ │
│ │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ │ │
│ │ iframe 是浏览器原生文档环境 │ │
│ │ 天然支持 ES Module │ │
│ │ </html> │ │
│ └──────────────────────────────────────┘ │
└─────────────────────────────────────────────┘
```
iframe 是浏览器原生的独立文档环境,拥有完整的浏览器能力——包括 `<script type="module">` 的解析和执行。Vite 的所有 ES Module 在 iframe 中可以正常工作,因为浏览器内核直接处理模块加载。
---
#### 完整对比表
| 维度 | Webpack (with 沙箱) | Vite (iframe 沙箱) |
|------|---------------------|--------------------|
| **产物格式** | UMD / IIFE 普通脚本 | ES Module |
| **`<script>` 标签** | `<script src="...">` | `<script type="module" src="...">` |
| **含 import/export** | ❌ 不含有 | ✅ 大量 |
| **`new Function()` 能执行?** | ✅ 可以(普通 JS | ❌ 不行(模块语法) |
| **沙箱方案** | with + Proxy 代理 window | 浏览器原生 iframe |
| **性能** | 较快(无额外渲染上下文) | 略慢(需创建 iframe |
| **隔离性** | 中等Proxy 可被绕过) | 强(浏览器原生隔离) |
| **Vite 兼容** | ❌ 不兼容 | ✅ 兼容 |
| **配置** | `iframe: false`(默认) | `iframe: true`(必须) |
---
#### 深入追问Vite 生产构建后也不能用 with 沙箱吗?
**答:即使生产构建也不行。**
Vite 生产构建虽然打包了代码,但内部仍然使用 **动态 import****ES Module 语法**
```js
// Vite 生产构建产物(简化)
const __vite__mapDeps = (i) => {
// 大量 import() 动态导入,用于 code splitting
}
// 仍然是 Module 格式
```
而且 Vite 的子应用在生产环境下图片、字体等静态资源的路径依赖 `base` 配置和 ES Module 的 `import.meta.url`,这些在 with 沙箱的 `new Function()` 上下文中同样不可用。
**结论:只要是 Vite 项目,无论开发还是生产,一律 `iframe: true`。**
---
#### 延伸Webpack 子应用也能用 iframe 吗?
**答:可以,但不是必须。**
Webpack 子应用用 with 沙箱就能正常工作,用 iframe 也行——只是在不需要的情况下创建额外的 iframe 略微浪费资源。选择逻辑:
```
子应用构建工具是什么?
├── Vite → 必须 iframe: true
new Function() 无法处理 ES Module
└── Webpack → iframe: false 即可
UMD/IIFE 在 new Function() 中正常执行)
└── 但如果对隔离性要求极高
→ 也可以设 iframe: true牺牲少量性能换更强隔离
```
---
## 附录:接入新子应用检查清单
| 步骤 | 文件 | 操作 |
|------|------|------|
| 1 | `src/config/subApps.ts` | 在 `subApps` 数组中添加新条目 |
| 2 | `src/router/index.ts` | 添加 `path: '/xxx/:page*'` 通配路由 |
| 3 | `src/App.vue` | 导航栏添加 `<router-link to="/xxx">` |
| 4 | 子应用 vite.config.ts | 设置 `base: '/xxx/'` 或使用 `__MICRO_APP_BASE_ROUTE__` |
| 5 | 子应用 router | `createWebHistory(window.__MICRO_APP_BASE_ROUTE__ \|\| '/')` |
| 6 | 子应用 devServer | 配置 CORS 头 `Access-Control-Allow-Origin: *` |
| 7 | 验证 | 启动子应用 → 启动主应用 → 访问对应路由确认加载成功 |