diff --git a/docs/micro-app面试题.md b/docs/micro-app面试题.md index 354da6e..13cc4d6 100644 --- a/docs/micro-app面试题.md +++ b/docs/micro-app面试题.md @@ -1,58 +1,61 @@ -# @micro-zoe/micro-app 微前端面试题 +# micro-app 微前端面试题 > 基于 Vue 3 + Vite 主应用搭建实战经验整理,涵盖从项目初始化、配置、到 bug 排查的全流程。 ---- +*** ## 一、基础概念 ### Q1:什么是微前端?micro-app 相比其他方案有什么优势? -**答:** -微前端是一种将多个独立的前端应用组合成一个统一应用的架构模式。每个子应用可以独立开发、独立部署、使用不同的技术栈。 +**答:** 微前端是一种将多个独立的前端应用组合成一个统一应用的架构模式。每个子应用可以独立开发、独立部署、使用不同的技术栈。 -`@micro-zoe/micro-app`(京东出品)的核心优势: -- **零依赖**:不依赖 single-spa,完全自研 -- **类 WebComponent**:使用 `` 标签加载子应用,对开发者透明 -- **双沙箱模式**:同时支持 with 沙箱和 iframe 沙箱 -- **不限制技术栈**:主应用和子应用可以任意组合(Vue 2/3、React、原生 HTML 等) -- **兼容 Vite**:通过 iframe 模式完美支持 Vite 构建的子应用 +`@micro-zoe/micro-app`的核心优势: -| 对比维度 | micro-app | qiankun | -|----------|-----------|---------| -| 接入成本 | 低(类 WebComponent) | 中(需改造子应用入口) | -| Vite 支持 | 原生支持(iframe 模式) | 需额外配置 | -| 沙箱方案 | with + iframe 双模式 | 基于 Proxy 的沙箱 | -| 包体积 | ~300KB | ~100KB | +* **零依赖**:不依赖 single-spa,完全自研 ---- +* **类 WebComponent**:使用 `` 标签加载子应用,对开发者透明 + +* **双沙箱模式**:同时支持 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 提供**三层隔离体系**,各管各的维度,可以组合使用: +**答:** micro-app 提供**三层隔离体系**,各管各的维度,css隔离(micro-app[name=xxx])、js隔离(with+proxy, iframe)、元素隔离(webcomponent, shadow-dom默认false)可以组合使用: ``` -┌──────────────────────────────────────────────────────────────┐ -│ micro-app 隔离体系 │ +┌───────────────────────────────┐ +│ micro-app 隔离体系 │ │ │ -│ ① CSS 样式隔离(默认开启,disableScopecss: false) │ -│ └── 方式:CSS 前缀改写,不是 Shadow DOM! │ -│ 子应用写了 h2 { color: red } │ -│ micro-app 提取 CSS 后自动改成 │ -│ micro-app[name=xxx] h2 { color: red } │ +│ ① CSS 样式隔离(默认开启,disableScopecss: false) │ +│ └── 方式:CSS 前缀改写,不是 Shadow DOM! │ +│ 子应用写了 h2 { color: red } │ +│ micro-app 提取 CSS 后自动改成 │ +│ micro-app[name=xxx] h2 { color: red } │ │ │ -│ ② JS 沙箱(默认开启,disableSandbox: false) │ -│ ├── with 沙箱:new Function() + Proxy(window) │ -│ └── iframe 沙箱:浏览器原生 iframe 隔离 │ +│ ② JS 沙箱(默认开启,disableSandbox: false) │ +│ ├── with 沙箱:new Function() + Proxy(window) │ +│ └── iframe 沙箱:浏览器原生 iframe 隔离 │ │ │ -│ ③ 元素隔离(默认关闭,shadowDOM: false) │ -│ └── 浏览器原生 Shadow DOM,DOM 树级别隔离 │ +│ ③ 元素隔离(默认关闭,shadowDOM: false) │ +│ └── 浏览器原生 Shadow DOM,DOM 树级别隔离 │ │ │ -└──────────────────────────────────────────────────────────────┘ +└───────────────────────────────┘ ``` ---- +*** **一、CSS 样式隔离 — CSS 前缀改写(默认方案)** @@ -84,20 +87,18 @@ micro-app 在加载子应用时,会**提取所有 ` -

子应用标题

← 样式天然隔离,内外互不穿透 + #shadow-root (open) ← Shadow DOM 边界 + +

子应用标题

← 样式天然隔离,内外互不穿透
``` ---- +*** **四、三种隔离方案对比(CSS 前缀 vs Shadow DOM vs iframe)** -| | CSS 前缀改写 | Shadow DOM | iframe 沙箱 | -|---|---|---|---| -| **配置** | 默认,无需配置 | `shadowDOM: true` | `iframe: true` | -| **样式隔离方向** | 子→主 ✅ / 主→子 ❌ | 双向 ✅ | 双向 ✅ | -| **JS 隔离** | 依赖沙箱配置 | 依赖沙箱配置 | ✅ 原生隔离 | -| **ES Module** | 不支持(with 沙箱) | 不支持(with 沙箱) | ✅ 支持 | -| **DOM 树** | 共享 | 独立 Shadow Tree | 独立 Document | -| **通信** | 同 window,直接引用 | 同 window,直接引用 | postMessage,序列化 | -| **兼容性** | 最好 | 部分 UI 库不兼容 | 最好 | -| **全局样式污染** | 会(主→子) | 不会 ✅ | 不会 ✅ | -| **适用场景** | Webpack 子应用 | 纯样式隔离需求 | Vite 子应用、完整隔离 | +| | CSS 前缀改写 | Shadow DOM | iframe 沙箱 | +| ------------- | ------------- | ----------------- | --------------- | +| **配置** | 默认,无需配置 | `shadowDOM: true` | `iframe: true` | +| **样式隔离方向** | 子→主 ✅ / 主→子 ❌ | 双向 ✅ | 双向 ✅ | +| **JS 隔离** | 依赖沙箱配置 | 依赖沙箱配置 | ✅ 原生隔离 | +| **ES Module** | 不支持(with 沙箱) | 不支持(with 沙箱) | ✅ 支持 | +| **DOM 树** | 共享 | 独立 Shadow Tree | 独立 Document | +| **通信** | 同 window,直接引用 | 同 window,直接引用 | postMessage,序列化 | +| **兼容性** | 最好 | 部分 UI 库不兼容 | 最好 | +| **全局样式污染** | 会(主→子) | 不会 ✅ | 不会 ✅ | +| **适用场景** | Webpack 子应用 | 纯样式隔离需求 | Vite 子应用、完整隔离 | ``` 隔离强度递增 → - - CSS 前缀改写 Shadow DOM iframe 沙箱 + CSS 前缀改写 Shadow DOM iframe 沙箱 ┌──────────┐ ┌──────────────┐ ┌──────────────┐ - │ 子→主 ✅ │ │ 子→主 ✅ │ │ 子→主 ✅ │ - │ 主→子 ❌ │ │ 主→子 ✅ │ │ 主→子 ✅ │ - │ JS 部分 │ │ JS 部分 │ │ JS 完全 ✅ │ - │ DOM 共享 │ │ DOM 隔离 │ │ 全部隔离 │ + │ 子→主 ✅ │ │ 子→主 ✅ │ │ 子→主 ✅ │ + │ 主→子 ❌ │ │ 主→子 ✅ │ │ 主→子 ✅ │ + │ JS 部分 │ │ JS 部分 │ │ JS 完全 ✅ │ + │ DOM 共享 │ │ DOM 隔离 │ │ 全部隔离 │ └──────────┘ └──────────────┘ └──────────────┘ ``` @@ -177,7 +177,7 @@ fn(microWindow) // microWindow = new Proxy(realWindow, handler) └── 注意主→子穿透!主应用样式需用 :not() 排除子应用容器 ``` ---- +*** ### Q2-1:为什么 CSS 前缀改写只能阻止「子→主」而不能阻止「主→子」? @@ -202,18 +202,20 @@ fn(microWindow) // microWindow = new Proxy(realWindow, handler) **核心原因**:micro-app 只改写**子应用的 CSS**,不改写主应用的 CSS。主应用的样式规则在全 document 范围内照常匹配,而 `iframe: false` 模式下子应用 DOM 就在这个 document 里。 **三种解决思路**: + 1. 主应用 CSS 加 `:not(.child-app-wrapper)` 排除子应用容器(本项目方案) + 2. 开启 `shadowDOM: true` — Shadow DOM 边界双向阻断 + 3. 开启 `iframe: true` — 独立 document,双向天然隔离 ---- +*** ## 二、项目搭建 ### Q3:使用 Vue 3 + Vite 搭建 micro-app 主应用需要哪些步骤? -**答:** -核心 4 步: +**答:** 核心 4 步: ```ts // 1. 安装依赖 @@ -221,16 +223,16 @@ npm install @micro-zoe/micro-app vue-router@4 // 2. vite.config.ts — 将 注册为自定义元素 export default defineConfig({ - plugins: [ - vue({ - template: { - compilerOptions: { - isCustomElement: (tag) => /^micro-app/.test(tag) - } - } - }) - ], - server: { port: 8080 } + plugins: [ + vue({ + template: { + compilerOptions: { + isCustomElement: (tag) => /^micro-app/.test(tag) + } + } + }) + ], + server: { port: 8080 } }) // 3. main.ts — 在 Vue 实例化前启动 micro-app @@ -242,12 +244,11 @@ createApp(App).use(router).mount('#app') ``` ---- +*** -### Q4:为什么要在 `vite.config.ts` 中配置 `isCustomElement`? +### Q4:为什么要在 vite.config.ts 中配置 isCustomElement? -**答:** -`` 是一个**自定义 HTML 标签**,不是 Vue 组件。Vue 模板编译器默认会把所有非标准 HTML 标签当作 Vue 组件去寻找,找不到就会报警告: +**答:** `` 是一个**自定义 HTML 标签**,不是 Vue 组件。Vue 模板编译器默认会把所有非标准 HTML 标签当作 Vue 组件去寻找,找不到就会报警告: ``` [Vue warn]: Failed to resolve component: micro-app @@ -257,28 +258,30 @@ createApp(App).use(router).mount('#app') **延伸**:如果子应用中也使用了 ``(嵌套微前端),同样需要配置。 ---- +*** ### Q5:主应用路由如何设计才能正确匹配子应用? -**答:** -使用 Vue Router 的**通配符** `:page*` 匹配子应用所有内部路由: +**答:** 使用 Vue Router 的**通配符** `:page*` 匹配子应用所有内部路由: ```ts const routes = [ - { path: '/', redirect: '/home' }, - { path: '/home', component: () => import('@/views/Home.vue') }, - // 关键::page* 匹配 /child-app 及其下所有子路由 - { path: '/child-app/:page*', component: () => import('@/views/ChildApp.vue') } + { 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` 都能被正确捕获 ---- +* `*` 只在 Vue Router 3 中可用 + +* Vue Router 4 中必须使用 `:page*`(或 `:pathMatch(.*)*`)来匹配多级路径 + +* 这样 `/child-app/page1`、`/child-app/a/b/c` 都能被正确捕获 + +*** ## 三、沙箱机制(重点 Bug 来源) @@ -286,29 +289,32 @@ const routes = [ **答:** -| 特性 | with 沙箱 | iframe 沙箱 | -|------|-----------|-------------| -| 配置 | `iframe: false`(默认) | `iframe: true` | -| 原理 | `with(window.proxy)` + `new Function()` | 浏览器原生 iframe | -| ES Module | ❌ 不支持 `import`/`export` | ✅ 原生支持 | -| 性能 | 较快(无额外渲染) | 略慢(需创建 iframe) | -| 隔离性 | 中等(Proxy 代理) | 强(浏览器原生隔离) | -| 调试体验 | 较好(同 document) | 较差(跨 iframe) | -| 适用场景 | Webpack 构建的 UMD 应用 | **Vite 构建的应用** | +| 特性 | 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` ---- +* **Vite 子应用 → 必须 `iframe: true`** -### Q7:遇到 `Cannot use import statement outside a module` 报错,是什么原因?如何解决? +* Webpack UMD 子应用 → `iframe: false` 即可 -**答:** -这是 micro-app 主应用对接 Vite 子应用时**必踩的坑**。 +* 对样式/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 @@ -317,45 +323,50 @@ const routes = [ ``` **根因分析:** + 1. Vite 开发服务器输出的 JS 是 ` ``` + 解决:子应用 Vite 配置 `base` 或使用相对路径 **2. `baseroute` 与 `base` 不匹配:** + ```ts // 主应用 // 子应用 vite.config.ts export default defineConfig({ - base: '/child-app/' // 必须与 baseroute 一致 + base: '/child-app/' // 必须与 baseroute 一致 }) ``` ---- +*** ## 九、实战总结 @@ -1470,6 +1534,7 @@ export default defineConfig({ 1. **子应用独立部署**:每个子应用有自己独立的 CI/CD 流水线,版本号独立管理 2. **统一资源路径管理**:通过环境变量管理各子应用的 URL: + ```ts export const subApps = [{ name: 'vue2-app', @@ -1479,11 +1544,15 @@ export default defineConfig({ ``` 3. **Nginx 配置**: - - 所有子应用必须配置 CORS 头 - - 主应用和子应用建议部署在**同源**下(同协议、同域名、同端口),可避免大量跨域问题 - - 如果异构部署,需要配置反向代理统一转发 + + * 所有子应用必须配置 CORS 头 + + * 主应用和子应用建议部署在**同源**下(同协议、同域名、同端口),可避免大量跨域问题 + + * 如果异构部署,需要配置反向代理统一转发 4. **公共依赖提取**:将 Vue、Vue Router 等公共依赖配置为 external,避免重复加载: + ```ts // vite.config.ts build: { @@ -1497,38 +1566,38 @@ export default defineConfig({ 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__ \|\| '/'` | +| 检查项 | 正确配置 | +| -------------------- | ------------------------------------------ | +| 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__ \|\| '/'` | ---- +*** ## 十、样式穿透实战 -### Q21:子应用 `iframe: false` 模式下,主应用的全局样式为什么会穿透到子应用?如何彻底解决? +### Q21:子应用 iframe: false 模式下,主应用的全局样式为什么会穿透到子应用?如何彻底解决? **答:** 这是微前端 `iframe: false` 模式下最常见的样式泄漏问题。即使使用了 `.app-main h2` 这样的"作用域选择器",样式仍然会穿透。 ---- +*** **一、问题现象** 在 `http://localhost:8080/mock-app` 页面中,子应用的 `

` 元素出现了主应用的紫色 `border-bottom` 样式,而不是子应用自己定义的样式。 ---- +*** **二、根因分析:DOM 层级重叠** @@ -1537,10 +1606,10 @@ export default defineConfig({ ```html
-
...
-
- -
+
...
+
+ +
``` @@ -1549,9 +1618,9 @@ export default defineConfig({ ```html
- - - + + +
``` @@ -1569,7 +1638,7 @@ main.app-main ← 主应用的样式容器 **关键点**:子应用的 DOM 被渲染在 `.app-main` 内部,所以 `.app-main h2` 这个**后代选择器**会匹配到子应用的 `

`。 ---- +*** **三、常见的错误认知** @@ -1578,7 +1647,7 @@ main.app-main ← 主应用的样式容器 ```css /* ❌ 错误:这样写仍然会穿透! */ .app-main h2 { - border-bottom: 2px solid #667eea !important; + border-bottom: 2px solid #667eea !important; } ``` @@ -1587,11 +1656,11 @@ main.app-main ← 主应用的样式容器 ```css /* ❌ 更差:这样写 100% 泄漏 */ h2 { - border-bottom: 2px solid purple; + border-bottom: 2px solid purple; } ``` ---- +*** **四、正确解法** @@ -1600,7 +1669,7 @@ h2 { ```css /* ✅ 正确:只匹配主应用页面内的 h2,排除子应用容器 */ .app-main > :not(.child-app-wrapper) h2 { - border-bottom: 2px solid #667eea !important; + border-bottom: 2px solid #667eea !important; } .app-main > :not(.child-app-wrapper) button { ... } @@ -1610,9 +1679,12 @@ h2 { ``` **原理:** -- `>` 子选择器:只匹配 `.app-main` 的**直接子元素** -- `:not(.child-app-wrapper)`:排除类名为 `.child-app-wrapper` 的直接子元素 -- 子应用的容器 `.child-app-wrapper` 被排除 → 其内部所有元素都不受主样式影响 + +* `>` 子选择器:只匹配 `.app-main` 的**直接子元素** + +* `:not(.child-app-wrapper)`:排除类名为 `.child-app-wrapper` 的直接子元素 + +* 子应用的容器 `.child-app-wrapper` 被排除 → 其内部所有元素都不受主样式影响 ``` .app-main @@ -1625,13 +1697,12 @@ h2 { ```ts { - name: 'mock-app', - iframe: true, // ← 浏览器原生隔离,样式 100% 不互通 + name: 'mock-app', + iframe: true, // ← 浏览器原生隔离,样式 100% 不互通 } ``` -**优点**:彻底的样式/JS 隔离 -**缺点**:性能略差、调试不便、通信成本增加 +**优点**:彻底的样式/JS 隔离 **缺点**:性能略差、调试不便、通信成本增加 **方案 3:使用 Shadow DOM(激进方案)** @@ -1645,25 +1716,26 @@ h2 { ```css @scope (.app-main) to (micro-app) { - h2 { border-bottom: 2px solid #667eea; } + h2 { border-bottom: 2px solid #667eea; } } ``` `@scope` 的 `to` 边界可以精确控制样式不穿透到 `micro-app` 内部。Chrome 118+、Safari 17.4+ 已支持,但目前还不够普及。 ---- +*** **五、各方案对比** -| 方案 | 隔离程度 | 性能 | 复杂度 | 适用场景 | -|------|----------|------|--------|----------| -| `:not(.child-app-wrapper)` | 中(单向阻断) | 无影响 | 低 | 已知子应用容器结构的场景 | -| iframe 沙箱 | 高(完全隔离) | 略低 | 低(配置即可) | Vite 子应用、高隔离要求 | -| Shadow DOM | 高(DOM 隔离) | 中 | 高(兼容问题多) | 对隔离有极致要求的场景 | -| CSS @scope | 中高 | 无影响 | 低 | 未来主流方案(等浏览器普及) | +| 方案 | 隔离程度 | 性能 | 复杂度 | 适用场景 | +| -------------------------- | --------- | --- | -------- | -------------- | +| `:not(.child-app-wrapper)` | 中(单向阻断) | 无影响 | 低 | 已知子应用容器结构的场景 | +| iframe 沙箱 | 高(完全隔离) | 略低 | 低(配置即可) | Vite 子应用、高隔离要求 | +| Shadow DOM | 高(DOM 隔离) | 中 | 高(兼容问题多) | 对隔离有极致要求的场景 | +| CSS @scope | 中高 | 无影响 | 低 | 未来主流方案(等浏览器普及) | ---- +*** **六、核心教训** > 在 `iframe: false` 模式下,子应用和主应用共享同一个 document,**任何后代选择器都会穿透到子应用内部**。给选择器加父级 class(如 `.app-main`)只在父级和子应用不在同一容器时才有效。真正有效的做法是用**子选择器**限制层级深度,同时用 `:not()` 排除子应用容器。 + diff --git a/vite.config.ts b/vite.config.ts index b064d52..b934fde 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,6 +1,9 @@ import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' -import { resolve } from 'path' +import { resolve } from 'node:path' +import { fileURLToPath } from 'node:url' + +const __dirname = fileURLToPath(new URL('.', import.meta.url)) export default defineConfig({ plugins: [