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

43 KiB
Raw Blame History

micro-app 多子应用接入实战面试题

基于在已有主应用中接入第二个 Vue 3 + Vite 子应用(端口 3001的完整实战经验整理。


一、多子应用注册与路由

Q1如何在已有 micro-app 主应用中新增一个子应用?需要改哪些文件?

答: 只需要改 3 个文件,遵循「配置 → 路由 → 导航」的顺序:

1. src/config/subApps.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 — 添加通配路由

const routes = [
  // ... 其他路由
  {
    path: '/vue3-app/:page*',   // ✅ 新增:通配符匹配子应用所有内部路由
    name: 'vue3App',
    component: () => import('@/views/ChildApp.vue')
  }
]

3. src/App.vue — 导航栏添加入口

<router-link to="/vue3-app">Vue3 子应用</router-link>

不需要改的文件:

  • ChildApp.vue — 容器组件是通用的,根据 URL 自动匹配配置
  • main.tsmicroApp.start() 是框架级初始化,不绑定具体子应用
  • vite.config.tsisCustomElement 配置对所有 <micro-app> 标签生效

Q2为什么要设计成"3 个文件改动"的模式?能否进一步减少?

答: 这是 micro-app 架构设计中最简洁的扩展模式:

改动 为什么必须
subApps.ts 子应用的元信息name、url、baseroute必须有地方存储
router/index.ts Vue Router 需要知道哪些 URL 前缀交给子应用处理
App.vue 用户需要入口点击进入子应用

能否合并?

可以。如果子应用数量很多,可以优化为自动注册模式:

// 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 一个文件。


Q3ChildApp.vue 组件是如何做到"一个组件服务所有子应用"的?

答: 核心原理是通过 路由路径匹配配置

// ChildApp.vue 的核心逻辑
const currentApp = computed(() => {
  const matched = subApps.find((app) =>
    route.path.startsWith(app.baseroute)
  )
  return matched || subApps[0]
})
<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 拦截,可能跳出微前端上下文

最佳实践:

// 子应用 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 接口设计

Q7SubAppConfig 接口各字段的作用和设计考量是什么?

答:

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,核心原因:

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 个后,路由和导航如何自动管理?

答: 将硬编码改为配置驱动

路由自动生成:

// 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')
  }))
]

导航自动生成:

<!-- 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.vuesubApps.find(app => route.path.startsWith(app.baseroute)) 返回数组中第一个匹配的配置
  3. 结果:第二个子应用永远加载不到,且不会有任何报错提示

防御措施:

// 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 不同,如何管理?

答: 使用环境变量:

// 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'
  }
]
# .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.tsmicro-app 全局类型声明)
入口改造 6 改造 index.html(独立挂载点 id
入口改造 7 改造 main.tsmount/unmount 生命周期 + 双模式)
业务代码 8 创建 router + views完成路由通配和页面

下面逐一拆解每个阶段的细节和踩坑点。


Q14子应用的 vite.config.ts 需要配置哪些关键项?为什么?

答: 完整的 Vite 子应用配置文件如下,每个配置项都有明确的原因:

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 同时存在于同一个 documentmount('#app') 会挂载到错误的节点,导致页面错乱

正确做法: 使用语义化的独立命名:

<!-- 子应用 index.html -->
<div id="microapp-vue3-root"></div>
// 子应用 main.ts — 挂载点保持一致
app.mount('#microapp-vue3-root')

// 卸载时清理
function unmount() {
  const root = document.getElementById('microapp-vue3-root')
  if (root) {
    root.innerHTML = ''
  }
}

命名建议: <项目名>-root,如 microapp-vue3-rootportal-rootdashboard-root


Q16子应用的 main.ts 中 mount/unmount 生命周期是如何设计的?

答: 核心模式是 双模式入口:微前端环境导出生命周期,独立运行直接挂载。

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 版本只导出运行时 APIcreateApprefcomputed 等),不导出类型。

// ❌ 错误写法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

import type { App } from 'vue'           // ✅ Vue 实例类型
import type { Router } from 'vue-router'  // ✅ 路由实例类型
import type { DefineComponent } from 'vue' // ✅ 组件类型

Q18vite-env.d.ts 中需要声明哪些 micro-app 相关的全局类型?

答: micro-app 框架会在子应用的 window 上注入 4 个关键全局变量,必须显式声明类型:

/// <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 拼接生命周期导出 keywindow['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 端口加载)。独立运行时这些变量不存在,所以必须标记为可选。


Q19tsconfig.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 子应用的可用性?

答: 三层验证策略:

第一层:类型检查

npx vue-tsc --noEmit
  • 验证所有类型标注正确
  • 检查 micro-app 全局变量的类型使用
  • 确保 import type 用在正确的位置

第二层:独立构建

npm run build
  • 验证 Vite 生产构建能通过(暴露 import type 问题)
  • 输出 dist/ 产物,确认资源路径基于 /vue3-app/ 正确生成
  • 典型坑:"App" is not exported → 修复 import type

第三层:联调验证

# 终端 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 项目中 erasableSyntaxOnlynoUnusedLocals 配置对 micro-app 子应用开发有什么影响?

答: Vite 8 的默认 tsconfig.app.json 引入了更严格的 TypeScript 检查:

{
  "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 值的设计覆盖了 两个关键场景

场景一:独立开发调试

npm run dev
# 子应用独立运行在 localhost:3001
# window.__MICRO_APP_BASE_ROUTE__ === undefined
# router base → '/'  ← 走兜底
# 访问 /、/about 正常工作

场景二:单元测试 / E2E 测试

// 测试文件中直接创建 router
const router = createRouter({
  history: createWebHistory('/'),
  routes
})
// 不需要 mock __MICRO_APP_BASE_ROUTE__

如果不写兜底会怎样?

// ❌ 没有兜底
const router = createRouter({
  history: createWebHistory(window.__MICRO_APP_BASE_ROUTE__)
  // 独立运行时createWebHistory(undefined) → 可能报错或行为异常
})

设计哲学: 子应用不应该依赖微前端环境才能运行。'/' 兜底体现了 "独立可运行" 的工程原则 — 子应用脱离主应用后依然是一个完整的、可访问的网站。


Q23在子应用中展示"当前运行环境"指示器有什么实用价值?

答:Home.vue 中我们设计了环境检测展示:

<p v-if="isMicroApp">🟢 当前运行在 <strong>micro-app 微前端环境</strong> </p>
<p v-else>🔵 当前<strong>独立运行</strong>模式</p>

这个看似简单的小功能有 4 个实用价值:

价值 说明
调试可视化 一眼看出当前是通过主应用加载还是独立访问,快速定位"为什么行为不同"的问题
新人友好 新加入的开发者不需要理解整个架构就能知道当前运行模式
排查跨环境 Bug "在独立运行正常、主应用里出问题" → 有了指示器更容易复现和对比
演示/汇报 向 Leader 或面试官展示时,直观证明微前端架构确实在工作

延伸设计: 可以在指示器中进一步展示:

// 环境信息面板
const envInfo = computed(() => ({
  运行模式: isMicroApp ? '微前端环境' : '独立运行',
  baseroute: window.__MICRO_APP_BASE_ROUTE__ || '/',
  应用名称: window.__MICRO_APP_NAME__ || '独立运行',
  实际路径: window.location.pathname
}))

Q24这次 Vue3 子应用创建过程中遇到了什么实际 bug是如何修复的

答: 实际遇到了一个构建错误

Bug 表现:

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 现场代码:

// 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

修复: 将值导入和类型导入分开:

import { createApp } from 'vue'       // 值导入
import type { App as VueApp } from 'vue'  // 类型导入 — Rolldown 会删除此行

修复后验证:

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机制 配合框架的生命周期,是一个零配置的内置能力。

核心代码:

<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" 加载失败时在回调中做降级处理

常见优化:

<micro-app ... @error="onError">
  <!-- 正常加载 -->
  <div class="loading-placeholder">
    <div class="spinner"></div>
    <p>正在加载子应用...</p>
  </div>
  
  <!-- 加载失败(可在 error 回调中切换状态来展示) -->
  <!-- 这里需要用 Vue 响应式变量控制,因为 slot 的可见性由 micro-app 框架管理 -->
</micro-app>
// 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 的代码输出:

<!-- Webpack dev/build 输出的 HTML -->
<script src="/js/chunk-vendors.js"></script>    <!-- 普通脚本 -->
<script src="/js/app.js"></script>              <!-- 普通脚本 -->
// 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 的代码输出:

<!-- Vite devServer 返回的 HTML -->
<script type="module" src="/src/main.ts"></script>
// 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() 这样的 APInew 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 生产构建虽然打包了代码,但内部仍然使用 动态 importES Module 语法

// 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 验证 启动子应用 → 启动主应用 → 访问对应路由确认加载成功