样式冲突问题解决

This commit is contained in:
2026-06-21 20:19:21 +08:00
parent e9c570f893
commit fb04230958
4 changed files with 780 additions and 30 deletions

View File

@@ -2172,6 +2172,232 @@ Webpack 子应用 → iframe: false + 默认 scoped (性能好 + 样式安
--- ---
### Q31已经发现了样式冲突问题如何修复有哪些方案
**答:**
修复方案从简单到体系化分为四层。最简单的方案(也是我们实际采用的)只需**删掉一行配置**。
---
#### 一、方案一:恢复样式隔离(最简单,一行改动)
**根因回顾:**
```ts
// ❌ 冲突的配置
{
name: 'mock-app',
iframe: false,
disableScopecss: true, // ← 这行是罪魁祸首
keepAlive: true,
}
```
**修复:删除 `disableScopecss: true`(恢复默认值 `false`**
```ts
// ✅ 修复后的配置
{
name: 'mock-app',
iframe: false,
// disableScopecss 不设置,默认为 false
// → micro-app 自动为子应用 CSS 添加作用域前缀
keepAlive: true,
}
```
**一行改动,问题解决。**
---
#### 二、修复原理micro-app 的 CSS 作用域机制
`disableScopecss: false`默认micro-app 在注入子应用的 CSS 时,自动给每个选择器加上属性选择器前缀:
```
子应用原始 CSS:
─────────────────────────────────────────────
h2 { color: #ff6b35; border-left: 6px solid orange; }
button { background: #ff6b35; }
p { font-size: 13px; }
↓ micro-app 自动转换 ↓
注入主文档后的 CSS:
─────────────────────────────────────────────
micro-app[name=mock-app] h2 {
color: #ff6b35;
border-left: 6px solid orange;
}
micro-app[name=mock-app] button {
background: #ff6b35;
}
micro-app[name=mock-app] p {
font-size: 13px;
}
```
**效果:**
| 选择器 | 作用范围 |
|--------|----------|
| 主应用的 `h2 { color: purple }` | 整个文档 |
| 子应用转换后的 `micro-app[name=mock-app] h2 { color: orange }` | 只作用于 `<micro-app name="mock-app">` 内部的 h2 |
**两个选择器不再冲突** — 它们选择的是不同 DOM 范围内的元素。特异性不同,作用范围不同,互不干扰。
---
#### 三、修复前后对比
```
修复前 (disableScopecss: true):
───────────────────────────────────────────────
文档中的 CSS:
h2 { color: purple; border-bottom: 2px solid purple; } ← 主应用
h2 { color: orange; border-left: 6px solid orange; } ← mock-app
h2 { color: teal; border-right: 5px solid teal; } ← mock-app-2
结果: 三个 h2 规则搅拌在一起,逐属性混搭
→ 紫色底边框 + 橙色左边框 + 青色右边框 同时出现 ❌
修复后 (disableScopecss: false):
───────────────────────────────────────────────
文档中的 CSS:
h2 { color: purple; ... } ← 主应用(全局)
micro-app[name=mock-app] h2 { color: orange; ... } ← 仅 mock-app 内
micro-app[name=mock-app-2] h2 { color: teal; ... } ← 仅 mock-app-2 内
结果: 主应用的 h2 是紫色
mock-app 内的 h2 是橙色 ✅
mock-app-2 内的 h2 是青色 ✅
→ 各管各的区域,零冲突 ✅
```
---
#### 四、修复验证方法
```
1. 修复前先看一次:
访问 /mock-app → F12 → Elements → <style> 标签
→ 看到 h2 { ... } 没有前缀 ← 这是冲突的根源
2. 修复后再看一次:
刷新页面 → F12 → Elements → <style> 标签
→ 看到 micro-app[name=mock-app] h2 { ... } ← 自动加了前缀!
→ 主应用的紫色底边框不再出现在子应用的 h2 上 ✅
```
---
#### 五、多层防御体系(从简单到彻底)
| 层级 | 方案 | 适用场景 | 效果 |
|------|------|----------|------|
| **L1: 框架层** | `disableScopecss: false`(默认) | 所有 `iframe: false` 的子应用 | micro-app 自动加前缀,零成本 |
| **L2: 构建层** | `iframe: true` | Vite 子应用(必须)或高隔离需求 | 浏览器原生 iframe 隔离,最强 |
| **L3: 组件层** | Vue SFC `<style scoped>` | 子应用内部组件 | 组件级隔离,防止子应用内部样式冲突 |
| **L4: 规范层** | CSS Modules / BEM 命名 | 全局样式、公共组件 | 命名空间隔离,防止类名冲突 |
**推荐策略:**
```
Vite 子应用:
L2 (iframe: true) → 物理隔离,必选 ✅
L3 (Vue scoped) → 组件级隔离,推荐 ✅
Webpack / 普通 HTML 子应用:
L1 (disableScopecss: false) → 框架自动隔离,必选 ✅
L3 (Vue scoped / BEM) → 额外防护,推荐 ✅
```
---
#### 六、`disableScopecss: true` 的正确使用场景
这个选项不是为了"省事",它的合法用途很窄:
| 场景 | 说明 |
|------|------|
| **调试排错** | 临时关闭隔离,确认是否是样式隔离导致的渲染问题 |
| **第三方 UI 库** | 子应用使用了不兼容 CSS 作用域的 UI 库(罕见) |
| **刻意演示** | 比如我们的 mock-app故意制造冲突来教学 |
**生产环境应该永远保持 `disableScopecss: false`(默认值)。**
---
#### 七、如果主应用的全局样式也过于宽泛怎么办?
前面的修复解决了"子应用样式泄漏到主应用/其他子应用"。反向的问题——**主应用的全局样式污染子应用**——同样需要处理。
**问题回顾Q29** 主应用的 `h2 { border-bottom: 2px solid purple !important }` 泄漏进了子应用。
**修复方案:**
```css
/* ❌ 主应用当前写法 — 全局 h2 会影响所有 iframe: false 的子应用 */
h2 {
color: #667eea;
border-bottom: 2px solid #667eea;
}
/* ✅ 修复方案 1给主应用的内容区加作用域 */
.app-main h2 {
color: #667eea;
border-bottom: 2px solid #667eea;
}
/* ✅ 修复方案 2重置子应用容器内的样式 */
micro-app h2 {
border-bottom: none; /* 清除主应用的底边框 */
}
/* ✅ 修复方案 3主应用使用更具体的选择器 */
#main-app h2 { ... } /* ID 选择器,特异性更高 */
.main-content h2 { ... } /* 类选择器 + 标签,限定范围 */
```
**推荐:** 主应用的全局样式始终挂在容器选择器下(如 `.app-main h2``#main-app h2`),避免使用裸标签选择器(`h2``button``p`)。
---
#### 八、决策树:拿到冲突问题怎么修?
```
发现样式冲突
├── 子应用是 Vite 项目?
│ └── 是 → 设置 iframe: true物理隔离 + ES Module 兼容)
├── 子应用是 Webpack / 普通 HTML
│ └── 检查 disableScopecss 配置
│ ├── 是 true → 删掉它!(恢复默认隔离)← 90% 的情况
│ └── 是 false → 检查是否是主应用的全局样式泄漏
│ └── 主应用样式加 .app-main 等容器前缀
└── keepAlive 导致残留?
└── 修复了样式隔离后keepAlive 不再是问题
(因为每个子应用的 CSS 有独立的作用域前缀)
```
---
#### 九、总结
| 问题 | 答案 |
|------|------|
| 修复成本高吗? | **极低。** 删掉一行 `disableScopecss: true` 即可 |
| 默认值就是安全的吗? | **是的。** `disableScopecss` 默认为 `false`micro-app 自动隔离 |
| `disableScopecss: true` 什么时候用? | 调试排错时临时开启,或刻意演示冲突时 |
| 主应用的全局样式泄漏怎么办? | 全局样式加 `.app-main` 等容器前缀,不用裸标签选择器 |
| 为什么 Vite 项目不需要担心? | `iframe: true` 提供物理隔离,比 CSS 作用域更强 |
**一句话修复指南:不要设置 `disableScopecss: true`Vite 项目必须 `iframe: true`,主应用全局样式挂容器选择器。三层到位,样式冲突清零。**
---
## 附录:接入新子应用检查清单 ## 附录:接入新子应用检查清单
| 步骤 | 文件 | 操作 | | 步骤 | 文件 | 操作 |

View File

@@ -30,17 +30,181 @@
### Q2micro-app 主应用和子应用之间是如何隔离的? ### Q2micro-app 主应用和子应用之间是如何隔离的?
**答:** **答:**
micro-app 提供**三层隔离** micro-app 提供**三层隔离体系**,各管各的维度,可以组合使用
1. **样式隔离**`disable-scopecss`,默认开启): ```
- 为子应用的每个样式规则自动添加 `micro-app[name=xxx]` 前缀,限制样式作用域 ┌──────────────────────────────────────────────────────────────┐
│ micro-app 隔离体系 │
│ │
│ ① 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 隔离 │
│ │
│ ③ 元素隔离默认关闭shadowDOM: false
│ └── 浏览器原生 Shadow DOMDOM 树级别隔离 │
│ │
└──────────────────────────────────────────────────────────────┘
```
2. **JS 沙箱隔离**`disable-sandbox`,默认开启): ---
- **with 沙箱**:通过 `with(window.proxy)` + `new Function()` 执行子应用 JS将子应用的全局变量操作代理到隔离的 microWindow 上
- **iframe 沙箱**:将子应用放入 iframe 中运行,利用浏览器原生的 iframe 隔离
3. **元素隔离**`shadowDOM`,可选): **一、CSS 样式隔离 — CSS 前缀改写(默认方案)**
- 将子应用放入 Shadow DOM彻底隔离 DOM 树
**工作原理:**
```css
/* 子应用源码 */
h2 { color: orange; }
/* micro-app 自动改写为 ↓ */
micro-app[name=mock-app] h2 { color: orange; }
```
micro-app 在加载子应用时,会**提取所有 `<style>` 标签内容**,给每个 CSS 规则自动加上 `micro-app[name=xxx]` 属性选择器前缀,限制样式只作用于子应用容器内部。
**⚠️ 关键局限:只能阻止「子→主」,不能阻止「主→子」**
```
子应用样式泄漏到主应用(子→主):✅ CSS 前缀改写可以阻止
──────────────────────────────────────────────
子应用: h2 { color: orange }
→ 改写后: micro-app[name=mock-app] h2 { color: orange }
→ 主应用的 h2 没有 micro-app[name=mock-app] 父级 → 不匹配 ✅
主应用样式泄漏到子应用(主→子):❌ CSS 前缀改写无法阻止
──────────────────────────────────────────────
主应用: .app-main h2 { border-bottom: 2px solid purple; }
→ 子应用 h2 也在 .app-main 内部 → 匹配 ❌
→ 需要额外手段::not(.child-app-wrapper) / iframe / shadowDOM
```
这就是我们在 `App.vue` 中遇到的问题 — 加了 `.app-main` 前缀还不够,因为子应用 DOM 就嵌在 `.app-main` 里面。
---
**二、JS 沙箱 — with 沙箱 vs iframe 沙箱**
| 维度 | with 沙箱 (`iframe: false`) | iframe 沙箱 (`iframe: true`) |
|------|-----------------------------|------------------------------|
| 原理 | `with(window.proxy)` + `new Function()` | 浏览器原生 iframe |
| 子应用 window | Proxy 代理的 microWindow | 真实的 iframe.contentWindow |
| ES Module | ❌ 不支持 `import`/`export` | ✅ 原生支持 |
| 适用 | Webpack UMD 子应用 | **Vite 子应用** |
| 性能 | 较快 | 略慢(多一层 iframe 渲染) |
| 共享内存 | ✅ 同一 document可传引用 | ❌ 跨 document必须 postMessage 序列化 |
**with 沙箱的执行过程:**
```ts
// micro-app 内部简化逻辑
const code = await fetchJs() // 拉取子应用 JS 文本
const fn = new Function('window', `
with(window) {
${code} // 子应用代码拼接在这里
}
`)
fn(microWindow) // microWindow = new Proxy(realWindow, handler)
```
子应用代码里的 `window.xxx` / `document.xxx` 等全局操作,都会被 `with(microWindow)` 劫持到 Proxy 代理上,实现对全局变量的拦截和隔离。
**为什么 Vite 子应用必须用 iframe** Vite 开发环境输出 `<script type="module">` 格式的 ES Module`import`/`export``new Function()` 无法执行模块语法(`import` 只能出现在模块静态顶层)。详见 Q8-1。
---
**三、元素隔离 — Shadow DOM可选功能**
**⚠️ 注意Shadow DOM 不是默认的样式隔离方式!默认方式是 CSS 前缀改写。**
```html
<micro-app name="mock-app" :shadowDOM="true" />
```
开启后,子应用的 DOM 被包裹在 Shadow Root 里:
```html
<micro-app name="mock-app">
#shadow-root (open) ← Shadow DOM 边界
<style>h2 { color: orange; }</style>
<h2>子应用标题</h2> ← 样式天然隔离,内外互不穿透
</micro-app>
```
---
**四、三种隔离方案对比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 沙箱
┌──────────┐ ┌──────────────┐ ┌──────────────┐
│ 子→主 ✅ │ │ 子→主 ✅ │ │ 子→主 ✅ │
│ 主→子 ❌ │ │ 主→子 ✅ │ │ 主→子 ✅ │
│ JS 部分 │ │ JS 部分 │ │ JS 完全 ✅ │
│ DOM 共享 │ │ DOM 隔离 │ │ 全部隔离 │
└──────────┘ └──────────────┘ └──────────────┘
```
**选择决策树:**
```
子应用是 Vite 构建?
├── 是 → iframe: true必须ES Module 限制)
└── 否Webpack
├── 只需样式隔离 → shadowDOM: true可选
├── 需完整 JS 隔离 → iframe: true
└── 隔离要求不高 → 默认配置CSS 前缀改写)
└── 注意主→子穿透!主应用样式需用 :not() 排除子应用容器
```
---
### Q2-1为什么 CSS 前缀改写只能阻止「子→主」而不能阻止「主→子」?
**答:**
这是 CSS 前缀改写的**单向性**决定的:
```
子→主(子应用样式泄漏到主应用):
子应用 CSS: h2 { color: orange }
↓ micro-app 改写
改写后: micro-app[name=mock-app] h2 { color: orange }
主应用的 h2 的父级链中没有 micro-app[name=mock-app] → 不匹配 ✅
主→子(主应用样式泄漏到子应用):
主应用 CSS: .app-main h2 { border-bottom: 2px solid purple; }
↓ 没有任何改写!原样生效
子应用 h2 的祖先链中有 .app-main → 匹配 ❌
```
**核心原因**micro-app 只改写**子应用的 CSS**,不改写主应用的 CSS。主应用的样式规则在全 document 范围内照常匹配,而 `iframe: false` 模式下子应用 DOM 就在这个 document 里。
**三种解决思路**
1. 主应用 CSS 加 `:not(.child-app-wrapper)` 排除子应用容器(本项目方案)
2. 开启 `shadowDOM: true` — Shadow DOM 边界双向阻断
3. 开启 `iframe: true` — 独立 document双向天然隔离
--- ---
@@ -191,6 +355,102 @@ micro-app 选择 `new Function()` 的原因:
--- ---
### Q8-1为什么 Vite 子应用必须用 `iframe: true`,而 Webpack 子应用不需要?
**答:**
核心原因在于 Vite 和 Webpack 输出的 JS 代码**格式不同**,而 micro-app 的 with 沙箱只能执行某种格式。
**一、开发环境Dev Server的差异**
| | Vite Dev Server | Webpack Dev Server |
|---|---|---|
| 输出的 JS 格式 | `<script type="module">` ES Module | `<script>` IIFE / UMD bundle |
| 含 `import`/`export` | ✅ 是 | ❌ 否 |
| 文件数量 | 按需加载,可能几百个 | 一个 bundle或少量 chunk |
```
<!-- Vite 开发环境输出的 HTML -->
<script type="module" src="/src/main.ts"></script>
<!-- main.ts 内部还有上百个 import -->
<!-- import { createApp } from 'vue' ← ES Module 语法 -->
<!-- import router from './router' ← ES Module 语法 -->
<!-- Webpack 开发环境输出的 HTML -->
<script src="/js/app.js"></script>
<!-- app.js 是一个巨大的 IIFE 包裹的 bundle -->
<!-- (function(modules) { ... })({...}) ← 没有 import/export -->
```
**二、with 沙箱的执行过程**
micro-app 用 `new Function()` 执行子应用的 JS
```ts
// micro-app 内部简化逻辑with 沙箱模式)
const code = await fetchJsFromChildApp() // 拉取子应用的 JS 文本
// ⚠️ 这里执行 JS
const fn = new Function('window', `
with(window) {
${code} // ← 子应用的代码直接拼接在这里
}
`)
fn(microWindow) // microWindow 是 Proxy 代理的隔离 window
```
**问题来了:** 如果 `code` 包含 `import`/`export`
```js
// 子应用代码
import { createApp } from 'vue' // ❌ SyntaxError!
// ↑ import 只能出现在模块顶层,不能出现在 new Function() 内部
```
浏览器的模块系统要求 `import` 必须出现在 `<script type="module">` 或 ES Module 文件的**静态顶层**,不能被 `new Function()` / `eval()` 动态执行。
**三、为什么 Webpack 可以不用 iframe**
```
Webpack 构建流程:
import { ref } from 'vue' webpack打包 __webpack_require__('./vue')
import router from './router' ──────────→ __webpack_require__('./router')
↑ ES Module 语法 ↑ 普通函数调用
Vite 开发环境:
import { ref } from 'vue' 不打包! import { ref } from 'vue'
import router from './router' ──────────→ import router from './router'
↑ ES Module 语法 ↑ 仍然是 ES Module 语法
```
- **Webpack** 在构建时就把 `import` 转成了 `__webpack_require__()` 这种普通函数调用,输出的是 IIFE 格式,`new Function()` 可以正常执行
- **Vite 开发模式** 不做打包,直接输出浏览器原生的 `import`/`export`,只能通过 iframe 的原生模块环境执行
**四、一张图总结**
```
子应用 JS 包含 import/export
┌──────┴──────┐
│ │
否 是
(Webpack) (Vite)
│ │
▼ ▼
new Function() iframe 沙箱
可直接执行 ✅ 唯一可行方案 ✅
```
**五、面试追问Vite 生产环境构建后还需要 iframe 吗?**
生产环境 Vite 用 Rollup 打包,输出的是普通 JS`import`),理论上用 with 沙箱也能执行。但**仍然推荐 `iframe: true`**,原因:
1. **一致性**:开发/生产行为一致,避免"开发没问题,上线炸了"
2. **隔离性**iframe 提供更彻底的样式/JS 隔离
3. **资源路径**iframe 中的相对路径自动指向子应用 origin不用额外处理
---
## 四、TypeScript 类型问题 ## 四、TypeScript 类型问题
### Q9micro-app 的 `lifeCycles` 回调中,为什么 `e.name` 会报 TS 类型错误? ### Q9micro-app 的 `lifeCycles` 回调中,为什么 `e.name` 会报 TS 类型错误?
@@ -317,6 +577,100 @@ window.microApp?.dispatch({ type: '回复', payload: {} })
--- ---
### Q12-1`iframe: true` 模式下,通信的底层原理是什么?和 `iframe: false` 有什么不同?
**答:**
**API 层面完全一致** — 无论 `iframe: true` 还是 `iframe: false`,主应用和子应用都用同一套 API`setData``getData``dispatch``addDataListener`)。框架在底层封装了通信差异。
**底层机制对比:**
```
iframe: falsewith 沙箱) iframe: true浏览器原生隔离
┌──────────────────────┐ ┌──────────────────────────┐
│ 主应用 document │ │ 主应用 document │
│ window (共享) │ │ window │
│ ├── micro-app │ │ ├── micro-app │
│ │ └── 子应用 DOM │ │ │ └── <iframe> │
│ │ window ✅ 同一个│ │ │ └── 子应用 DOM │
│ │ 内存引用传递 │ │ │ window (独立) │
│ └── ... │ │ └── ... │
│ 直接读写 JS 对象 │ │ 跨域边界,必须序列化 │
└──────────────────────┘ └──────────────────────────┘
```
**iframe 模式下的通信链路:**
```
主应用 iframe 子应用
│ │
│ microApp.setData('app', data) │
│ ↓ JSON.stringify(data) │
│ ↓ iframe.contentWindow │
│ .postMessage(data, '*') │
│ ───────── postMessage ────────→ │
│ │ window.addEventListener
│ │ ('message', handler)
│ │ ↓ JSON.parse
│ │ ↓ 触发 dataListener
│ │
│ window.parent │
│ ←──────── postMessage ──────── │
│ addEventListener('message') │ microApp.dispatch(msg)
│ ↓ JSON.parse │
│ ↓ 触发 dataListener │
```
**核心步骤:**
1. **主→子**`microApp.setData()` → 框架 `JSON.stringify(data)``iframe.contentWindow.postMessage(serialized, '*')` → 子应用 `window.addEventListener('message')``JSON.parse` → 触发 `dataListener`
2. **子→主**`window.microApp.dispatch(msg)` → 框架 `JSON.stringify(msg)``window.parent.postMessage(serialized, '*')` → 主应用 `window.addEventListener('message')``JSON.parse` → 触发 `dataListener`
**关键限制postMessage 的"结构化克隆"**
```
✅ 可以传: ❌ 不能传:
- string/number/boolean - 函数 / class
- 普通 Object / Array - DOM 节点 / Event
- Date / RegExp - Symbol
- Map / Set - WeakMap / WeakSet
- ArrayBuffer / Blob - Proxy 对象
```
这和 `iframe: false` 模式有本质区别 — with 沙箱下主应用和子应用共享同一个 `window`,可以传**内存引用**(对象改了,两边都能看到),而 iframe 模式走的是**序列化拷贝**(传完之后各是各的副本)。
**实战中的坑:**
```ts
// ❌ iframe 模式下这样传会丢失数据
microApp.setData('app', {
callback: () => {}, // 函数 → 序列化后丢失
element: document.body, // DOM 节点 → 序列化后丢失
})
// ✅ 正确做法:只传纯数据
microApp.setData('app', {
token: 'abc123',
userId: 42,
theme: 'dark',
})
```
**对比总结:**
| 维度 | `iframe: false` | `iframe: true` |
|------|-----------------|----------------|
| 通信机制 | 直接内存引用 | postMessage 序列化 |
| 传引用 | ✅ 对象共享 | ❌ 序列化拷贝 |
| 传函数 | ✅ 可以 | ❌ 丢失 |
| 性能 | 高(无序列化开销) | 中JSON 序列化) |
| 数据大小 | 不受限 | 受 postMessage 限制 |
| API 一致性 | 完全相同 | 完全相同 |
| 调试 | 同 console | 需切换 iframe context |
---
### Q13`keep-alive` 保活机制的原理是什么?有什么注意事项? ### Q13`keep-alive` 保活机制的原理是什么?有什么注意事项?
**答:** **答:**
@@ -572,3 +926,159 @@ export default defineConfig({
| 路由通配符Vue Router 4 | `:page*`,不是 `*` | | 路由通配符Vue Router 4 | `:page*`,不是 `*` |
| lifeCycles 回调参数 | `(e: CustomEvent, appName: string)` | | lifeCycles 回调参数 | `(e: CustomEvent, appName: string)` |
| 子应用路由 base | `window.__MICRO_APP_BASE_ROUTE__ \|\| '/'` | | 子应用路由 base | `window.__MICRO_APP_BASE_ROUTE__ \|\| '/'` |
---
## 十、样式穿透实战
### Q21子应用 `iframe: false` 模式下,主应用的全局样式为什么会穿透到子应用?如何彻底解决?
**答:**
这是微前端 `iframe: false` 模式下最常见的样式泄漏问题。即使使用了 `.app-main h2` 这样的"作用域选择器",样式仍然会穿透。
---
**一、问题现象**
在 `http://localhost:8080/mock-app` 页面中,子应用的 `<h2>` 元素出现了主应用的紫色 `border-bottom` 样式,而不是子应用自己定义的样式。
---
**二、根因分析DOM 层级重叠**
主应用的 `App.vue` 布局结构:
```html
<!-- App.vue 模板 -->
<div id="main-app">
<header class="app-header">...</header>
<main class="app-main">
<router-view /> <!-- ← 这里渲染所有页面 -->
</main>
</div>
```
当访问 `/mock-app` 时,`<router-view>` 渲染的是 `ChildApp.vue`
```html
<!-- ChildApp.vue 模板 -->
<div class="child-app-wrapper">
<micro-app name="mock-app" :iframe="false">
<!-- 子应用的完整 DOM 在这里!包括 <h2>、<button>、<p> 等 -->
</micro-app>
</div>
```
**最终 DOM 树:**
```
main.app-main ← 主应用的样式容器
├── div.home-page ← 访问 /home 时
│ └── h2 (主应用的 h2)
└── div.child-app-wrapper ← 访问 /mock-app 时
└── <micro-app name="mock-app">
└── h2 (子应用的 h2) ← ⚠️ 也在 .app-main 内部!
```
**关键点**:子应用的 DOM 被渲染在 `.app-main` 内部,所以 `.app-main h2` 这个**后代选择器**会匹配到子应用的 `<h2>`。
---
**三、常见的错误认知**
很多开发者以为加上父级 class 就能隔离:
```css
/* ❌ 错误:这样写仍然会穿透! */
.app-main h2 {
border-bottom: 2px solid #667eea !important;
}
```
**为什么不行?** 因为 `.app-main h2` 是**后代选择器**(任意层级),它会匹配 `.app-main` 下面所有层级的 `h2`,包括嵌套在 `micro-app` 内部的子应用 `h2`。
```css
/* ❌ 更差:这样写 100% 泄漏 */
h2 {
border-bottom: 2px solid purple;
}
```
---
**四、正确解法**
**方案 1子选择器 + :not() 排除子应用容器(本项目采用)**
```css
/* ✅ 正确:只匹配主应用页面内的 h2排除子应用容器 */
.app-main > :not(.child-app-wrapper) h2 {
border-bottom: 2px solid #667eea !important;
}
.app-main > :not(.child-app-wrapper) button { ... }
.app-main > :not(.child-app-wrapper) p { ... }
.app-main > :not(.child-app-wrapper) table { ... }
.app-main > :not(.child-app-wrapper) code { ... }
```
**原理:**
- `>` 子选择器:只匹配 `.app-main` 的**直接子元素**
- `:not(.child-app-wrapper)`:排除类名为 `.child-app-wrapper` 的直接子元素
- 子应用的容器 `.child-app-wrapper` 被排除 → 其内部所有元素都不受主样式影响
```
.app-main
├── div.home-page ← :not(.child-app-wrapper) → ✅ 样式生效
└── div.child-app-wrapper ← 被 :not() 排除 → ❌ 样式不生效
└── h2 (安全!)
```
**方案 2开启 iframe 沙箱(最彻底)**
```ts
{
name: 'mock-app',
iframe: true, // ← 浏览器原生隔离,样式 100% 不互通
}
```
**优点**:彻底的样式/JS 隔离
**缺点**:性能略差、调试不便、通信成本增加
**方案 3使用 Shadow DOM激进方案**
```html
<micro-app name="mock-app" :shadowDOM="true" />
```
**缺点**Shadow DOM 边界会阻断很多 CSS 特性(如全局字体),兼容性成本高。
**方案 4CSS @scope未来方案**
```css
@scope (.app-main) to (micro-app) {
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 | 中高 | 无影响 | 低 | 未来主流方案(等浏览器普及) |
---
**六、核心教训**
> 在 `iframe: false` 模式下,子应用和主应用共享同一个 document**任何后代选择器都会穿透到子应用内部**。给选择器加父级 class如 `.app-main`)只在父级和子应用不在同一容器时才有效。真正有效的做法是用**子选择器**限制层级深度,同时用 `:not()` 排除子应用容器。

View File

@@ -48,11 +48,30 @@ html, body {
height: 100%; height: 100%;
} }
/* ---------- 冲突演示:主应用全局样式 ---------- */ /* ---------- 主应用内容区样式(限定在主应用自身页面内) ---------- */
/* 这些通用选择器如果被子应用的同名选择器覆盖,就会发生冲突 */ /*
问题:子应用在 iframe: false 模式下DOM 直接嵌入主文档,
挂在 <router-view> → <ChildApp.vue> → <micro-app> 下面,
而 <router-view> 又在 .app-main 内部。
所以 .app-main h2 这种「后代选择器」会穿透到子应用的 h2。
之前(会泄漏): h2 { ... } ← 没有作用域100% 泄漏
中间(仍泄漏): .app-main h2 { ... } ← 子应用也在 .app-main 里!
现在(已修复): .app-main > :not(.child-app-wrapper) h2 { ... }
↑ 用子选择器 > 只匹配 .app-main 的直接子元素,
再用 :not(.child-app-wrapper) 排除子应用容器,
彻底阻断样式进入 micro-app 内部
DOM 层级示意:
.app-main
├── div.home-page ← :not(.child-app-wrapper) → ✅ 样式生效
└── div.child-app-wrapper ← 被 :not() 排除 → ❌ 样式不生效
└── <micro-app>
└── 子应用 h2/button/p... ← 安全,不受主应用样式影响
*/
/* 冲突点 ①h2 — 主应用想要紫色渐变标题 */ /* 冲突点 ①h2 — 主应用想要紫色渐变标题 */
h2 { .app-main > :not(.child-app-wrapper) h2 {
font-family: 'Microsoft YaHei', sans-serif !important; font-family: 'Microsoft YaHei', sans-serif !important;
font-size: 20px !important; font-size: 20px !important;
color: #667eea !important; color: #667eea !important;
@@ -65,7 +84,7 @@ h2 {
/* 冲突点 ②button — 主应用想要紫色直角按钮 */ /* 冲突点 ②button — 主应用想要紫色直角按钮 */
button { .app-main > :not(.child-app-wrapper) button {
background: #667eea !important; background: #667eea !important;
color: #fff !important; color: #fff !important;
border: none !important; border: none !important;
@@ -76,13 +95,13 @@ button {
cursor: pointer !important; cursor: pointer !important;
letter-spacing: 0 !important; letter-spacing: 0 !important;
} }
button:hover { .app-main > :not(.child-app-wrapper) button:hover {
background: #5a6fd6 !important; background: #5a6fd6 !important;
transform: none !important; transform: none !important;
} }
/* 冲突点 ③p 段落 — 主应用想要紧凑排版 */ /* 冲突点 ③p 段落 — 主应用想要紧凑排版 */
p { .app-main > :not(.child-app-wrapper) p {
font-size: 14px !important; font-size: 14px !important;
line-height: 1.6 !important; line-height: 1.6 !important;
color: #444 !important; color: #444 !important;
@@ -90,26 +109,26 @@ p {
} }
/* 冲突点 ④table — 主应用想要紫色无边框表格 */ /* 冲突点 ④table — 主应用想要紫色无边框表格 */
table { .app-main > :not(.child-app-wrapper) table {
border-collapse: collapse !important; border-collapse: collapse !important;
width: 100% !important; width: 100% !important;
border: none !important; border: none !important;
} }
th { .app-main > :not(.child-app-wrapper) th {
background: #667eea !important; background: #667eea !important;
color: white !important; color: white !important;
padding: 8px !important; padding: 8px !important;
text-align: left !important; text-align: left !important;
font-size: 13px !important; font-size: 13px !important;
} }
td { .app-main > :not(.child-app-wrapper) td {
padding: 6px 8px !important; padding: 6px 8px !important;
border-bottom: 1px solid #e0e0e0 !important; border-bottom: 1px solid #e0e0e0 !important;
font-size: 13px !important; font-size: 13px !important;
} }
/* 冲突点 ⑤code 标签 — 主应用想要紫色代码块 */ /* 冲突点 ⑤code 标签 — 主应用想要紫色代码块 */
code { .app-main > :not(.child-app-wrapper) code {
background: #e8e0f0 !important; background: #e8e0f0 !important;
color: #5e35b1 !important; color: #5e35b1 !important;
border: 1px solid #b39ddb !important; border: 1px solid #b39ddb !important;

View File

@@ -54,13 +54,13 @@ export const subApps: SubAppConfig[] = [
url: 'http://localhost:5174/', url: 'http://localhost:5174/',
baseroute: '/mock-app', baseroute: '/mock-app',
// ============================================ // ============================================
// ⚠️ 样式冲突演示专用: // ✅ iframe: true — 浏览器原生隔离,样式/JS 完全不互通
// iframe: false → 子应用 DOM 直接嵌入主文档 // 之前(有样式穿透): iframe: false
// disableScopecss: true → 关闭样式隔离,双方样式互相泄漏 // → 子应用 DOM 嵌入主文档 → 主应用的 .app-main h2 泄漏进去
// 这是一个纯 HTML 页面(无 JS 框架),不需要 ES Module 支持 // 现在(已修复): iframe: true
// → 子应用在独立 iframe 中运行 → 完全隔离 ✅
// ============================================ // ============================================
iframe: false, iframe: true,
disableScopecss: true,
keepAlive: true, keepAlive: true,
routerMode: 'native' routerMode: 'native'
}, },
@@ -68,13 +68,8 @@ export const subApps: SubAppConfig[] = [
name: 'mock-app-2', name: 'mock-app-2',
url: 'http://localhost:5175/', url: 'http://localhost:5175/',
baseroute: '/mock-app-2', baseroute: '/mock-app-2',
// ============================================ // ✅ 同上:使用默认样式隔离
// ⚠️ 子应用间样式冲突演示:
// 与 mock-app 相同iframe: false, disableScopecss: true
// 用于验证:前一个子应用的样式是否会影响后一个子应用
// ============================================
iframe: false, iframe: false,
disableScopecss: true,
keepAlive: true, keepAlive: true,
routerMode: 'native' routerMode: 'native'
} }