17 KiB
@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 |
Q2:micro-app 主应用和子应用之间是如何隔离的?
答: micro-app 提供三层隔离:
-
样式隔离(
disable-scopecss,默认开启):- 为子应用的每个样式规则自动添加
micro-app[name=xxx]前缀,限制样式作用域
- 为子应用的每个样式规则自动添加
-
JS 沙箱隔离(
disable-sandbox,默认开启):- with 沙箱:通过
with(window.proxy)+new Function()执行子应用 JS,将子应用的全局变量操作代理到隔离的 microWindow 上 - iframe 沙箱:将子应用放入 iframe 中运行,利用浏览器原生的 iframe 隔离
- with 沙箱:通过
-
元素隔离(
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 来源)
Q6:micro-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>)
}
根因分析:
- Vite 开发服务器输出的 JS 是
<script type="module">格式的 ES Module - micro-app 默认的 with 沙箱 使用
new Function()来执行子应用的 JS 代码 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 中可以正常执行。
Q8:new Function() 和 eval() 有什么区别?为什么 micro-app 用 new Function()?
答:
| 特性 | eval() |
new Function() |
|---|---|---|
| 作用域 | 访问当前局部作用域 | 只能访问全局作用域 |
| 性能 | 慢(影响局部变量优化) | 较快(引擎可优化) |
| 安全性 | 较低(泄漏局部变量) | 较高(作用域隔离) |
micro-app 选择 new Function() 的原因:
- 它创建的函数只在全局作用域执行,配合
with(window.proxy)可以精确控制子应用对全局变量的访问 eval()会渗透到调用方的局部变量,无法做到完全隔离
但局限性也很明显: new Function() 无法处理 ES Module 语法,这就是为什么 Vite 子应用必须使用 iframe 模式。
四、TypeScript 类型问题
Q9:micro-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} 被创建`)
}
}
Q10:vue-tsc --noEmit 在 CI/CD 中有什么作用?我们遇到了什么类型错误?
答:
vue-tsc --noEmit 是 Vue 项目的类型检查命令:
- 只做类型检查,不输出编译产物
- 相当于
tsc --noEmit,但额外支持.vue文件
我们在项目中遇到了两类类型错误:
- lifeCycles 回调参数类型错误:
e.name不存在于CustomEvent类型 → 改用第二个参数appName - 未使用的导入:
getSubAppConfig在ChildApp.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: {} })
Q13:keep-alive 保活机制的原理是什么?有什么注意事项?
答:
原理:当 <micro-app> 元素从 DOM 中移除时(如路由切换),不执行 unmount(),而是调用 hiddenKeepAliveApp() 将子应用隐藏。当元素重新插入 DOM 时,调用 showKeepAliveApp() 恢复显示。
优点: 避免重复加载资源,保持子应用状态
注意事项:
- 保活的子应用仍然占用内存,需注意内存泄漏
- 子应用的定时器、事件监听需要在
unmount生命周期中手动清理 - 用
microApp.getActiveApps({ excludeHiddenApp: false })可以查看所有活跃应用(包括隐藏的保活应用)
六、生命周期
Q14:micro-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 子应用对接
Q16:Vue 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:子应用加载白屏,如何排查?
答: 按以下顺序排查:
-
检查跨域:浏览器 Network 面板看子应用资源是否被 CORS 阻止 → 确认子应用 devServer 配置了
Access-Control-Allow-Origin: * -
检查控制台错误:
Cannot use import statement outside a module→ 子应用是 Vite 项目,需配置iframe: trueFailed to resolve component: micro-app→ Vite 未配置isCustomElementUnexpected token '<'→ 子应用静态资源路径错误,检查url配置
-
检查沙箱模式:临时设置
disable-sandbox: true确认是否是沙箱引起的问题 -
检查子应用生命周期:在
created/mounted/error回调中打印日志,确认子应用加载到哪一步 -
查看 micro-app 日志:启用
__DEV__模式可看到详细日志
Q19:为什么 Vite 子应用的静态资源会 404?
答: 两个常见原因:
1. 资源路径使用绝对路径:
<!-- 子应用中这样写会 404,因为主应用在 localhost:8080 -->
<script src="/assets/index.js"></script>
<!-- 实际应该请求子应用 localhost:5173/assets/index.js -->
解决:子应用 Vite 配置 base 或使用相对路径
2. baseroute 与 base 不匹配:
// 主应用
<micro-app baseroute="/child-app" />
// 子应用 vite.config.ts
export default defineConfig({
base: '/child-app/' // 必须与 baseroute 一致
})
九、实战总结
Q20:这套微前端架构在生产环境中部署需要注意什么?
答:
-
子应用独立部署:每个子应用有自己独立的 CI/CD 流水线,版本号独立管理
-
统一资源路径管理:通过环境变量管理各子应用的 URL:
export const subApps = [{ name: 'vue2-app', url: import.meta.env.VITE_CHILD_APP_URL || 'http://localhost:5173/', baseroute: '/child-app', }] -
Nginx 配置:
- 所有子应用必须配置 CORS 头
- 主应用和子应用建议部署在同源下(同协议、同域名、同端口),可避免大量跨域问题
- 如果异构部署,需要配置反向代理统一转发
-
公共依赖提取:将 Vue、Vue Router 等公共依赖配置为 external,避免重复加载:
// vite.config.ts build: { rollupOptions: { external: ['vue', 'vue-router'] } } -
灰度发布:利用
microApp.start()的plugins.global机制,可以在加载子应用 HTML 时做 A/B 测试控制 -
错误兜底:配置全局
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__ || '/' |