微前端(无界)样式架构重构方案

日期: 2025-02-10
影响范围: 主应用 apps/main、共享包 packages/ui、所有子应用


一、现状诊断

1.1 样式文件分布(主应用)

当前主应用存在 3 个互不关联的样式目录,没有统一入口:

apps/main/src/
├── assets/
│   ├── main.css              ← Vue 脚手架残留 + Tailwind 入口
│   ├── base.css              ← Vue 脚手架残留 CSS Reset + 变量
│   └── styles/
│       └── micro-app.css     ← 微前端"隔离"样式(!important 硬覆盖)
├── styles/
│   ├── index.scss            ← 样式入口(⚠️ 从未被任何文件引用!)
│   ├── var.css               ← 布局 + 主题 CSS 变量
│   ├── dark.css              ← 暗色主题变量
│   ├── resizeStyle.scss      ← 632 行巨型文件(EP 覆盖 + 布局 + 业务混杂)
│   ├── dialog.scss           ← 弹窗尺寸
│   ├── theme.scss            ← 空文件(全注释)
│   ├── variables.scss        ← 仅 2 个 SCSS 变量
│   └── global.module.scss    ← 导出命名空间给 JS

1.2 main.ts 样式导入链

main.ts
  ├── assets/main.css           → @import base.css + @import tailwindcss
  ├── assets/styles/micro-app.css
  ├── element-plus 组件样式 ×4(message-box / message / notification / loading)
  └── @cmclink/ui/styles        → variables + element-override + base + utilities + animations + tailwindcss

关键发现styles/index.scss(包含 var.cssresizeStyle.scssdialog.scss在整个项目中没有被任何文件 import。这意味着 632 行的 Element Plus 覆盖样式、布局变量、暗色主题等可能完全没有生效,或者曾经生效但在某次重构中被遗漏。

1.3 核心问题清单

#问题严重程度说明
1Tailwind CSS 重复引入🔴 高assets/main.css@cmclink/ui/styles 各引入一次,产生重复样式
2CSS Reset 重复执行🔴 高assets/base.css@cmclink/ui/base.scss 各做一次全局 reset
3主色定义冲突🔴 高var.css#005aae@cmclink/ui#004889resizeStyle.scss→硬编码 #005aae,三处不一致
4styles/index.scss 未被引用🔴 高632 行 EP 覆盖 + 布局变量 + 暗色主题可能完全未生效
5body 样式双重定义🟡 中base.css@cmclink/ui/base.scss 都设置了 body 样式
6微前端样式隔离 = !important 战争🔴 高micro-app.css!important 硬覆盖,没有利用 wujie CSS 沙箱
7脚手架残留代码🟡 中base.css--vt-c-* 变量、main.css.green
8废文件堆积🟡 中theme.scss(空)、variables.scss(2 行)、global.module.scss(价值存疑)
9!important 泛滥🟡 中resizeStyle.scssmicro-app.css 大量使用
10样式职责不清🔴 高EP 覆盖、页面布局、业务样式、主题变量全混在一个文件

1.4 @cmclink/ui 样式包分析

packages/ui/src/styles/ 已经有一套相对完整的设计体系:

  • variables.scss — 完整的设计令牌(颜色、字体、间距、圆角、阴影等)+ CSS 变量映射
  • mixins.scss — 响应式断点、文本截断、布局、按钮变体等混合器
  • base.scss — 全局 reset、标题、段落、表格、滚动条、打印、暗色、无障碍
  • element-override.scss — Element Plus 样式覆盖
  • utilities.scss — CMC 专用工具类(cmc-* 前缀)
  • animations.scss — 动画库(1250 行,含大量与 Tailwind 重复的工具类)

问题@cmclink/uianimations.scss 中有大量 .translate-*.scale-*.rotate-*.duration-* 等类名,与 Tailwind CSS 完全重复。


二、目标架构设计

2.1 设计原则

  1. 单一真相源(Single Source of Truth):设计令牌只在 @cmclink/ui 中定义一次
  2. 分层隔离:全局样式、主题变量、EP 覆盖、布局样式、业务样式严格分层
  3. 微前端天然隔离:依赖 wujie 的 CSS 沙箱(WebComponent + iframe),而非 !important
  4. Tailwind 唯一入口:整个主应用只在一个地方引入 Tailwind CSS
  5. 可维护性:每个文件职责单一,命名语义化,新人 5 分钟能理解结构

2.2 目标目录结构

apps/main/src/
├── styles/                          ← 主应用样式唯一目录
│   ├── index.scss                   ← ⭐ 唯一入口文件(main.ts 只 import 这一个)
│   ├── tailwind.css                 ← Tailwind CSS 唯一引入点
│   │
│   ├── tokens/                      ← 主应用级设计令牌(覆盖/扩展 @cmclink/ui)
│   │   ├── _variables.scss          ← SCSS 变量(供 SCSS 文件内部使用)
│   │   └── _css-variables.scss      ← CSS 自定义属性(布局变量、主应用专属变量)
│   │
│   ├── themes/                      ← 主题系统
│   │   ├── _light.scss              ← 亮色主题变量
│   │   └── _dark.scss               ← 暗色主题变量
│   │
│   ├── overrides/                   ← Element Plus 样式覆盖(主应用级)
│   │   ├── _button.scss             ← 按钮覆盖
│   │   ├── _table.scss              ← 表格覆盖
│   │   ├── _form.scss               ← 表单覆盖(filterBox 等)
│   │   ├── _dialog.scss             ← 弹窗覆盖
│   │   ├── _message-box.scss        ← 消息框覆盖
│   │   ├── _select.scss             ← 下拉框覆盖
│   │   ├── _tag.scss                ← 标签覆盖
│   │   ├── _tabs.scss               ← 标签页覆盖
│   │   └── _index.scss              ← EP 覆盖汇总入口
│   │
│   ├── layout/                      ← 布局样式
│   │   ├── _app-view.scss           ← AppViewContent / AppViewScroll
│   │   ├── _login.scss              ← 登录页样式
│   │   └── _index.scss              ← 布局汇总入口
│   │
│   ├── vendors/                     ← 第三方库样式适配
│   │   └── _nprogress.scss          ← NProgress 主题色适配
│   │
│   └── legacy/                      ← 遗留业务样式(逐步迁移到组件 scoped 中)
│       ├── _si-detail.scss          ← 出口单证详情
│       ├── _marketing.scss          ← 营销模块
│       └── _index.scss              ← 遗留样式汇总入口
│
├── assets/                          ← 仅保留静态资源
│   └── images/                      ← 图片资源
│       ├── avatar.gif
│       ├── cmc-logo.png
│       └── ...

2.3 入口文件设计

styles/index.scss(唯一入口):

// =============================================================================
// CMCLink 主应用样式入口
// 加载顺序严格按照优先级排列,请勿随意调整
// =============================================================================

// 1️⃣ Tailwind CSS(最先加载,作为基础原子类层)
@use './tailwind.css';

// 2️⃣ 主应用级设计令牌(覆盖/扩展 @cmclink/ui 的变量)
@use './tokens/css-variables';

// 3️⃣ 主题系统
@use './themes/light';
@use './themes/dark';

// 4️⃣ Element Plus 样式覆盖
@use './overrides/index';

// 5️⃣ 布局样式
@use './layout/index';

// 6️⃣ 第三方库适配
@use './vendors/nprogress';

// 7️⃣ 遗留业务样式(逐步清理)
@use './legacy/index';

main.ts(重构后):

// 样式:唯一入口
import './styles/index.scss'

// Element Plus 反馈组件样式(非按需导入的全局组件)
import 'element-plus/es/components/message-box/style/css'
import 'element-plus/es/components/message/style/css'
import 'element-plus/es/components/notification/style/css'
import 'element-plus/es/components/loading/style/css'

// 组件库样式(@cmclink/ui 提供设计令牌 + 基础样式 + EP 覆盖)
import '@cmclink/ui/styles'

// 其余保持不变...

注意@cmclink/ui/styles 中的 Tailwind CSS 引入需要移除,改为只在主应用的 styles/tailwind.css 中引入一次。

2.4 样式加载顺序

┌─────────────────────────────────────────────────────────┐
│ 1. Tailwind CSS(原子类基础层)                           │
├─────────────────────────────────────────────────────────┤
│ 2. @cmclink/ui/styles                                   │
│    ├── variables.scss  → 设计令牌 + CSS 变量映射          │
│    ├── base.scss       → 全局 reset + 排版               │
│    ├── element-override.scss → 组件库级 EP 覆盖           │
│    ├── utilities.scss  → CMC 工具类                       │
│    └── animations.scss → 动画(需清理与 Tailwind 重复部分)│
├─────────────────────────────────────────────────────────┤
│ 3. Element Plus 反馈组件样式(message/notification 等)    │
├─────────────────────────────────────────────────────────┤
│ 4. 主应用 styles/index.scss                              │
│    ├── tokens/    → 主应用级变量(布局尺寸、主题色扩展)    │
│    ├── themes/    → 亮色/暗色主题                         │
│    ├── overrides/ → 主应用级 EP 覆盖                      │
│    ├── layout/    → 页面布局                              │
│    ├── vendors/   → 第三方库适配                          │
│    └── legacy/    → 遗留业务样式                          │
├─────────────────────────────────────────────────────────┤
│ 5. 组件 <style scoped>(组件级样式,天然隔离)             │
└─────────────────────────────────────────────────────────┘

三、微前端样式隔离策略

3.1 wujie CSS 沙箱机制

wujie 使用 WebComponent(shadowDOM)+ iframe 双重沙箱:

  • JS 沙箱:子应用 JS 运行在 iframe 中,天然隔离
  • CSS 沙箱:子应用 DOM 渲染在 WebComponent 的 shadowDOM 中,样式天然隔离

这意味着:

  • ✅ 子应用的样式不会泄漏到主应用
  • ✅ 主应用的样式不会侵入子应用(shadowDOM 边界)
  • ⚠️ 但是:Element Plus 的弹窗(Dialog/Drawer/MessageBox)默认挂载到 document.body,会逃逸出 shadowDOM

3.2 弹窗逃逸问题的正确解决方案

当前做法(错误):在主应用 micro-app.css 中用 !important 覆盖子应用弹窗样式。

正确做法:在子应用中配置 Element Plus 的 teleportedappend-to 属性,让弹窗挂载到子应用自身的 DOM 容器内,而非 document.body

方案 A:子应用全局配置(推荐)

在子应用的入口文件中配置 Element Plus 的全局属性:

// 子应用 main.ts
import { createApp } from 'vue'
import ElementPlus from 'element-plus'

const app = createApp(App)
app.use(ElementPlus, {
  // 弹窗不使用 teleport,保持在子应用 DOM 树内
  // 这样弹窗样式就在 shadowDOM 内,天然隔离
})

同时在子应用的弹窗组件中统一设置:

<!-- 子应用中使用 Dialog -->
<el-dialog :teleported="false">
  <!-- 内容 -->
</el-dialog>

<!-- 或者指定挂载到子应用容器 -->
<el-dialog append-to=".child-app-root">
  <!-- 内容 -->
</el-dialog>

方案 B:wujie 插件拦截(自动化)

通过 wujie 的插件系统,自动为子应用的弹窗组件注入配置:

// plugins/wujie.ts 中增加插件配置
import { setupApp } from 'wujie'

setupApp({
  name: 'child-app',
  // ... 其他配置
  plugins: [
    {
      // CSS loader:可以在子应用 CSS 加载时做处理
      cssLoader: (code: string) => code,
      // JS loader:可以在子应用 JS 加载时做处理
      jsLoader: (code: string) => code,
    }
  ]
})

方案 C:共享样式包(兜底方案)

如果确实需要主应用和子应用共享某些样式(如统一的弹窗规范),应该通过 @cmclink/ui 包来分发,而非在主应用中硬写覆盖:

packages/ui/src/styles/
├── shared/                    ← 可被子应用独立引入的共享样式
│   ├── dialog-standard.scss   ← 弹窗标准样式
│   └── form-standard.scss     ← 表单标准样式

子应用按需引入:

// 子应用 main.ts
import '@cmclink/ui/styles/shared/dialog-standard.scss'

3.3 样式隔离总结

场景隔离机制是否需要额外处理
子应用普通样式wujie shadowDOM 天然隔离❌ 无需处理
子应用弹窗/抽屉配置 :teleported="false"✅ 子应用侧配置
主应用与子应用共享样式通过 @cmclink/ui 包分发✅ 包级别管理
主应用全局样式不会侵入子应用(shadowDOM)❌ 无需处理

3.4 删除 micro-app.css

重构完成后,assets/styles/micro-app.css 应该被完全删除。其中的内容处理方式:

原内容处理方式
.micro-app .el-dialog 覆盖子应用配置 :teleported="false" 后不再需要
.micro-app-test.marketing测试代码,直接删除
.test-paragraph / .test-box测试代码,直接删除

四、主色统一方案

4.1 现状:三处主色定义

位置主色值说明
styles/var.css#005aae主应用布局变量
@cmclink/ui/variables.scss#004889组件库设计令牌
resizeStyle.scss#005aae(硬编码)登录页等处直接写死

4.2 统一方案

@cmclink/ui 的设计令牌为唯一真相源

  1. 确认最终主色值(与设计团队对齐):

    • 如果设计稿是 #004889 → 主应用的 var.css 需要同步修改
    • 如果设计稿是 #005aae@cmclink/uivariables.scss 需要同步修改
  2. 主应用中所有硬编码的颜色值,改为引用 CSS 变量:

    // ❌ 错误
    background-color: #005aae;
    
    // ✅ 正确
    background-color: var(--el-color-primary);
    
  3. 主应用的 tokens/_css-variables.scss 只定义布局相关的变量,颜色变量全部继承 @cmclink/ui


五、文件迁移映射

5.1 需要删除的文件

文件原因
assets/main.css脚手架残留,Tailwind 迁移到 styles/tailwind.css
assets/base.css脚手架残留,与 @cmclink/ui/base.scss 重复
assets/styles/micro-app.css!important 硬覆盖,改用 wujie 沙箱
styles/theme.scss空文件
styles/variables.scss仅 2 行,合并到 tokens/_variables.scss
styles/global.module.scss价值存疑,如需保留可合并

5.2 需要拆分的文件

styles/var.css(154 行)→ 拆分为:

目标文件内容
tokens/_css-variables.scss布局变量(--left-menu-*--top-header-*--tags-view-*--app-content-* 等)
删除颜色变量(--el-color-primary 等),改为继承 @cmclink/ui

styles/resizeStyle.scss(632 行)→ 拆分为:

目标文件内容行数(约)
overrides/_button.scss按钮样式覆盖(L1-20)~20
overrides/_tag.scss标签样式(L29-33)~5
overrides/_select.scss下拉框样式(L36-38)~3
overrides/_table.scss表格样式(L41-80, L374-417)~80
overrides/_message-box.scss消息框样式(L83-98, L399-406)~20
overrides/_form.scssfilterBox 表单样式(L484-618)~135
layout/_login.scss登录页样式(L100-117)~18
layout/_app-view.scssAppViewContent / AppViewScroll / tabPage(L119-306)~190
legacy/_si-detail.scss出口单证详情(index.scss L50-105)~55
legacy/_marketing.scss营销模块(L308-373, L418-438)~70
删除.container-selected-row(箱管选中行)→ 迁移到对应组件 scoped~8

styles/index.scss(126 行)→ 拆分为:

目标文件内容
vendors/_nprogress.scssNProgress 主题色适配(L22-38)
overrides/_form.scssappend-click-input(L39-49)
legacy/_si-detail.scsssi-detail-dialog / si-detail-tabs(L50-105)
删除.reset-margin.el-popup-parent--hidden.el-scrollbar__bar → 评估是否仍需要

styles/dark.css(85 行)→ 迁移到:

目标文件说明
themes/_dark.scss完整保留暗色主题变量,清理注释掉的废代码

styles/dialog.scss(17 行)→ 迁移到:

目标文件说明
overrides/_dialog.scss弹窗尺寸规范

5.3 @cmclink/ui 需要调整的部分

文件调整内容
styles/index.scss移除 @use 'tailwindcss',Tailwind 只在应用层引入
styles/animations.scss清理与 Tailwind 重复的工具类(.translate-*.scale-*.rotate-*.duration-* 等约 800 行)
styles/utilities.scss保留 cmc-* 前缀的工具类,删除与 Tailwind 重复的部分

六、实施步骤

Phase 1:基础清理(低风险,可立即执行)

预计耗时:1-2 小时

  1. 创建新目录结构

    • 创建 styles/tokens/styles/themes/styles/overrides/styles/layout/styles/vendors/styles/legacy/
  2. 创建 styles/tailwind.css

    @import "tailwindcss";
    
  3. 删除废文件

    • styles/theme.scss(空文件)
  4. 统一主色

    • 与设计团队确认最终主色值
    • 更新所有硬编码颜色为 CSS 变量引用

Phase 2:文件拆分迁移(中风险,需逐步验证)

预计耗时:3-4 小时

  1. 拆分 resizeStyle.scss(632 行 → 8 个文件)
  2. 拆分 var.css(布局变量 → tokens/_css-variables.scss
  3. 迁移 dark.cssthemes/_dark.scss
  4. 迁移 dialog.scssoverrides/_dialog.scss
  5. 拆分 index.scss(NProgress → vendors,业务 → legacy)
  6. 创建新的 styles/index.scss 入口文件

Phase 3:入口重构(高风险,需完整回归测试)

预计耗时:1-2 小时

  1. 修改 main.ts

    • 移除 import './assets/main.css'
    • 移除 import './assets/styles/micro-app.css'
    • 添加 import './styles/index.scss'
    • 调整 @cmclink/ui/styles 的导入顺序
  2. 删除旧文件

    • assets/main.css
    • assets/base.css
    • assets/styles/micro-app.css
    • assets/styles/ 目录
  3. 完整回归测试

    • 登录页样式
    • 主布局(侧边栏、顶部导航、标签页)
    • 表格页面(筛选框、表格、分页)
    • 弹窗/抽屉
    • 暗色主题切换
    • 子应用加载和样式隔离

Phase 4:微前端隔离优化(需子应用配合)

预计耗时:2-3 小时(每个子应用)

  1. 子应用配置弹窗不逃逸

    • 全局配置 :teleported="false"append-to
    • @cmclink/micro-bootstrap 中提供统一配置能力
  2. 删除 micro-app.css

  3. 验证子应用样式隔离

    • 主应用样式不侵入子应用
    • 子应用样式不泄漏到主应用
    • 弹窗/抽屉样式正确

Phase 5:@cmclink/ui 优化(独立进行)

预计耗时:2-3 小时

  1. 移除 Tailwind CSS 引入(从 @cmclink/ui/styles/index.scss
  2. 清理 animations.scss 中与 Tailwind 重复的工具类(约 800 行)
  3. 审查 utilities.scss 中与 Tailwind 重复的部分

七、风险评估与回退方案

7.1 风险点

风险概率影响缓解措施
styles/index.scss 未被引用但样式实际生效先在浏览器 DevTools 确认哪些样式实际生效
拆分后样式加载顺序变化导致覆盖失效严格按照原有顺序组织 @use
子应用弹窗 :teleported="false" 导致层级问题逐个子应用测试,必要时用 z-index 调整
删除 base.css 后某些页面样式异常@cmclink/ui/base.scss 已覆盖所有 reset

7.2 回退方案

每个 Phase 独立提交 Git,如果出现问题可以精确回退到任意阶段:

feat(styles): phase-1 基础清理和目录结构
feat(styles): phase-2 文件拆分迁移
feat(styles): phase-3 入口重构
feat(styles): phase-4 微前端隔离优化
feat(styles): phase-5 @cmclink/ui 优化

八、验证清单

8.1 样式正确性

  • 登录页样式正常(背景、表单、按钮)
  • 主布局样式正常(侧边栏展开/收起、顶部导航、面包屑)
  • 标签页样式正常(激活态、hover、关闭按钮)
  • 表格页面样式正常(筛选框、表头、行、分页)
  • 弹窗/抽屉样式正常(大/中/小尺寸)
  • 表单样式正常(输入框、下拉框、日期选择器)
  • 按钮样式正常(主要、链接、危险、禁用)
  • NProgress 进度条颜色正确
  • 暗色主题切换正常

8.2 微前端隔离

  • 子应用加载后样式正常
  • 主应用样式未侵入子应用
  • 子应用样式未泄漏到主应用
  • 子应用弹窗样式正确(不逃逸到主应用 body)
  • 多个子应用切换时样式无残留

8.3 构建产物

  • 构建无报错
  • CSS 产物体积不增长(预期减少 30%+)
  • 无重复的 Tailwind CSS 输出
  • 无重复的 CSS Reset

8.4 开发体验

  • HMR 样式热更新正常
  • 新增样式时知道该放在哪个文件
  • SCSS 变量和 CSS 变量可正常引用

九、预期收益

维度现状重构后
CSS 产物体积Tailwind ×2 + Reset ×2 + 大量重复减少约 30-40%
样式文件数散落 3 个目录,8+ 个文件1 个目录,清晰分层
入口文件main.ts 导入 2 个样式文件 + 隐式依赖main.ts 导入 1 个入口
新人上手不知道样式该写在哪5 分钟理解结构
微前端隔离!important 硬覆盖wujie 沙箱天然隔离
主色一致性3 处定义,2 个不同值1 处定义,全局统一
!important 使用泛滥仅在必要的 EP 覆盖中使用