样式冲突问题解决
This commit is contained in:
@@ -30,17 +30,181 @@
|
||||
### Q2:micro-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 DOM,DOM 树级别隔离 │
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
2. **JS 沙箱隔离**(`disable-sandbox`,默认开启):
|
||||
- **with 沙箱**:通过 `with(window.proxy)` + `new Function()` 执行子应用 JS,将子应用的全局变量操作代理到隔离的 microWindow 上
|
||||
- **iframe 沙箱**:将子应用放入 iframe 中运行,利用浏览器原生的 iframe 隔离
|
||||
---
|
||||
|
||||
3. **元素隔离**(`shadowDOM`,可选):
|
||||
- 将子应用放入 Shadow DOM,彻底隔离 DOM 树
|
||||
**一、CSS 样式隔离 — CSS 前缀改写(默认方案)**
|
||||
|
||||
**工作原理:**
|
||||
|
||||
```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 类型问题
|
||||
|
||||
### Q9:micro-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: false(with 沙箱) 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` 保活机制的原理是什么?有什么注意事项?
|
||||
|
||||
**答:**
|
||||
@@ -572,3 +926,159 @@ export default defineConfig({
|
||||
| 路由通配符(Vue Router 4) | `:page*`,不是 `*` |
|
||||
| lifeCycles 回调参数 | `(e: CustomEvent, appName: string)` |
|
||||
| 子应用路由 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 特性(如全局字体),兼容性成本高。
|
||||
|
||||
**方案 4:CSS @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()` 排除子应用容器。
|
||||
|
||||
Reference in New Issue
Block a user