样式冲突问题模板

This commit is contained in:
2026-06-21 16:31:22 +08:00
parent b14e3d1186
commit e9c570f893
5 changed files with 1273 additions and 4 deletions

View File

@@ -1234,6 +1234,944 @@ Webpack 子应用用 with 沙箱就能正常工作,用 iframe 也行——只
---
## 八、子应用路由改造与业务兼容
### Q27接入 micro-app 后,子应用的路由行为发生了哪些变化?原本基于 `route.name` 的业务判断该如何改造?
**答:**
这是一个子应用迁移中最容易忽视但影响面最大的问题。结论先行:**`route.name``route.path`(子应用内部值)不变,但 `window.location``route.fullPath` 变了。所有直接操作浏览器 URL 的代码必须改为使用 Vue Router API。**
---
#### 一、路由行为变化全景图
假设子应用 `vue3-app` 有一个 `/dashboard` 路由,用户通过主应用访问 `localhost:8080/vue3-app/dashboard`
```
┌─────────────────────────────────────────────────────────────────┐
│ 浏览器地址栏 │
│ localhost:8080/vue3-app/dashboard │
└──────────────────────────────┬──────────────────────────────────┘
主应用 Vue Router (createWebHistory)
匹配 /vue3-app/:page*
渲染 ChildApp.vue → 加载 <micro-app>
micro-app 框架: baseroute 剥离
去掉 /vue3-app 前缀
子应用内部 Vue Router (createWebHistory)
收到路径: /dashboard
匹配路由: { name: 'dashboard', path: '/dashboard' }
```
**关键结论:子应用的 Vue Router 在内部看到的是一个"干净"的路径baseroute 已剥离),所有 route 对象的 `name`、`path`、`params`、`query` 都和独立运行时完全一致。**
但是 — `window.location` 是浏览器层面的,不受 Vue Router 控制,所以它不变。
---
#### 二、各路由 API 的实际值对比
假设子应用 `/dashboard` 路由定义如下:
```ts
// 子应用 router
const routes = [
{
path: '/dashboard',
name: 'dashboard',
meta: { requiresAuth: true },
component: () => import('@/views/Dashboard.vue')
}
]
```
用户在浏览器访问:`http://localhost:8080/vue3-app/dashboard?id=1`
| API | 独立运行时的值 | 微前端环境中的值 | 是否变化 |
|-----|--------------|-----------------|---------|
| `route.name` | `'dashboard'` | `'dashboard'` | ✅ 不变 |
| `route.path` | `'/dashboard'` | `'/dashboard'` | ✅ 不变 |
| `route.params` | `{}` | `{}` | ✅ 不变 |
| `route.query` | `{ id: '1' }` | `{ id: '1' }` | ✅ 不变 |
| `route.meta` | `{ requiresAuth: true }` | `{ requiresAuth: true }` | ✅ 不变 |
| `route.fullPath` | `'/dashboard?id=1'` | ⚠️ **取决于 micro-app 版本** | ⚠️ 可能变化 |
| `route.matched` | `[dashboardRoute]` | `[dashboardRoute]` | ✅ 不变 |
| `window.location.pathname` | `'/dashboard'` | **`'/vue3-app/dashboard'`** | ❌ 变了! |
| `window.location.href` | `'http://localhost:3001/dashboard?id=1'` | **`'http://localhost:8080/vue3-app/dashboard?id=1'`** | ❌ 变了! |
| `window.location.origin` | `'http://localhost:3001'` | **`'http://localhost:8080'`** | ❌ 变了! |
---
#### 三、典型业务场景的改造方法
##### 场景 1基于 `route.name` 控制侧边栏/菜单高亮
这是最常见也最无需担心的场景。
```ts
// ✅ 这段代码在微前端环境中完全不需要改动
import { useRoute } from 'vue-router'
const route = useRoute()
// 判断当前是否在 dashboard 页面
const isDashboard = computed(() => route.name === 'dashboard')
// 侧边栏菜单项
const menuItems = [
{ title: '首页', routeName: 'home', icon: 'home' },
{ title: '控制台', routeName: 'dashboard', icon: 'dashboard' },
]
// 当前激活的菜单项
const activeMenu = computed(() => menuItems.find(item => item.name === route.name))
```
**原因:** `route.name` 是子应用内部路由器解析出来的,不经过浏览器 URL。baseroute 对它完全透明。
---
##### 场景 2基于 `route.name` 做导航守卫
```ts
// ✅ 同样不需要改动
router.beforeEach((to, from) => {
if (to.name === 'dashboard' && !isLoggedIn()) {
return { name: 'login', query: { redirect: to.path } }
}
})
```
**原因:** 导航守卫接收的 `to`/`from` 是子应用内部路由解析结果,`name``path` 都已被 micro-app 处理过。
---
##### 场景 3❌ 直接读取 `window.location.pathname` 做业务判断
这是**最容易出 bug 的写法**,必须改造。
```ts
// ❌ 错误:独立运行能工作,微前端环境判断失败
function getActiveTab() {
if (window.location.pathname === '/dashboard') {
return 'dashboard' // → 微前端环境pathname 是 '/vue3-app/dashboard',永不匹配
}
if (window.location.pathname.startsWith('/user')) {
return 'user'
}
return 'home'
}
// ✅ 正确:使用 Vue Router 的 route 对象
import { useRoute } from 'vue-router'
function getActiveTab() {
if (route.name === 'dashboard') {
return 'dashboard' // → 无论独立运行还是微前端,永远正确
}
if (route.path.startsWith('/user')) {
return 'user'
}
return 'home'
}
```
---
##### 场景 4❌ 使用 `window.location.href` 做页面跳转
```ts
// ❌ 错误:跳过 Vue Router且跳出微前端命名空间
function goToDashboard() {
window.location.href = '/dashboard'
// 微前端环境实际跳转: localhost:8080/dashboard
// 但主应用 /dashboard 可能是主应用的页面!子应用的 dashboard 实际路径是 /vue3-app/dashboard
}
// ✅ 正确:使用 Vue Router
import { useRouter } from 'vue-router'
const router = useRouter()
function goToDashboard() {
router.push({ name: 'dashboard' }) // ← 最推荐:命名路由
// 或 router.push('/dashboard') // ← 也可以micro-app 会自动加 baseroute 前缀
}
```
**为什么 `router.push('/dashboard')` 也能正常工作?**
```
子应用代码: router.push('/dashboard')
子应用 Vue Router: 解析路径 → /dashboard
micro-app 拦截 pushState
自动拼接 baseroute: /vue3-app + /dashboard = /vue3-app/dashboard
浏览器地址栏: localhost:8080/vue3-app/dashboard ✅
```
micro-app 通过**劫持 `history.pushState` / `replaceState`** 实现了路由的自动前缀拼接。
---
##### 场景 5❌ 使用 `window.open()` 打开子应用内页面
```ts
// ❌ 错误
function openDetail(id: string) {
window.open(`/detail/${id}`)
// 微前端环境实际打开: localhost:8080/detail/123
// 主应用无此路由 → 404
}
// ✅ 正确方案 1使用 Vue Router
function openDetail(id: string) {
const url = router.resolve({ name: 'detail', params: { id } }).href
window.open(url) // micro-app 已处理路径拼接
}
// ✅ 正确方案 2手动拼接 baseroute
function openDetail(id: string) {
const base = window.__MICRO_APP_BASE_ROUTE__ || ''
window.open(`${base}/detail/${id}`)
}
```
---
##### 场景 6❌ 面包屑基于 `window.location` 解析
```ts
// ❌ 错误
function getBreadcrumbs() {
const segments = window.location.pathname.split('/').filter(Boolean)
// 微前端环境: segments = ['vue3-app', 'dashboard', 'overview']
// 独立运行: segments = ['dashboard', 'overview']
// → 第一段不一致,面包屑不一致!
return segments.map((seg, i) => ({
label: seg,
path: '/' + segments.slice(0, i + 1).join('/')
}))
}
// ✅ 正确:基于 route.matched
import { useRoute } from 'vue-router'
function getBreadcrumbs() {
return route.matched
.filter(r => r.meta?.title)
.map(r => ({
label: r.meta.title as string,
path: r.path
}))
}
// 路由定义中配置 meta.title
// { path: '/dashboard', name: 'dashboard', meta: { title: '控制台' } }
```
---
##### 场景 7❌ 接口回调 URL / OAuth 重定向使用硬编码路径
```ts
// ❌ 错误OAuth 回调写死路径
function login() {
const callbackUrl = 'http://localhost:3001/auth/callback'
// 微前端环境的实际域名是 localhost:8080端口也不对
window.location.href = `https://oauth.example.com?redirect=${callbackUrl}`
}
// ✅ 正确:动态构建完整 URL
function login() {
const base = window.__MICRO_APP_BASE_ROUTE__ || ''
const callbackPath = router.resolve({ name: 'authCallback' }).href
const callbackUrl = window.location.origin + callbackPath
// → localhost:8080/vue3-app/auth/callback
window.location.href = `https://oauth.example.com?redirect=${callbackUrl}`
}
```
---
#### 四、改造原则总结
```
是否涉及 URL 字符串?
┌────────┴────────┐
│ │
否 是
│ │
▼ ▼
✅ 不需要改 这个 URL 是谁用的?
┌──────────┼──────────┐
│ │ │
Vue Router 外部系统 浏览器跳转
router.push 回调URL window.open
route.name fetch location.href
route.path
│ │ │
▼ ▼ ▼
✅ 不改 ⚠️ 需要 ⚠️ 需要
micro-app 手动拼接 使用 router
自动处理 baseroute API
```
**核心口诀:**
| 旧习惯 | 新习惯 | 原因 |
|--------|--------|------|
| `window.location.pathname` | `route.path` / `route.name` | route 对象已被 micro-app 处理,值正确 |
| `window.location.href = '...'` | `router.push(...)` | 走 Vue Routermicro-app 自动加前缀 |
| `window.open('/xxx')` | `window.open(router.resolve(...).href)` | 手动解析出带 baseroute 的正确路径 |
| 手动 `split('/')` 解析路径 | `route.matched` + `meta` | 用路由元信息替代字符串解析 |
| 硬编码完整 URL | `window.location.origin + baseroute + path` | 动态拼接,适配任何环境 |
---
#### 五、一个完整的改造前后对比
```ts
// ============================================
// 改造前 — 充满隐患的旧代码
// ============================================
export function useNavigation() {
// ❌ 基于 window.location 判断
const currentPage = window.location.pathname.split('/').pop()
function navigateTo(page: string) {
// ❌ 硬编码路径
window.location.href = `/${page}`
}
function isActive(pageName: string): boolean {
// ❌ 字符串匹配
return window.location.pathname.includes(pageName)
}
// ❌ 直接拼接 URL 给外部
function getShareUrl(): string {
return `http://localhost:3001${window.location.pathname}`
}
return { currentPage, navigateTo, isActive, getShareUrl }
}
// ============================================
// 改造后 — 微前端安全的代码
// ============================================
import { useRouter, useRoute } from 'vue-router'
export function useNavigation() {
const router = useRouter()
const route = useRoute()
// ✅ 基于 route.name与环境无关
const currentPage = computed(() => route.name as string)
function navigateTo(name: string) {
// ✅ 命名路由跳转micro-app 自动处理 URL
router.push({ name })
}
function isActive(name: string): boolean {
// ✅ 精确匹配 route.name
return route.name === name
}
function getShareUrl(): string {
// ✅ 动态拼接完整路径
const base = (window as any).__MICRO_APP_BASE_ROUTE__ || ''
const fullPath = base + route.fullPath
return window.location.origin + fullPath
}
return { currentPage, navigateTo, isActive, getShareUrl }
}
```
---
## 九、CORS 跨域排错实战
### Q28子应用加载时报 CORS 错误 `No 'Access-Control-Allow-Origin' header is present`,如何排查和修复?
**答:**
这是搭建样式冲突 Demo 时实际遇到的一个排错案例,完整的排查链路非常有教学价值。
---
#### 一、错误现场
**控制台报错:**
```
Access to fetch at 'http://localhost:5173/' from origin 'http://localhost:8080'
has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the requested resource.
```
**micro-app 错误回调:**
```
[micro-app] 子应用 vue2-app 加载错误:
CustomEvent { isTrusted: false, detail: {...}, type: 'error', ... }
```
**用户看到的现象:** 子应用页面白屏(或一直显示 Loading无法加载。
---
#### 二、排查步骤
```
第 1 步:确认子应用服务器是否在运行
─────────────────────────────────
curl -s -o /dev/null -w "%{http_code}" http://localhost:5173/
→ 200 ✅ 服务器在运行HTML 能正常返回
第 2 步:检查响应头中是否有 CORS 头
─────────────────────────────────
curl -s -I http://localhost:5173/ | grep -i access-control
→ (空) ❌ 没有 Access-Control-Allow-Origin 头!
第 3 步:确认请求的 Origin 和服务器的 Origin 是否不同
─────────────────────────────────
主应用: http://localhost:8080
子应用: http://localhost:5173
→ 不同端口 = 不同源 → 浏览器强制 CORS 检查
第 4 步:定位服务器为什么不发 CORS 头
─────────────────────────────────
当前使用: npx serve -p 5173
→ npx serve 默认不添加 CORS 头 ❌
```
---
#### 三、根因分析
```
┌─────────────────────────────────────────────────────────────┐
│ 浏览器同源策略 │
│ │
│ http://localhost:8080 (主应用) │
│ │ │
│ │ fetch('http://localhost:5173/') │
│ │ 协议相同 (http) │
│ │ 域名相同 (localhost) │
│ │ 端口不同 (8080 ≠ 5173) ← 跨域! │
│ │ │
│ ▼ │
│ http://localhost:5173 (子应用服务器) │
│ │ │
│ │ 响应头中没有 Access-Control-Allow-Origin │
│ │ │
│ ▼ │
│ ❌ 浏览器拦截响应 → 子应用加载失败 │
└─────────────────────────────────────────────────────────────┘
```
**本质:** `npx serve` 是一个简化版静态文件服务器,设计目标是本地预览,不包含 CORS 配置。在微前端场景下,主应用需要通过 `fetch()` 跨域请求子应用的 HTML所以子应用服务器**必须**返回 CORS 头。
**注意:** 即使子应用服务器正常运行、curl 能拿到 HTML浏览器也会拦截跨域请求的响应。这是浏览器安全策略不是网络问题。
---
#### 四、解决方案
**方案一:写一个带 CORS 的 Node.js 服务器Demo 采用)**
```js
// server.js — 最简单的带 CORS 的静态文件服务器
const http = require('http')
const fs = require('fs')
const path = require('path')
const PORT = 5173
http.createServer((req, res) => {
// 每个响应都加上 CORS 头
res.setHeader('Access-Control-Allow-Origin', '*')
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
res.setHeader('Access-Control-Allow-Headers', '*')
// 处理预检请求
if (req.method === 'OPTIONS') {
res.writeHead(204)
res.end()
return
}
// 返回静态文件
let filePath = req.url === '/' ? '/index.html' : req.url.split('?')[0]
const fullPath = path.join(__dirname, filePath)
fs.readFile(fullPath, (err, data) => {
if (err) {
res.writeHead(404)
res.end('Not Found')
} else {
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
res.end(data)
}
})
}).listen(PORT, () => {
console.log(`Mock sub-app with CORS: http://localhost:${PORT}/`)
})
```
```bash
node server.js
# → Mock sub-app with CORS: http://localhost:5173/
```
**验证修复:**
```bash
curl -s -I http://localhost:5173/ | grep -i access-control
# → Access-Control-Allow-Origin: * ✅
# → Access-Control-Allow-Methods: GET, POST ✅
```
**方案二Vite 项目自带 CORS推荐用于真实子应用**
```ts
// vite.config.ts
export default defineConfig({
server: {
port: 3001,
headers: {
'Access-Control-Allow-Origin': '*'
}
}
})
```
Vite 的 dev server 本身支持自定义 headers不需要额外写服务器代码。
**方案三Webpack 项目配置 devServer**
```js
// vue.config.js 或 webpack.config.js
module.exports = {
devServer: {
port: 3000,
headers: {
'Access-Control-Allow-Origin': '*'
}
}
}
```
**方案四nginx 反向代理(生产环境)**
```nginx
location /child-app/ {
proxy_pass http://localhost:5173/;
add_header Access-Control-Allow-Origin *;
}
```
---
#### 五、排查 CORS 问题的通用检查清单
| 检查项 | 命令/方法 | 通过标准 |
|--------|-----------|----------|
| 子应用服务器在运行 | `curl -s -o /dev/null -w "%{http_code}" http://localhost:PORT/` | 返回 200 |
| 有 CORS 头 | `curl -s -I http://localhost:PORT/ \| grep -i access-control` | 看到 `Access-Control-Allow-Origin` |
| 主应用和子应用同源? | 比较 `protocol + host + port` | 如果同源则不需要 CORS |
| 浏览器 Network 面板 | F12 → Network → 找到子应用请求 → Response Headers | 有 CORS 相关头 |
| 控制台无 CORS 报错 | F12 → Console | 无 `blocked by CORS policy` |
---
#### 六、常见 CORS 相关误区
| 误区 | 纠正 |
|------|------|
| "curl 能返回 HTML说明服务器没问题" | curl 不执行同源策略浏览器会执行。curl 成功 ≠ 浏览器能加载 |
| "两个 localhost 不同端口也跨域?" | 对,同源 = 协议+域名+端口**完全相同**。不同端口就是跨域 |
| "加了 CORS 头就不安全了" | `Access-Control-Allow-Origin: *` 确实允许任何来源,生产环境建议限定具体域名 |
| "生产环境部署在同域就不需要 CORS" | 正确。如果子应用部署在主应用的子路径(如 `/vue3-app/`),由 nginx 统一转发,浏览器看到的是同源请求,不需要 CORS |
---
## 十、样式冲突深度剖析
### Q29mock-app 的 h2 样式(橙色系)在页面上实际渲染出来了吗?有样式冲突吗?为什么?
**答:**
这是一个考察 CSS 级联Cascade机制的经典问题。答案分两层**冲突确实存在,但不是"谁覆盖谁",而是产生了双方都不想要的"混合样式"。**
---
#### 一、两套样式规则对比
```css
/* 主应用的 h2紫色系*/
h2 {
font-family: 'Microsoft YaHei', sans-serif !important;
font-size: 20px !important;
color: #667eea !important;
border-bottom: 2px solid #667eea !important; /* ← 底边框 */
padding-bottom: 8px !important; /* ← 下内边距 */
margin: 12px 0 !important;
text-transform: none !important;
border-left: none !important; /* ← 明确取消左边框 */
}
/* mock-app 的 h2橙色系*/
h2 {
font-family: 'Georgia', serif !important;
font-size: 32px !important;
color: #ff6b35 !important;
border-left: 6px solid #ff6b35 !important; /* ← 左边框 */
padding-left: 14px !important; /* ← 左内边距 */
margin: 18px 0 !important;
text-transform: uppercase !important;
}
```
---
#### 二、为什么会产生冲突:两个条件同时满足
`subApps.ts` 中 mock-app 的配置:
```ts
{
name: 'mock-app',
iframe: false, // ① DOM 直接嵌入主文档,无隔离
disableScopecss: true, // ② 关闭样式前缀h2 就是全局 h2
}
```
| 条件 | 作用 | 如果没有会怎样 |
|------|------|---------------|
| `iframe: false` | 子应用 HTML 被直接注入主文档的 DOM 树 | `iframe: true` → 独立文档,双方样式物理隔离 |
| `disableScopecss: true` | micro-app 不为子应用 CSS 加 `micro-app[name=mock-app]` 前缀 | 子应用的 `h2` 变成 `micro-app[name=mock-app] h2`,只作用于子应用内部 |
**两者叠加 → 子应用的 `h2 { ... }` 就是整个文档的 `h2 { ... }`。**
---
#### 三、CSS 属性级别的逐条分析
关键认知:**CSS 的覆盖单位是"属性",不是"规则"。** 浏览器不会整体选择"用主应用的 h2"还是"用子应用的 h2",而是逐属性比较。
由于两套规则都使用了 `!important` 且选择器都是 `h2`(特异性相同),**后加载的样式表在冲突属性上胜出**。mock-app 的样式是在主应用样式之后由 micro-app 注入的,所以 mock-app 的属性在冲突时优先。
下面是 h2 元素实际渲染结果的逐属性分析:
```
h2 最终渲染结果:
┌──────────────────────────────────────────────────────┐
│ │
│ ┃ ← 橙色左边框 (border-left: 6px solid #ff6b35) │
│ ┃ 来源: mock-app胜出覆盖了主应用的 none
│ ┃ │
│ ┃ GEORGIA 32px 橙色大写文字 │
│ ┃ 来源: mock-appfont-family/size/color/text-transform 均胜出)│
│ ┃ │
│ ━━━━━ ← 紫色底部边框 (border-bottom: 2px solid #667eea) │
│ 来源: 主应用mock-app 没设这个属性,所以保留) │
│ │
└──────────────────────────────────────────────────────┘
```
| CSS 属性 | 主应用设置的值 | mock-app 设置的值 | 最终渲染 | 胜出方 |
|----------|--------------|------------------|---------|--------|
| `font-family` | `'Microsoft YaHei', sans-serif` | `'Georgia', serif` | **Georgia** | mock后加载 |
| `font-size` | `20px` | `32px` | **32px** | mock |
| `color` | `#667eea` (紫色) | `#ff6b35` (橙色) | **橙色** | mock |
| `text-transform` | `none` | `uppercase` | **uppercase** | mock |
| `margin` | `12px 0` | `18px 0` | **18px 0** | mock |
| `border-left` | `none` | `6px solid #ff6b35` | **6px 橙色** | mock |
| `padding-left` | (未设置) | `14px` | **14px** | mock |
| **`border-bottom`** | `2px solid #667eea` | **(未设置)** | **2px 紫色** | ⚠️ 主应用! |
| **`padding-bottom`** | `8px` | **(未设置)** | **8px** | ⚠️ 主应用! |
---
#### 四、关键洞察:这不是覆盖,是缝合
**这就是样式冲突的本质 — 它不是 A 替代 B而是 A 和 B 搅拌在一起:**
- mock-app 的设计师期望 h2 是一个纯橙色的标题,没有底边框
- 主应用的设计师期望 h2 是一个纯紫色的标题,没有左边框
- 实际渲染:**既有橙色左边框,又有紫色底边框** — 双方都不想要这个效果
```
预期的 mock-app h2: 预期的 main-app h2:
┃ GEORGIA 橙色 大写 Microsoft YaHei 紫色
┃ ━━━━━━━━━━━━━━
实际渲染(冲突产物):
┃ GEORGIA 橙色 大写
┃ ← 橙色左边框 (mock-app 的)
━━━ ← 紫色底边框 (主应用的)
← 双方都不想要的"缝合怪"
```
---
#### 五、为什么 `border-left` mock 胜出而 `border-bottom` 主应用胜出?
这是 CSS Cascade 中最容易误解的点:
```
border-left:
- 主应用明确设置了 border-left: none
- mock-app 明确设置了 border-left: 6px solid #ff6b35
- 两者都声明了同一个属性 → 后加载的 mock-app 胜出 ✅
border-bottom:
- 主应用设置了 border-bottom: 2px solid #667eea
- mock-app 没有设置 border-bottom ← 关键!
- 只有一方声明 → 不存在冲突 → 主应用的设置保留 ✅
```
**CSS 级联规则:** 后加载的规则只会覆盖它**声明了的属性**不会影响前一个规则中它未涉及的属性。mock-app 没有声明 `border-bottom`,所以主应用的 `border-bottom` 永远不会被 mock-app 覆盖。
---
#### 六、如果 `disableScopecss: false`(默认),会怎样?
当样式隔离开启时micro-app 会自动给子应用的 CSS 规则添加作用域前缀:
```css
/* 子应用写的 */
h2 { color: #ff6b35; }
/* micro-app 处理后注入(概念示意)*/
micro-app[name=mock-app] h2 { color: #ff6b35; }
```
此时:
- 主应用的 `h2 { color: #667eea }` 作用于整个文档
- 子应用的 `micro-app[name=mock-app] h2 { color: #ff6b35 }` 只作用于 `<micro-app name="mock-app">` 内部
- 两个规则的选择器不同,各管各的区域,**零冲突**
```
主应用 h2 → color: purple 子应用内部的 h2 → color: orange
(选择性作用在全局 h2) (选择性作用在 micro-app[name=mock-app] 内的 h2)
```
---
#### 七、实际验证方法
在浏览器中打开 `http://localhost:8081/mock-app`,按 F12 打开开发者工具:
```bash
# 1. 检查 h2 元素的实际样式
# F12 → Elements → 选中 mock-app 内的 h2 → Styles 面板
# 你会看到:
# h2 {
# font-family: 'Georgia', serif !important; ← (来自 mock-app)
# font-size: 32px !important; ← (来自 mock-app)
# color: #ff6b35 !important; ← (来自 mock-app)
# border-left: 6px solid #ff6b35 !important; ← (来自 mock-app)
# border-bottom: 2px solid #667eea !important; ← (来自主应用!被 mock-app 覆盖失败)
# ... ← 有些属性来自主应用,有些来自 mock-app
# }
# 2. 被划掉strikethrough的属性说明它被覆盖了
# 3. 注意 border-bottom 没有被划掉 — 因为 mock-app 根本没声明它
```
你会清楚地看到:**同一个 `h2` 的 Computed Style 里,既有主应用的紫色底边框,又有子应用的橙色左边框。**
---
#### 八、总结
| 问题 | 答案 |
|------|------|
| mock-app 的 h2 样式符合预期吗? | **不符合。** mock-app 期望纯橙色无底边框,实际多了紫色底边框 |
| 有样式冲突吗? | **有。** 颜色/字体/大小 mock 胜出(预期内),但底边框主应用残留(预期外) |
| 为什么有冲突? | `iframe: false` + `disableScopecss: true` → 两套 h2 规则作用于同一文档,属性级搅拌 |
| 为什么底边框没被覆盖? | mock-app 没声明 `border-bottom`CSS 级联只覆盖声明的属性,不覆盖未涉及的 |
**核心教训:** CSS 冲突不是"整个规则"的替换,而是"逐属性"的混搭。在 `disableScopecss: true` 时,任何一个子应用未声明而主应用声明了的属性,都会在主应用的默认值上"残留",产生双方都不想要的缝合效果。
---
### Q30子应用之间的样式会互相影响吗keepAlive 在其中扮演了什么角色?
**答:**
会。而且 **keepAlive 会让子应用间的样式冲突更隐蔽、更严重**。我们通过创建两个 mock 子应用进行了实际实验验证。
---
#### 一、实验设计
| 子应用 | 颜色主题 | 关键样式特征 | 端口 | 配置 |
|--------|----------|-------------|------|------|
| `mock-app` | 🟠 橙色 | h2 左边框 `border-left: 6px solid #ff6b35` | 5174 | `iframe: false, disableScopecss: true, keepAlive: true` |
| `mock-app-2` | 🔵 青色 | h2 右边框 `border-right: 5px solid #00838f` | 5175 | 同上 |
两个子应用都用了 `iframe: false` + `disableScopecss: true` + `keepAlive: true`
---
#### 二、实验步骤与结果
```
步骤 1: 访问 http://localhost:8081/mock-app橙色
→ mock-app 的 CSS 被注入主文档
→ 文档中现有: main h2 + mock-app h2
步骤 2: 点击导航回到首页
→ mock-app DOM 隐藏keepAlive
→ ⚠️ 但 mock-app 的 <style> 标签仍在主文档 <head> 中!
步骤 3: 访问 http://localhost:8081/mock-app-2青色
→ mock-app-2 的 CSS 也被注入主文档
→ 文档中现有: main h2 + mock-app h2 + mock-app-2 h2
→ 三个 h2 规则同时存在!
```
**mock-app-2 的 h2 实际渲染结果(预期 vs 实际):**
```
预期mock-app-2 设计师想要):
青色文字右对齐 → 右边青色边框
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━→
实际(被 mock-app 污染后):
┃ ← 橙色左边框mock-app 残留mock-app-2 根本没写 border-left
┃ 青色文字右对齐 → 右边青色边框mock-app-2 本意)
┃ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━→
┃ ← 同时存在橙色左 + 青色右,双方都不想要!
```
---
#### 三、keepAlive 是"帮凶"
```
无 keepAlive 时:
访问 mock-app → CSS 注入
切换到首页 → mock-app 被销毁CSS 被移除 ✅
访问 mock-app-2 → 只有 mock-app-2 的 CSS ✅
→ 无残留问题
有 keepAlive 时:
访问 mock-app → CSS 注入
切换到首页 → mock-app DOM 隐藏CSS 保留在 <head> ❌
访问 mock-app-2 → mock-app 的 CSS + mock-app-2 的 CSS 共存 ❌
→ 前一个子应用的样式污染了后一个子应用!
```
**为什么会这样?**
micro-app 的 keepAlive 机制只负责隐藏/恢复子应用的 DOM**不会去 `<head>` 中移除 `<style>` 标签**。这本身是性能优化(下次恢复时不需要重新注入 CSS但在 `disableScopecss: true` 的场景下,这些残留的全局样式就成了污染源。
---
#### 四、冲突范围矩阵(完整版)
| 配置 | 主应用↔子应用 | 子应用↔子应用 | 原理 |
|------|:---:|:---:|------|
| `iframe: true` | ❌ 无 | ❌ 无 | iframe 独立文档,物理隔离 |
| `iframe: false` + `disableScopecss: false`(默认) | ❌ 无 | ❌ 无 | micro-app 自动给子应用 CSS 加 `[name=xxx]` 作用域前缀 |
| **`iframe: false` + `disableScopecss: true`** | **✅ 有** | **✅ 有** | 双方 `h2` 都是全局选择器,同一文档内逐属性混搭 |
| `iframe: false` + `disableScopecss: true` + **`keepAlive: true`** | **✅ 有** | **✅ 更严重** | 前一个子应用的 CSS 残留不清理,与后一个的 CSS 叠加 |
---
┌──────────────────────────────────────────────────────────────┬────────────────┬─────────────────────────────────────────────────────────┐
│ 场景 │ 子应用间冲突? │ 原因 │
├──────────────────────────────────────────────────────────────┼────────────────┼─────────────────────────────────────────────────────────┤
│ 两方都 iframe: true │ ❌ 无 │ iframe 独立文档,物理隔离 │
├──────────────────────────────────────────────────────────────┼────────────────┼─────────────────────────────────────────────────────────┤
│ 一方 iframe: true一方 iframe: false │ ❌ 无 │ iframe 内的样式出不来,传不进去 │
├──────────────────────────────────────────────────────────────┼────────────────┼─────────────────────────────────────────────────────────┤
│ 两方都 iframe: false, disableScopecss: false │ ❌ 无 │ micro-app 自动添加 micro-app[name=xxx] 前缀 │
├──────────────────────────────────────────────────────────────┼────────────────┼─────────────────────────────────────────────────────────┤
│ 两方都 iframe: false, disableScopecss: true │ ✅ 有! │ 所有 h2/button/p 等全局选择器作用在同一文档,逐属性混搭 │
├──────────────────────────────────────────────────────────────┼────────────────┼─────────────────────────────────────────────────────────┤
│ 两方都 iframe: false, disableScopecss: true, keepAlive: true │ ✅ 更严重! │ 前一个子应用的 CSS 残留 + 后一个子应用的 CSS 同时存在 │
└──────────────────────────────────────────────────────────────┴────────────────┴─────────────────────────────────────────────────────────┘
你可以现在就在浏览器里验证 — 先访问 mock-app再切换到 mock-app-2观察 h2 是否同时出现了橙色左边框和青色右边框。这就是子应用间样式污染的真实表现。
#### 五、在浏览器中亲自验证的方法
```
1. 打开 http://localhost:8081/mock-app
F12 → Elements → <head> 观察 <style> 标签(橙色主题)
2. 导航到首页 /home
F12 → <head> 中橙色 <style> 标签仍然存在keepAlive 未清理)
3. 打开 http://localhost:8081/mock-app-2
F12 → <head> 中同时存在:
- 主应用的紫色 <style>
- mock-app 的橙色 <style> ← 残留!
- mock-app-2 的青色 <style>
4. 选中 h2 元素 → Styles 面板
你会看到 border-left: orange 被划掉(或被覆盖)
同时 border-right: teal 正常生效
→ Computed 面板中两个边框同时存在
```
---
#### 六、如何杜绝子应用间样式冲突
| 方案 | 做法 | 代价 |
|------|------|------|
| **用 `iframe: true`**(最推荐) | 每个子应用独立 iframe物理隔离 | 性能略降Vite 项目本来就必须用 |
| **用 `disableScopecss: false`**(默认) | 让 micro-app 自动加作用域前缀 | 几乎无代价,是最佳默认值 |
| **关掉 keepAlive** | 切换时销毁子应用CSS 一并移除 | 切换回来需重新加载 |
| **子应用用 BEM/Scoped CSS** | 不强依赖全局选择器 | 工程量大,需改造存量代码 |
**推荐组合:**
```
Vite 子应用 → iframe: true (物理隔离 + ES Module 兼容)
Webpack 子应用 → iframe: false + 默认 scoped (性能好 + 样式安全)
纯 HTML mock → iframe: false + 默认 scoped (除非刻意演示冲突)
```
---
#### 七、总结
| 问题 | 答案 |
|------|------|
| 子应用之间会互相影响吗? | **会**,当 `iframe: false` + `disableScopecss: true` 时 |
| keepAlive 有什么影响? | **加重**冲突 — 隐藏子应用时不清理 `<style>`CSS 残留累积 |
| 如何验证? | F12 → Elements → `<head>``<style>` 标签数量 — 访问过的子应用越多,残留的 `<style>` 越多 |
| 如何避免? | 默认配置(`disableScopecss: false`就是安全的Vite 子应用 `iframe: true` 物理隔离更安全 |
**一句话:主应用 ↔ 子应用之间有冲突,子应用 ↔ 子应用之间同样有,而且 keepAlive 让后者更隐蔽。三者在同一文档中共同搅拌所有同名的全局 CSS 属性。**
---
## 附录:接入新子应用检查清单
| 步骤 | 文件 | 操作 |