Files
microapp-main-interview/docs/micro-app面试题.md

17 KiB
Raw Blame History

@micro-zoe/micro-app 微前端面试题

基于 Vue 3 + Vite 主应用搭建实战经验整理,涵盖从项目初始化、配置、到 bug 排查的全流程。


一、基础概念

Q1什么是微前端micro-app 相比其他方案有什么优势?

答: 微前端是一种将多个独立的前端应用组合成一个统一应用的架构模式。每个子应用可以独立开发、独立部署、使用不同的技术栈。

@micro-zoe/micro-app(京东出品)的核心优势:

  • 零依赖:不依赖 single-spa完全自研
  • 类 WebComponent:使用 <micro-app> 标签加载子应用,对开发者透明
  • 双沙箱模式:同时支持 with 沙箱和 iframe 沙箱
  • 不限制技术栈主应用和子应用可以任意组合Vue 2/3、React、原生 HTML 等)
  • 兼容 Vite:通过 iframe 模式完美支持 Vite 构建的子应用
对比维度 micro-app qiankun
接入成本 低(类 WebComponent 中(需改造子应用入口)
Vite 支持 原生支持iframe 模式) 需额外配置
沙箱方案 with + iframe 双模式 基于 Proxy 的沙箱
包体积 ~300KB ~100KB

Q2micro-app 主应用和子应用之间是如何隔离的?

答: micro-app 提供三层隔离

  1. 样式隔离disable-scopecss,默认开启):

    • 为子应用的每个样式规则自动添加 micro-app[name=xxx] 前缀,限制样式作用域
  2. JS 沙箱隔离disable-sandbox,默认开启):

    • with 沙箱:通过 with(window.proxy) + new Function() 执行子应用 JS将子应用的全局变量操作代理到隔离的 microWindow 上
    • iframe 沙箱:将子应用放入 iframe 中运行,利用浏览器原生的 iframe 隔离
  3. 元素隔离shadowDOM,可选):

    • 将子应用放入 Shadow DOM彻底隔离 DOM 树

二、项目搭建

Q3使用 Vue 3 + Vite 搭建 micro-app 主应用需要哪些步骤?

答: 核心 4 步:

// 1. 安装依赖
npm install @micro-zoe/micro-app vue-router@4

// 2. vite.config.ts — 将 <micro-app> 注册为自定义元素
export default defineConfig({
  plugins: [
    vue({
      template: {
        compilerOptions: {
          isCustomElement: (tag) => /^micro-app/.test(tag)
        }
      }
    })
  ],
  server: { port: 8080 }
})

// 3. main.ts — 在 Vue 实例化前启动 micro-app
import microApp from '@micro-zoe/micro-app'
microApp.start({ /* 全局配置 */ })
createApp(App).use(router).mount('#app')

// 4. 页面中使用 <micro-app> 标签加载子应用
<micro-app name="my-app" url="http://localhost:3000/" baseroute="/my-app" />

Q4为什么要在 vite.config.ts 中配置 isCustomElement

答: <micro-app> 是一个自定义 HTML 标签,不是 Vue 组件。Vue 模板编译器默认会把所有非标准 HTML 标签当作 Vue 组件去寻找,找不到就会报警告:

[Vue warn]: Failed to resolve component: micro-app

通过 isCustomElement: (tag) => /^micro-app/.test(tag) 告诉编译器:以 micro-app 开头的标签是自定义元素,跳过 Vue 组件解析。

延伸:如果子应用中也使用了 <micro-app>(嵌套微前端),同样需要配置。


Q5主应用路由如何设计才能正确匹配子应用

答: 使用 Vue Router 的通配符 :page* 匹配子应用所有内部路由:

const routes = [
  { path: '/', redirect: '/home' },
  { path: '/home', component: () => import('@/views/Home.vue') },
  // 关键::page* 匹配 /child-app 及其下所有子路由
  { path: '/child-app/:page*', component: () => import('@/views/ChildApp.vue') }
]

为什么用 :page* 而不是 *

  • * 只在 Vue Router 3 中可用
  • Vue Router 4 中必须使用 :page*(或 :pathMatch(.*)*)来匹配多级路径
  • 这样 /child-app/page1/child-app/a/b/c 都能被正确捕获

三、沙箱机制(重点 Bug 来源)

Q6micro-app 的两种沙箱模式有什么区别?如何选择?

答:

特性 with 沙箱 iframe 沙箱
配置 iframe: false(默认) iframe: true
原理 with(window.proxy) + new Function() 浏览器原生 iframe
ES Module 不支持 import/export 原生支持
性能 较快(无额外渲染) 略慢(需创建 iframe
隔离性 中等Proxy 代理) 强(浏览器原生隔离)
调试体验 较好(同 document 较差(跨 iframe
适用场景 Webpack 构建的 UMD 应用 Vite 构建的应用

选择原则:

  • Vite 子应用 → 必须 iframe: true
  • Webpack UMD 子应用 → iframe: false 即可
  • 对样式/JS 隔离要求极高的场景 → iframe: true

Q7遇到 Cannot use import statement outside a module 报错,是什么原因?如何解决?

答: 这是 micro-app 主应用对接 Vite 子应用时必踩的坑

报错堆栈:

[micro-app] app vue2-app: {
  error: SyntaxError: Cannot use import statement outside a module
  at new Function (<anonymous>)
}

根因分析:

  1. Vite 开发服务器输出的 JS 是 <script type="module"> 格式的 ES Module
  2. micro-app 默认的 with 沙箱 使用 new Function() 来执行子应用的 JS 代码
  3. new Function() 创建的函数运行在「非模块」作用域,无法解析 import/export 语法

解决方案:

// src/config/subApps.ts
{
  name: 'vue2-app',
  url: 'http://localhost:5173/',
  baseroute: '/child-app',
  iframe: true,  // ← 关键:开启 iframe 沙箱
}

为什么 iframe 能解决? iframe 是浏览器原生的独立文档环境Vite 的 <script type="module"> 在 iframe 中可以正常执行。


Q8new Function()eval() 有什么区别?为什么 micro-app 用 new Function()

答:

特性 eval() new Function()
作用域 访问当前局部作用域 只能访问全局作用域
性能 慢(影响局部变量优化) 较快(引擎可优化)
安全性 较低(泄漏局部变量) 较高(作用域隔离)

micro-app 选择 new Function() 的原因:

  • 它创建的函数只在全局作用域执行,配合 with(window.proxy) 可以精确控制子应用对全局变量的访问
  • eval() 会渗透到调用方的局部变量,无法做到完全隔离

但局限性也很明显: new Function() 无法处理 ES Module 语法,这就是为什么 Vite 子应用必须使用 iframe 模式。


四、TypeScript 类型问题

Q9micro-app 的 lifeCycles 回调中,为什么 e.name 会报 TS 类型错误?

答: 这是 TypeScript 类型定义与实际使用的差异问题。

错误代码:

lifeCycles: {
  created(e) {
    console.log(e.name) // TS2339: Property 'name' does not exist on type 'CustomEvent<any>'
  }
}

根因: 根据 @micro-zoe/micro-app 的类型定义文件 global.d.ts

interface lifeCyclesType {
  created?(e: CustomEvent, appName: string): void
  beforemount?(e: CustomEvent, appName: string): void
  // ...
}

生命周期回调接收两个参数:第一个是 CustomEvent 事件对象,第二个才是 appName 字符串

正确写法:

lifeCycles: {
  created(_e, appName) {
    console.log(`子应用 ${appName} 被创建`)
  }
}

Q10vue-tsc --noEmit 在 CI/CD 中有什么作用?我们遇到了什么类型错误?

答: vue-tsc --noEmit 是 Vue 项目的类型检查命令:

  • 只做类型检查,不输出编译产物
  • 相当于 tsc --noEmit,但额外支持 .vue 文件

我们在项目中遇到了两类类型错误:

  1. lifeCycles 回调参数类型错误e.name 不存在于 CustomEvent 类型 → 改用第二个参数 appName
  2. 未使用的导入getSubAppConfigChildApp.vue 中导入但未使用 → 移除无用导入

配置建议:package.json 中将类型检查加入构建流程:

{
  "scripts": {
    "build": "vue-tsc --noEmit && vite build"
  }
}

五、子应用容器与通信

Q11<micro-app> 标签有哪些核心属性?各有什么作用?

答:

属性 类型 说明
name string 必填,子应用唯一标识,字母开头
url string 必填子应用入口地址HTML 路径)
baseroute string 路由前缀,主应用分配给子应用的路由基路径
iframe boolean 是否使用 iframe 沙箱Vite 子应用必须开启)
keep-alive boolean 保活子应用,切换路由时不销毁
router-mode string 路由模式:native(原生同步)/ native-scope(带作用域)
disable-scopecss boolean 禁用样式隔离
disable-sandbox boolean 禁用 JS 沙箱(调试时可用)
data object 向子应用传递的初始数据
shadowDOM boolean 是否使用 Shadow DOM

Q12主应用如何向子应用传递数据有哪几种方式

答: 三种方式:

方式一:通过 data 属性(初始化传参)

<micro-app name="app1" url="..." :data="{ token: 'abc', userId: 1 }" />
// 子应用接收
const data = window.microApp?.getData()

方式二:通过 microApp.setData() API动态传参

import microApp from '@micro-zoe/micro-app'
microApp.setData('app1', { path: '/detail' })
// 子应用监听
window.microApp?.addDataListener((data) => {
  console.log('收到数据:', data)
  if (data.path) router.push(data.path)
}, true) // autoTrigger = true初始化时立即触发一次

方式三:通过 EventCenterForMicroApp(双向通信)

// 主应用
import { EventCenterForMicroApp } from '@micro-zoe/micro-app'
const eventCenter = new EventCenterForMicroApp('app1')
eventCenter.dispatch({ type: '通知', payload: {} })
// 子应用
window.microApp?.dispatch({ type: '回复', payload: {} })

Q13keep-alive 保活机制的原理是什么?有什么注意事项?

答: 原理:当 <micro-app> 元素从 DOM 中移除时(如路由切换),不执行 unmount(),而是调用 hiddenKeepAliveApp() 将子应用隐藏。当元素重新插入 DOM 时,调用 showKeepAliveApp() 恢复显示。

优点: 避免重复加载资源,保持子应用状态

注意事项:

  • 保活的子应用仍然占用内存,需注意内存泄漏
  • 子应用的定时器、事件监听需要在 unmount 生命周期中手动清理
  • microApp.getActiveApps({ excludeHiddenApp: false }) 可以查看所有活跃应用(包括隐藏的保活应用)

六、生命周期

Q14micro-app 子应用加载经历了哪些生命周期?顺序是什么?

答:

主应用生命周期(全局 lifeCycles
created → beforemount → (beforeshow) → mounted → (afterhidden) → unmount

子应用内部生命周期window 上的钩子):
window.mount() → window.unmount()

完整流程:

阶段 全局回调 说明
资源加载完成 created HTML 和静态资源已下载
挂载前 beforemount 沙箱准备就绪,即将渲染
挂载前keep-alive 恢复) beforeshow 保活应用恢复显示前触发
渲染完成 mounted 子应用已渲染到容器中
保活隐藏后 afterhidden 保活应用被隐藏后触发
卸载 unmount 子应用被销毁
报错 error 加载或执行出错时触发

Q15全局 lifeCycles 和子应用内部的 mount/unmount 有什么区别?

答:

维度 全局 lifeCycles 子应用 mount/unmount
定义位置 主应用 microApp.start() 子应用 window['micro-app-xxx']
关注点 框架层面的加载状态 子应用业务层面的渲染状态
参数 (e: CustomEvent, appName: string) 无参数(或接收主应用传递的配置)
典型用途 全局 loading 控制、埋点 初始化 Vue Router、挂载根组件
// 子应用Vue 2入口 — 导出生命周期
if (window.__MICRO_APP_ENVIRONMENT__) {
  window[`micro-app-${window.__MICRO_APP_NAME__}`] = {
    mount() {
      instance = new Vue({ router, render: h => h(App) }).$mount('#child-app-root')
    },
    unmount() {
      instance.$destroy()
      instance.$el.innerHTML = ''
    }
  }
} else {
  // 独立运行时直接渲染
  new Vue({ router, render: h => h(App) }).$mount('#app')
}

七、Vue 2 子应用对接

Q16Vue 2 Webpack 子应用需要做哪些改造才能接入 micro-app

答: 需要 4 处改造:

1. webpack 配置 — UMD 输出

// vue.config.js 或 webpack.config.js
module.exports = {
  output: {
    libraryTarget: 'umd',
    library: 'vue2App'
  }
}

2. devServer 配置 — 跨域

module.exports = {
  devServer: {
    port: 3000,
    headers: {
      'Access-Control-Allow-Origin': '*'
    }
  }
}

3. 入口文件 — 导出生命周期

let instance = null

function mount() {
  instance = new Vue({ router, render: h => h(App) }).$mount('#child-app-root')
}

function unmount() {
  instance.$destroy()
  instance.$el.innerHTML = ''
}

if (window.__MICRO_APP_ENVIRONMENT__) {
  window[`micro-app-${window.__MICRO_APP_NAME__}`] = { mount, unmount }
} else {
  mount()
}

4. 路由 base — 动态适配

const router = new VueRouter({
  mode: 'history',
  base: window.__MICRO_APP_BASE_ROUTE__ || '/',
  routes
})

Q17子应用挂载的 DOM 元素 id 为什么不能用 #app

答: 因为主应用已经使用了 <div id="app"></div>。如果子应用也挂载到 #app,会与主应用的根节点冲突,导致:

  • 子应用覆盖主应用内容
  • 样式污染
  • 路由混乱

正确做法: 子应用使用独立的 id

<!-- 子应用的 index.html -->
<div id="child-app-root"></div>
// 子应用入口
new Vue({ ... }).$mount('#child-app-root')

八、排错与调试

Q18子应用加载白屏如何排查

答: 按以下顺序排查:

  1. 检查跨域:浏览器 Network 面板看子应用资源是否被 CORS 阻止 → 确认子应用 devServer 配置了 Access-Control-Allow-Origin: *

  2. 检查控制台错误

    • Cannot use import statement outside a module → 子应用是 Vite 项目,需配置 iframe: true
    • Failed to resolve component: micro-app → Vite 未配置 isCustomElement
    • Unexpected token '<' → 子应用静态资源路径错误,检查 url 配置
  3. 检查沙箱模式:临时设置 disable-sandbox: true 确认是否是沙箱引起的问题

  4. 检查子应用生命周期:在 created/mounted/error 回调中打印日志,确认子应用加载到哪一步

  5. 查看 micro-app 日志:启用 __DEV__ 模式可看到详细日志


Q19为什么 Vite 子应用的静态资源会 404

答: 两个常见原因:

1. 资源路径使用绝对路径:

<!-- 子应用中这样写会 404因为主应用在 localhost:8080 -->
<script src="/assets/index.js"></script>
<!-- 实际应该请求子应用 localhost:5173/assets/index.js -->

解决:子应用 Vite 配置 base 或使用相对路径

2. baseroutebase 不匹配:

// 主应用
<micro-app baseroute="/child-app" />

// 子应用 vite.config.ts
export default defineConfig({
  base: '/child-app/'  // 必须与 baseroute 一致
})

九、实战总结

Q20这套微前端架构在生产环境中部署需要注意什么

答:

  1. 子应用独立部署:每个子应用有自己独立的 CI/CD 流水线,版本号独立管理

  2. 统一资源路径管理:通过环境变量管理各子应用的 URL

    export const subApps = [{
      name: 'vue2-app',
      url: import.meta.env.VITE_CHILD_APP_URL || 'http://localhost:5173/',
      baseroute: '/child-app',
    }]
    
  3. Nginx 配置

    • 所有子应用必须配置 CORS 头
    • 主应用和子应用建议部署在同源下(同协议、同域名、同端口),可避免大量跨域问题
    • 如果异构部署,需要配置反向代理统一转发
  4. 公共依赖提取:将 Vue、Vue Router 等公共依赖配置为 external避免重复加载

    // vite.config.ts
    build: {
      rollupOptions: {
        external: ['vue', 'vue-router']
      }
    }
    
  5. 灰度发布:利用 microApp.start()plugins.global 机制,可以在加载子应用 HTML 时做 A/B 测试控制

  6. 错误兜底:配置全局 error 回调 + 子应用容器的 @error 事件,展示友好的降级页面


附录:快速检查清单

检查项 正确配置
Vite isCustomElement (tag) => /^micro-app/.test(tag)
Vite 子应用沙箱 iframe: true(必须)
Webpack 子应用沙箱 iframe: false 即可
子应用跨域 Access-Control-Allow-Origin: *
子应用挂载 id 不能用 #app,需独立命名
路由通配符Vue Router 4 :page*,不是 *
lifeCycles 回调参数 (e: CustomEvent, appName: string)
子应用路由 base window.__MICRO_APP_BASE_ROUTE__ || '/'