阅读视图

发现新文章,点击刷新页面。

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

日期: 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 核心问题清单

# 问题 严重程度 说明
1 Tailwind CSS 重复引入 🔴 高 assets/main.css@cmclink/ui/styles 各引入一次,产生重复样式
2 CSS Reset 重复执行 🔴 高 assets/base.css@cmclink/ui/base.scss 各做一次全局 reset
3 主色定义冲突 🔴 高 var.css#005aae@cmclink/ui#004889resizeStyle.scss→硬编码 #005aae,三处不一致
4 styles/index.scss 未被引用 🔴 高 632 行 EP 覆盖 + 布局变量 + 暗色主题可能完全未生效
5 body 样式双重定义 🟡 中 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.scss filterBox 表单样式(L484-618) ~135
layout/_login.scss 登录页样式(L100-117) ~18
layout/_app-view.scss AppViewContent / 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.scss NProgress 主题色适配(L22-38)
overrides/_form.scss append-click-input(L39-49)
legacy/_si-detail.scss si-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 覆盖中使用

微前端图标治理方案


一、背景与问题

在微前端架构下,主应用长期积累了 5 套图标方案并存 的混乱局面:

# 方案 位置 使用方式 核心问题
1 iconfont JS assets/icon.min.js(115KB) <SvgIcon name="xxx">#icon-xxx 全量加载无 Tree-shaking,iconfont 平台维护成本高
2 本地 SVG assets/svgs/(30 个文件) vite-plugin-svg-icons → SVG Sprite 仅主应用可用,子应用无法共享
3 @purge-icons + Iconify Icon.vue <Icon icon="ep:edit"> 运行时渲染,依赖 @purge-icons/generated
4 IconJson 硬编码 Icon/src/data.ts(1962 行) IconSelect 组件消费 手动维护 EP / FA 图标名列表,极易过时
5 CmcIcon @cmclink/ui <CmcIcon name="xxx"> 已有基础但只支持 SVG Sprite,未与其他方案打通

核心痛点

  • 子应用无法共享主应用图标,每个应用各自维护
  • 同一个图标可能通过 3 种不同方式引用
  • iconfont JS 全量加载 115KB,无法按需
  • 1962 行硬编码图标列表,维护成本极高
  • 中后台系统 90% 以上使用通用图标,不需要每个应用单独管理

二、治理目标

统一入口 + 集中管理 + 零配置共享
  • 一个组件<CmcIcon> 统一消费所有图标
  • 一个图标包@cmclink/icons 集中管理 SVG 资源
  • 零配置:子应用迁入 Monorepo 后自动获得所有共享图标
  • 按需加载:Element Plus 图标异步 import,不影响首屏

三、方案架构

┌──────────────────────────────────────────────────────┐
│                    使用层(所有子应用)                  │
│                                                      │
│  <CmcIcon name="Home" />           — SVG Sprite 图标  │
│  <CmcIcon name="ep:Edit" />        — Element Plus 图标 │
│  <CmcIcon name="Star" size="lg" color="primary" />   │
├──────────────────────────────────────────────────────┤
│               @cmclink/ui — CmcIcon 组件              │
│                                                      │
│  ┌─────────────┐    ┌──────────────────┐             │
│  │ SVG Sprite  │    │ Element Plus     │             │
│  │ <svg><use>  │    │ 动态 import      │             │
│  │ 无前缀      │    │ ep: 前缀         │             │
│  └─────────────┘    └──────────────────┘             │
├──────────────────────────────────────────────────────┤
│            @cmclink/icons — 共享图标资源包              │
│                                                      │
│  packages/icons/src/svg/                             │
│  ├── Home.svg                                        │
│  ├── Star.svg                                        │
│  ├── Logo.svg                                        │
│  └── ... (30+ 通用图标)                               │
├──────────────────────────────────────────────────────┤
│           @cmclink/vite-config — 构建自动集成           │
│                                                      │
│  vite-plugin-svg-icons 自动扫描:                      │
│  1. packages/icons/src/svg/  (共享图标,优先)          │
│  2. apps/{app}/src/assets/svgs/ (本地图标,可覆盖)     │
└──────────────────────────────────────────────────────┘

四、CmcIcon 组件设计

4.1 Props 接口

interface CmcIconProps {
  /**
   * 图标名称
   * - 无前缀: SVG Sprite 图标(如 "Home"、"Star")
   * - "ep:" 前缀: Element Plus 图标(如 "ep:Edit"、"ep:Delete")
   */
  name: string
  /** 尺寸:数字(px) | 预设('xs'|'sm'|'md'|'lg'|'xl') | CSS 字符串 */
  size?: number | string | 'xs' | 'sm' | 'md' | 'lg' | 'xl'
  /** 颜色:CSS 值 | 主题色('primary'|'success'|'warning'|'danger'|'info') */
  color?: string
  /** 旋转角度 */
  rotate?: number
  /** 旋转动画 */
  spin?: boolean
  /** 禁用状态 */
  disabled?: boolean
  /** 可点击 */
  clickable?: boolean
}

4.2 预设尺寸

尺寸 像素 场景
xs 12px 辅助文字旁小图标
sm 14px 表单项内图标
md 16px 默认,正文行内图标
lg 20px 按钮内图标
xl 24px 标题旁图标

4.3 主题色

使用 CSS 变量自动跟随 Element Plus 主题:

const colorMap = {
  primary: 'var(--el-color-primary, #004889)',
  success: 'var(--el-color-success, #10b981)',
  warning: 'var(--el-color-warning, #f59e0b)',
  danger:  'var(--el-color-danger, #ef4444)',
  info:    'var(--el-color-info, #3b82f6)',
}

4.4 Element Plus 图标异步加载

// ep: 前缀触发异步加载,不影响首屏 bundle
watch(
  () => props.name,
  async (name) => {
    if (!name.startsWith('ep:')) return
    const iconName = name.slice(3) // "ep:Edit" → "Edit"
    const icons = await import('@element-plus/icons-vue')
    elIconComponent.value = icons[iconName] ?? null
  },
  { immediate: true }
)

五、Vite 构建集成

5.1 主应用配置(main-app.ts)

createSvgIconsPlugin({
  iconDirs: [
    // 共享图标库(@cmclink/icons)— 所有子应用共享
    resolve(root, '../../packages/icons/src/svg'),
    // 应用本地图标(可覆盖共享图标,或放置业务特有图标)
    resolve(root, 'src/assets/svgs'),
  ],
  symbolId: 'icon-[dir]-[name]',
  svgoOptions: true,
})

5.2 子应用配置(child-app.ts)

// svgIcons 选项默认 true,子应用零配置即可共享图标
export interface ChildAppOptions {
  svgIcons?: boolean  // 默认 true
  // ...
}

关键设计iconDirs 数组中共享图标在前、本地图标在后,本地同名 SVG 可覆盖共享图标,实现灵活的图标定制能力。

六、迁移实施

6.1 迁移映射表

旧用法 新用法 说明
<SvgIcon name="Home" :size="20" /> <CmcIcon name="Home" :size="20" /> 仅改标签名
<Icon icon="ep:edit" /> <CmcIcon name="ep:Edit" /> iconname,PascalCase
<Icon icon="ep:user-filled" /> <CmcIcon name="ep:UserFilled" /> kebab → PascalCase
<Icon icon="fontisto:email" /> <CmcIcon name="ep:Message" /> 替换为 EP 等效图标
<svg><use href="#icon-xxx" /></svg> <CmcIcon name="xxx" /> 直接使用组件

6.2 实施清单

已完成 ✅

步骤 变更 影响文件数
创建 @cmclink/icons 共享图标包 packages/icons/ 新建
迁移 SVG 到共享包 assets/svgs/packages/icons/src/svg/ 30 个 SVG
重写 CmcIcon 组件 支持 SVG Sprite + ep: 前缀 1 个文件
main-app.ts 配置共享图标扫描 iconDirs 新增共享目录 1 个文件
child-app.ts 同步配置 新增 svgIcons 选项 1 个文件
替换 <SvgIcon><CmcIcon> 删除 import + 替换标签 10 个文件
替换 <Icon><CmcIcon> iconname,PascalCase 9 个文件
删除 icon.min.js 移除 iconfont 全量加载 -115KB
删除 Icon/ 目录 Icon.vue + IconSelect.vue + data.ts -1962 行
删除 SvgIcon.vue 旧 SVG 图标组件 1 个文件
清理 setupGlobCom 移除旧 Icon 全局注册 1 个文件
清理 Form.vue <Icon><CmcIcon> (JSX) 1 个文件

6.3 收益量化

指标 治理前 治理后 收益
图标方案数量 5 套 1 套 维护成本降低 80%
首屏资源 +115KB (iconfont JS) 0KB (按需加载) -115KB
硬编码图标列表 1962 行 0 行 消除过时风险
子应用图标配置 每个应用单独维护 零配置 开发效率提升
图标使用入口 3 个组件 1 个组件 心智负担降低

七、使用指南

7.1 SVG Sprite 图标(推荐)

<!-- 基础用法 -->
<CmcIcon name="Home" />

<!-- 预设尺寸 -->
<CmcIcon name="Star" size="lg" />

<!-- 自定义像素 -->
<CmcIcon name="Document" :size="32" />

<!-- 主题色 -->
<CmcIcon name="Warning" color="danger" />

<!-- 旋转动画 -->
<CmcIcon name="Loading" spin />

<!-- 可点击 -->
<CmcIcon name="Close" clickable @click="handleClose" />

7.2 Element Plus 图标

<!-- ep: 前缀,异步加载 -->
<CmcIcon name="ep:Edit" />
<CmcIcon name="ep:Delete" color="danger" />
<CmcIcon name="ep:Search" :size="18" />
<CmcIcon name="ep:Loading" spin />

7.3 添加新图标

  1. 将 SVG 文件放入 packages/icons/src/svg/
  2. 文件名即图标名(如 MyIcon.svg<CmcIcon name="MyIcon" />
  3. 无需任何额外配置,Vite HMR 自动生效
  4. 所有子应用自动可用

7.4 应用级图标覆盖

如果某个子应用需要定制某个图标的样式:

  1. apps/{app}/src/assets/svgs/ 放入同名 SVG
  2. 本地版本自动覆盖共享版本
  3. 其他子应用不受影响

八、目录结构

packages/
├── icons/                          # 共享图标包
│   ├── package.json                # @cmclink/icons
│   ├── README.md                   # 使用文档
│   └── src/
│       ├── index.ts                # 导出图标目录路径常量
│       └── svg/                    # 所有共享 SVG 图标
│           ├── Home.svg
│           ├── Star.svg
│           ├── UnStar.svg
│           ├── Logo.svg
│           ├── TopMenu.svg
│           └── ...
├── ui/
│   └── src/base/CmcIcon/
│       ├── index.ts
│       └── src/CmcIcon.vue         # 统一图标组件
└── vite-config/
    └── src/
        ├── main-app.ts             # iconDirs: [共享, 本地]
        └── child-app.ts            # svgIcons 选项

九、FAQ

Q: IconSelect 组件删除后,图标选择功能怎么办?

A: IconSelect 依赖已删除的 data.ts(1962 行硬编码列表)。如果业务确实需要图标选择器,建议基于 @element-plus/icons-vue 的导出列表动态生成,而非硬编码。后续可在 @cmclink/ui 中实现新版 CmcIconPicker

Q: 子应用还在外部独立仓库,如何使用共享图标?

A: 当前 child-app.tsiconDirs 使用相对路径 ../../packages/icons/src/svg,仅适用于 Monorepo 内的子应用。外部子应用迁入 Monorepo 后自动生效。迁入前可通过 extraPlugins 自行配置 vite-plugin-svg-icons

Q: 第三方图标库(如 Font Awesome)怎么处理?

A: 当前 CmcIcon 支持 SVG Sprite 和 Element Plus 两种源。如需扩展第三方图标库,可在 CmcIcon 中增加新的前缀识别(如 fa: → Font Awesome),通过异步 import 按需加载。但中后台系统建议优先使用 Element Plus 图标,保持设计一致性。

微前端路由设计方案 & 子应用管理保活

版本: v2.0(sync 模式重构)
日期: 2026-02-09
框架: wujie(无界)— alive + sync 模式
适用范围: CMCLink 微前端主应用 + 7 个子应用


一、架构总览

1.1 系统拓扑

┌─────────────────────────────────────────────────────────────────┐
│                    CMCLink 微前端架构                             │
│                                                                 │
│  ┌───────────────────────────────────────────────────────────┐  │
│  │              主应用 @cmclink/main (:3000)                  │  │
│  │  ┌─────────┐  ┌──────────────┐  ┌─────────────────────┐  │  │
│  │  │ Router  │  │ AuthLayout   │  │     App.vue         │  │  │
│  │  │ (Vue)   │  │ (Header/Tab) │  │ (WujieVue 容器)     │  │  │
│  │  └────┬────┘  └──────┬───────┘  └──────────┬──────────┘  │  │
│  │       │              │                      │             │  │
│  │       │     wujie bus (EventEmitter)        │             │  │
│  │       │    ┌─────────┴──────────┐           │             │  │
│  └───────┼────┼────────────────────┼───────────┼─────────────┘  │
│          │    │                    │           │                 │
│  ┌───────┴────┴────────────────────┴───────────┴─────────────┐  │
│  │                    子应用沙箱层 (wujie)                      │  │
│  │                                                           │  │
│  │  ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌──────────────┐   │  │
│  │  │  doc    │ │  mkt    │ │ common  │ │ ibs-manage   │   │  │
│  │  │ :3003   │ │ :3001   │ │ :3006   │ │    :3007     │   │  │
│  │  └─────────┘ └─────────┘ └─────────┘ └──────────────┘   │  │
│  │  ┌─────────┐ ┌─────────┐ ┌─────────┐                    │  │
│  │  │commerce │ │operation│ │ general │                    │  │
│  │  │-finance │ │ :3004   │ │ :3005   │                    │  │
│  │  │ :3002   │ │         │ │         │                    │  │
│  │  └─────────┘ └─────────┘ └─────────┘                    │  │
│  └───────────────────────────────────────────────────────────┘  │
│                                                                 │
│  ┌───────────────────────────────────────────────────────────┐  │
│  │              共享包 @cmclink/micro-bridge                   │  │
│  │  registry.ts │ url.ts │ types.ts │ bridges                │  │
│  └───────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────┘

1.2 子应用注册表

所有子应用在 packages/micro-bridge/src/registry.ts 中统一注册:

子应用名称 端口 activeRule entry 状态
mkt 3001 /mkt /mkt/ 待迁移
commerce-finance 3002 /commerce-finance /commerce-finance/ 待迁移
doc 3003 /doc /doc/ ✅ 已迁移
operation 3004 /operation /operation/ 待迁移
general 3005 /general /general/ 待迁移
common 3006 /common /common/ 待迁移
ibs-manage 3007 /ibs-manage /ibs-manage/ ✅ 已迁移

1.3 Monorepo 项目结构

微前端/
├── apps/
│   ├── cmclink-web-micro-main/   # 主应用 @cmclink/main
│   ├── doc/                       # 子应用 @cmclink/doc
│   └── ibs-manage/                # 子应用 @cmclink/ibs-manage
├── packages/
│   ├── micro-bridge/              # 微前端通信 SDK
│   ├── micro-bootstrap/           # 子应用启动器
│   ├── tsconfig/                  # 共享 TS 配置
│   └── vite-config/               # 共享 Vite 配置
├── pnpm-workspace.yaml
└── turbo.json

二、路由设计方案

2.1 URL 设计原则

核心原则:URL 对用户无感,子应用路径直接拼接在主应用路径后。

主应用 base:  /micro-main/
子应用路由:   /micro-main/{appName}/{子应用内部路径}

示例:
  /micro-main/                                          → 主应用首页
  /micro-main/ibs-manage/operation/enterpriseMgmt       → ibs-manage 子应用
  /micro-main/doc/document/blManage                     → doc 子应用
  /micro-main/profile                                   → 主应用个人中心

2.2 主应用路由配置

主应用 Vue Router 采用 createWebHistory(VITE_BASE_PATH) 模式,路由分为三层:

路由树:
├── /login                          # 公开路由(无需登录)
├── /forget-password                # 公开路由
├── /resetPwd                       # 公开路由
├── /                               # AuthenticatedLayout(需登录)
│   ├── /                           # 主应用首页
│   ├── /profile                    # 个人中心
│   ├── /message-center             # 消息中心
│   ├── /mkt/:pathMatch(.*)*        # 子应用占位路由(render null)
│   ├── /commerce-finance/:pathMatch(.*)*
│   ├── /doc/:pathMatch(.*)*
│   ├── /ibs-manage/:pathMatch(.*)*
│   ├── /common/:pathMatch(.*)*
│   ├── /operation/:pathMatch(.*)*
│   └── /general/:pathMatch(.*)*
└── /:pathMatch(.*)*                # 404 兜底

关键设计点

  • 子应用路由使用 /:pathMatch(.*)* 通配符,确保 /ibs-manage/operation/xxx 等深层路径都能匹配
  • 子应用路由的 component 设为 { render: () => null },不渲染任何主应用组件
  • 子应用路由的 meta.appName 标识所属子应用,用于 Tab 管理
  • 所有子应用路由都是 AuthenticatedLayout 的 children,确保 Header/Tab 始终显示

2.3 主应用显隐控制

App.vueAuthenticatedLayout.vue 协同控制主应用内容与子应用容器的显隐:

App.vue:
├── <RouterView>                    # 始终渲染(AuthenticatedLayout 被 keep-alive 缓存)
│   └── <AuthenticatedLayout>
│       ├── <LayoutHeader>          # 始终显示
│       ├── <SiderMenu>             # 始终显示
│       └── <main-app-content>      # v-show="!findAppByRoute(route.path)"
│           └── <RouterView>        # 主应用页面
│
└── <child-app-container>           # v-if="userStore.isSetUser && isChildRoute"
    └── <WujieVue v-for>            # v-show="route.path.startsWith(app.activeRule)"
        ├── doc
        ├── ibs-manage
        └── ...

显隐判断逻辑

场景 main-app-content child-app-container 说明
/ (首页) ✅ 显示 ❌ 隐藏 findAppByRoute 返回 undefined
/profile ✅ 显示 ❌ 隐藏 主应用页面
/ibs-manage/xxx ❌ 隐藏 ✅ 显示 子应用路由,前缀匹配
/doc/xxx ❌ 隐藏 ✅ 显示 子应用路由

2.4 子应用 URL 生成

开发环境和生产环境使用不同的 URL 生成策略:

// packages/micro-bridge/src/url.ts
function getAppUrl(app: MicroAppConfig, envGetter): string {
  // 环境变量 key: VITE_${APP_NAME}_APP_URL
  // 例如: VITE_IBS_MANAGE_APP_URL=http://localhost:3007
  const envKey = `VITE_${app.name.toUpperCase().replace(/-/g, '_')}_APP_URL`
  const envUrl = envGetter(envKey)

  if (envUrl) {
    // 开发环境: http://localhost:3007 + /ibs-manage/ = http://localhost:3007/ibs-manage/
    return `${envUrl}${app.entry}`
  }
  // 生产环境: 直接使用 entry(相对路径,由 nginx 反向代理)
  return app.entry
}

环境变量配置.env.base):

VITE_MKT_APP_URL=http://localhost:3001
VITE_COMMERCE_FINANCE_APP_URL=http://localhost:3002
VITE_DOC_APP_URL=http://localhost:3003
VITE_OPERATION_APP_URL=http://localhost:3004
VITE_GENERAL_APP_URL=http://localhost:3005
VITE_COMMON_APP_URL=http://localhost:3006
VITE_IBS_MANAGE_APP_URL=http://localhost:3007

三、路由同步机制

3.1 wujie sync 模式

采用 wujie 官方推荐的 sync 模式,由框架自动完成主应用与子应用之间的路由同步,无需手动 bus 通信、无需防循环标记

<!-- App.vue -->
<WujieVue
  :name="app.name"
  :url="getAppUrl(app)"
  :alive="true"
  :sync="true"    ← 启用 sync 模式
/>

sync 模式原理

  • wujie 内部劫持子应用的 history.pushState / history.replaceState
  • 子应用路由变化时,wujie 自动将子应用路径同步到主应用 URL
  • 主应用 URL 变化时,wujie 自动驱动子应用路由跳转
  • 整个过程由框架内部处理,无循环风险

3.2 数据流设计

设计原则:路由同步完全委托给 wujie sync 模式,业务层只负责 router.push

┌─────────────────────────────────────────────────────────────┐
│                     路由同步数据流                             │
│                                                             │
│  场景 A: 菜单/Tab 点击                                       │
│  ──────────────────────                                     │
│  menuClick / tabClick                                       │
│    └── router.push('/ibs-manage/operation/xxx')             │
│         │                                                   │
│         ▼                                                   │
│  主应用 URL 变化 → wujie sync 自动驱动子应用路由跳转           │
│    └── 子应用 router 自动跳转到 /operation/xxx               │
│                                                             │
│  ✅ 无需 bus 通信,无循环风险                                  │
│                                                             │
│                                                             │
│  场景 B: 子应用内部导航                                       │
│  ──────────────────────                                     │
│  子应用内部点击链接 → router.push('/operation/yyy')           │
│         │                                                   │
│         ▼                                                   │
│  wujie sync 自动同步到主应用 URL                              │
│    └── 主应用地址栏更新为 /ibs-manage/operation/yyy           │
│                                                             │
│  ✅ 无需 bus 通信,无循环风险                                  │
└─────────────────────────────────────────────────────────────┘

3.3 与手动方案对比

维度 sync 模式(当前方案) 手动 bus 双向通信(旧方案)
路由同步 框架自动处理 手动 bus.$emit + bus.$on
防循环 框架内部处理,无风险 _fromMainApp 标记,存在竞态风险
代码量 零额外代码 setupRouterSync + updateQuery + afterEach
子应用改造 无需任何路由同步代码 每个子应用需实现 setupRouterSync
可维护性 高(依赖框架标准能力) 低(自定义逻辑,排查困难)

3.4 路径规范化

menuClick 中对路径进行规范化,兼容不同来源的路径格式:

// tabs.ts → menuClick
const prefix = `/${tab.appName}`;
let fullPath = tab.path || prefix;
// TO_ROUTE 传来的可能是子应用内部路径(如 /operation/xxx),需要补前缀
if (!fullPath.startsWith(prefix)) {
  fullPath = `${prefix}${fullPath.startsWith("/") ? "" : "/"}${fullPath}`;
}
// wujie sync 模式会自动同步子应用路由,只需 push 即可
router.push(fullPath);

四、通信事件协议

4.1 事件总线

使用 wujie 内置的 bus(基于 EventEmitter),所有子应用共享同一个 bus 实例。

4.2 事件清单

注意:路由同步已由 wujie sync 模式自动处理,bus 事件仅用于业务通信

子应用 → 主应用

事件名 触发时机 数据结构 处理方
TO_ROUTE 子应用请求跨应用跳转 { appName, path, query, name } AuthenticatedLayoutmenuClick
ASSETS_404 子应用静态资源加载失败 { appName } AuthenticatedLayout → 弹窗提示刷新
CLOSE_ALL_TABS 子应用请求关闭所有 Tab { appName } AuthenticatedLayoutremoveTab

主应用 → 子应用

事件名 触发时机 数据结构 处理方
CLOSE_ALL_TAB_TO_CHILD 关闭子应用 Tab { appName } 子应用监听 → 重置状态
REFRESH_CHILD 刷新子应用 { appName } 子应用监听 → 重新加载当前路由

已废弃事件(由 sync 模式替代)

事件名 废弃原因
ROUTE_CHANGE 子→主路由同步已由 sync 模式自动处理
ROUTER_CHANGE_TO_CHILD 主→子路由同步已由 sync 模式自动处理

4.3 事件使用原则

  • 路由同步:完全依赖 wujie sync 模式,禁止通过 bus 手动同步路由
  • 业务通信:跨应用跳转(TO_ROUTE)、资源异常(ASSETS_404)等业务场景仍使用 bus
  • 事件过滤:子应用通过 data.appName 过滤非自身事件

五、子应用管理与保活方案

5.1 wujie alive 模式

所有子应用均使用 alive 保活模式

<!-- App.vue -->
<WujieVue
  v-for="app in loadedApps"
  :key="app.name"
  :alive="true"           ← 保活模式
  v-show="route.path.startsWith(app.activeRule)"
  :name="app.name"
  :url="getAppUrl(app)"
  :props="{ token, userInfo }"
/>

alive 模式特性

  • 子应用首次加载后,实例不销毁,切换时仅做 display: none
  • 子应用的 Vue 实例、Pinia Store、DOM 状态全部保留
  • 切换回来时无需重新初始化,体验接近原生 Tab 切换
  • 子应用内部的表单填写、滚动位置、弹窗状态等全部保留

5.2 按需渲染策略

为避免未启动的子应用触发加载错误,采用按需渲染策略:

// App.vue
const visitedApps = reactive(new Set<string>())

// 仅渲染用户已访问过的子应用
const loadedApps = computed(() =>
  microAppRegistry.filter((app) => visitedApps.has(app.name))
)

// 监听路由变化,标记已访问
watch(() => route.path, (path) => {
  const matched = findAppByRoute(path)
  if (matched) {
    visitedApps.add(matched.name)
  }
}, { immediate: true })

生命周期

用户首次访问 /ibs-manage/xxx
  → visitedApps.add('ibs-manage')
  → loadedApps 包含 ibs-manage
  → WujieVue 组件渲染 → 加载子应用 → 挂载
  → v-show=true(当前激活)

用户切换到 /doc/xxx
  → visitedApps.add('doc')
  → ibs-manage: v-show=false(隐藏但保活)
  → doc: WujieVue 渲染 → 加载 → v-show=true

用户切回 /ibs-manage/yyy
  → ibs-manage: v-show=true(瞬间恢复,无需重新加载)
  → doc: v-show=false(隐藏但保活)

5.3 预加载策略

AuthenticatedLayoutonMounted 中触发预加载,分优先级:

// plugins/wujie.ts
export function preloadChildApps() {
  const highPriority = ['doc', 'mkt']  // 高频子应用

  // 高优先级:立即预加载
  highPriorityApps.forEach(app => {
    preloadApp({ name: app.name, url: getAppUrl(app) })
  })

  // 低优先级:延迟 3 秒后预加载
  setTimeout(() => {
    lowPriorityApps.forEach(app => {
      preloadApp({ name: app.name, url: getAppUrl(app) })
    })
  }, 3000)
}

预加载 vs 按需渲染的区别

维度 预加载 (preloadApp) 按需渲染 (WujieVue)
时机 登录成功后立即 用户首次访问时
作用 提前下载子应用静态资源 创建子应用实例并挂载
资源 仅网络请求 网络 + DOM + JS 执行
目的 减少首次打开延迟 实际渲染子应用

5.4 子应用容器布局

// App.vue
.child-app-container {
  width: 100%;
  height: calc(100vh - 66px);  // 减去 Header 高度
  overflow: hidden;
}

// AuthenticatedLayout.vue
.authenticated-layout {
  display: flex;
  flex-direction: column;
  height: 100vh;
}
.custom-tabs-content {
  flex: 1;
  height: calc(100vh - 66px);
  overflow: hidden;
  position: relative;
}

5.5 子应用 Props 传递

主应用通过 WujieVue 的 :props 向子应用传递共享数据:

<WujieVue
  :props="{ token: userStore.token, userInfo: userStore.userInfo }"
/>

子应用通过 window.__WUJIE.props 读取:

// 子应用 wujie-bridge.ts
export function getWujieProps(): Record<string, any> {
  return (window as any).__WUJIE?.props || {}
}

六、子应用接入规范

6.1 子应用改造清单

每个子应用需要完成以下改造才能接入微前端:

步骤 文件 改动内容
1 vite.config.ts 配置 base: VITE_BASE_PATHserver.headers 添加 CORS
2 .env.dev 设置 VITE_DEV_PORTVITE_BASE_PATHVITE_APP_NAME
3 src/utils/wujie-bridge.ts 新建通信桥接器(环境检测、bus 通信、资源 404 检测)
4 src/main.ts 调用 errorCheck()
5 src/App.vue 移除旧的 iframe postMessage 监听

注意:路由同步由 wujie sync 模式自动处理,子应用无需编写任何路由同步代码。

6.2 子应用 wujie-bridge.ts 标准模板

// 核心导出
export { isInWujie, isInIframe }       // 环境检测
export { notifyMainApp }               // 向主应用发事件
export { onMainAppEvent, offMainAppEvent } // 监听主应用事件
export { errorCheck }                  // 资源 404 检测
export { MESSAGE_TYPE }                // 事件类型常量(TO_ROUTE, ASSETS_404, CLOSE_ALL_TABS)

6.3 主应用注册新子应用

  1. registry.ts 添加子应用配置(childPathList 自动从 registry 派生,无需手动维护)
  2. router/index.ts 添加占位路由 /{appName}/:pathMatch(.*)*
  3. .env.base 添加 VITE_{APP_NAME}_APP_URL
  4. tabs.ts appList 添加 Tab 配置

七、生产环境部署

7.1 Nginx 配置要点

# 主应用
location /micro-main/ {
  try_files $uri $uri/ /micro-main/index.html;
}

# 子应用(以 ibs-manage 为例)
location /ibs-manage/ {
  proxy_pass http://ibs-manage-server/;
  # 或静态文件
  # alias /path/to/ibs-manage/dist/;
  # try_files $uri $uri/ /ibs-manage/index.html;
}

7.2 URL 生成策略

开发环境:
  主应用: http://localhost:3000/micro-main/
  子应用: http://localhost:3007/ibs-manage/  (由环境变量 VITE_IBS_MANAGE_APP_URL 提供)
  WujieVue url = http://localhost:3007/ibs-manage/

生产环境:
  主应用: https://domain.com/micro-main/
  子应用: https://domain.com/ibs-manage/  (由 nginx 反向代理)
  WujieVue url = /ibs-manage/  (相对路径)

八、已知限制与后续规划

8.1 当前限制

限制 说明 影响
子应用使用 WebHistory 子应用 router 使用 createWebHistory(BASE_URL),在 wujie 沙箱中 location 被代理 子应用独立运行和微前端运行行为一致
菜单路径依赖后端 module 字段 buildFullPath 根据 menu.module 拼接 /${appName} 前缀 后端菜单配置需正确设置 module
预加载依赖子应用 dev server 开发环境下子应用未启动时预加载会静默失败 不影响功能,仅影响首次加载速度

8.2 后续规划

阶段 内容 优先级
Phase 3.1 剩余 5 个子应用迁移到 monorepo
Phase 3.2 子应用间直接通信(不经过主应用中转)
Phase 3.3 子应用独立部署 + 版本管理

附录 A:完整数据流时序图

A.1 菜单点击 → 子应用渲染(sync 模式)

用户          SiderMenu      tabs.ts       Vue Router     App.vue        wujie(sync)    子应用
 │               │              │              │             │              │             │
 │──点击菜单──→  │              │              │             │              │             │
 │               │──menuClick──→│              │             │              │             │
 │               │              │──push────────→│             │              │             │
 │               │              │              │──路由变化───→│              │             │
 │               │              │              │             │──标记已访问   │             │
 │               │              │              │             │  (visitedApps)│             │
 │               │              │              │             │──v-show=true  │             │
 │               │              │              │             │              │             │
 │               │              │              │  URL 变化 → wujie sync 自动同步          │
 │               │              │              │             │──────────────→│──replace───→│
 │               │              │              │             │              │             │──渲染页面
 │               │              │              │             │              │             │
 ✅ 无需 bus 通信,无防循环标记,wujie 框架自动处理

A.2 子应用内部导航 → 主应用 URL 同步(sync 模式)

用户          子应用         wujie(sync)    Vue Router     App.vue
 │              │              │              │             │
 │──点击链接──→ │              │              │             │
 │              │──push────────│              │             │
 │              │              │              │             │
 │              │  路由变化 → wujie sync 自动同步到主应用 URL │
 │              │──────────────→│──replace────→│             │
 │              │              │              │──路由变化───→│
 │              │              │              │             │──仅标记已访问
 │              │              │              │             │
 ✅ 无需 bus 通信,地址栏自动更新为 /ibs-manage/operation/yyy

A.3 跨应用跳转(TO_ROUTE 事件)

用户          子应用A        wujie bus     AuthLayout     tabs.ts       Vue Router    wujie(sync)   子应用B
 │              │              │             │              │             │              │             │
 │──操作────→   │              │             │              │             │              │             │
 │              │──emit────────→│             │              │             │              │             │
 │              │  TO_ROUTE     │──收到───────→│              │             │              │             │
 │              │              │             │──menuClick──→│             │              │             │
 │              │              │             │              │──push───────→│              │             │
 │              │              │             │              │             │  URL 变化 → sync 自动同步   │
 │              │              │             │              │             │──────────────→│──replace───→│
 │              │              │             │              │             │              │             │──渲染

前端架构治理演进规划

一、背景与目标

1.1 现状分析

经过 Phase 1~4 的微前端治理(详见 doc/wujie集成.md),已完成:

已完成项 成果
微前端框架 iframe → wujie,7 个子应用统一接入
通信协议 postMessage → wujie bus,类型化事件
子应用预加载 preloadChildApps 高/低优先级分级
CSS 统一 UnoCSS → Tailwind CSS 4
Monorepo pnpm workspace + Turborepo

但微前端只是架构治理的第一步。 当前 7 个子应用之间存在大量重复建设:

重复领域 现状 影响
UI 组件 每个子应用独立封装 Table/Form/Dialog 7 份重复代码,风格不统一
业务组件 客户选择器、产品选择器等各自实现 逻辑不一致,Bug 修一处漏六处
Utils 工具函数 日期格式化、金额计算、权限判断各写一套 维护成本 ×7
Hooks/Composables useTable、useForm、useDict 各子应用独立 无法共享最佳实践
API 层 接口定义、拦截器、错误处理各自维护 后端改一个字段,前端改 7 处
类型定义 业务实体 TS 类型各子应用独立定义 类型不同步,联调困难

1.2 治理目标

                    ┌─────────────────────────────────┐
                    │        业务应用层(7 个子应用)     │
                    │   mkt / doc / ibs-manage / ...   │
                    └──────────────┬──────────────────┘
                                   │ 消费
                    ┌──────────────┴──────────────────┐
                    │        公共资源层(packages)       │
                    │                                   │
                    │  ┌───────────┐  ┌─────────────┐  │
                    │  │ UI 组件库  │  │ 业务组件库   │  │
                    │  │@cmclink/ui│  │@cmclink/biz  │  │
                    │  └───────────┘  └─────────────┘  │
                    │  ┌───────────┐  ┌─────────────┐  │
                    │  │ Hooks 库  │  │  Utils 库    │  │
                    │  │@cmclink/  │  │@cmclink/     │  │
                    │  │ hooks     │  │ utils        │  │
                    │  └───────────┘  └─────────────┘  │
                    │  ┌───────────┐  ┌─────────────┐  │
                    │  │ API SDK   │  │  类型定义    │  │
                    │  │@cmclink/  │  │@cmclink/     │  │
                    │  │ api       │  │ types        │  │
                    │  └───────────┘  └─────────────┘  │
                    └──────────────┬──────────────────┘
                                   │ 支撑
                    ┌──────────────┴──────────────────┐
                    │        基础设施层                  │
                    │  micro-bridge / vite-config /     │
                    │  tsconfig / eslint-config         │
                    └──────────────────────────────────┘

核心原则

  1. 资源化 — 可复用的代码提取为独立 package
  2. 公共化 — 跨子应用共享,单点维护
  3. 文档化 — 每个公共包配套使用文档和示例
  4. AI 友好 — 沉淀为 AI Agent 可消费的 Skills/MCP 资源

二、公共资源沉淀规划

2.1 @cmclink/ui — 基础 UI 组件库

定位:基于 Element Plus 二次封装的业务通用 UI 组件。

组件 说明 来源
CmcTable 统一表格(分页、排序、列配置、导出) 各子应用 useTable + 模板代码
CmcForm 统一表单(校验、布局、动态字段) 各子应用表单封装
CmcDialog 统一弹窗(确认、表单弹窗、详情弹窗) 各子应用 Dialog 封装
CmcSearch 搜索栏(条件组合、折叠展开、快捷搜索) 各子应用搜索区域
CmcUpload 文件上传(拖拽、预览、进度、断点续传) 各子应用上传组件
CmcEditor 富文本编辑器(统一配置) 各子应用编辑器封装
CmcDescription 详情描述列表 各子应用详情页

实施策略

packages/
└── ui/
    ├── package.json          # @cmclink/ui
    ├── src/
    │   ├── components/       # 组件源码
    │   │   ├── CmcTable/
    │   │   ├── CmcForm/
    │   │   └── ...
    │   ├── composables/      # 组件内部 hooks
    │   └── index.ts          # 统一导出
    └── docs/                 # 组件文档(可选 VitePress)

2.2 @cmclink/biz — 业务组件库

定位:与业务强相关的可复用组件,跨产品线共享。

组件 说明 使用方
CustomerSelector 客户选择器(搜索、分页、多选) mkt / doc / commerce-finance
ProductSelector 产品选择器 mkt / operation
PortSelector 港口选择器 doc / operation
VesselSelector 船名航次选择器 doc / operation
DictSelect 字典下拉(统一字典管理) 全部子应用
UserSelector 用户/员工选择器 全部子应用
ApprovalFlow 审批流程组件 多个子应用

2.3 @cmclink/hooks — 通用 Composables

定位:跨子应用复用的 Vue 3 组合式函数。

Hook 说明 当前状态
useTable 表格数据管理(分页、排序、筛选、刷新) 各子应用独立实现
useForm 表单状态管理(校验、提交、重置) 各子应用独立实现
useDict 字典数据获取与缓存 各子应用独立实现
usePermission 权限判断(按钮级、菜单级) 各子应用独立实现
useExport 数据导出(Excel/CSV/PDF) 各子应用独立实现
useWebSocket WebSocket 连接管理 部分子应用实现
useI18n 国际化增强(业务术语统一翻译) 各子应用独立实现
useCrud CRUD 操作封装(增删改查一体) 各子应用独立实现

2.4 @cmclink/utils — 工具函数库

定位:纯函数工具集,零依赖或仅依赖 lodash-es

模块 函数示例 说明
date formatDate, diffDays, toUTC 日期处理(统一格式)
money formatMoney, toFixed, currencyConvert 金额计算(精度安全)
validator isPhone, isEmail, isTaxNo 业务校验规则
formatter formatFileSize, formatDuration 格式化工具
tree flatToTree, treeToFlat, findNode 树结构操作
auth getToken, setToken, removeToken 认证工具
storage getCache, setCache, removeCache 存储封装

2.5 @cmclink/api — API SDK

定位:统一的后端接口定义层,前后端类型对齐。

// packages/api/src/modules/customer.ts
import type { Customer, CustomerQuery } from '@cmclink/types'
import { request } from '../request'

/** 客户列表 */
export const getCustomerList = (params: CustomerQuery) =>
  request.get<PageResult<Customer>>('/admin-api/customer/page', { params })

/** 客户详情 */
export const getCustomerDetail = (id: number) =>
  request.get<Customer>(`/admin-api/customer/get?id=${id}`)

价值

  • 后端改接口 → 只改 @cmclink/api 一处 → 所有子应用自动同步
  • TypeScript 类型约束 → 编译期发现接口不匹配
  • 可自动生成 → 结合 Swagger/OpenAPI 自动生成 SDK

2.6 @cmclink/types — 共享类型定义

定位:业务实体的 TypeScript 类型定义,前后端对齐。

// packages/types/src/customer.ts
export interface Customer {
  id: number
  name: string
  code: string
  contactPerson: string
  phone: string
  email: string
  status: CustomerStatus
  createdAt: string
}

export type CustomerStatus = 'active' | 'inactive' | 'pending'

export interface CustomerQuery {
  name?: string
  code?: string
  status?: CustomerStatus
  pageNo: number
  pageSize: number
}

三、前后端职能对齐

3.1 基础架构团队职责矩阵

职责领域 前端基础架构 后端基础架构 协同点
框架治理 微前端(wujie)、Monorepo 微服务、网关 子应用 ↔ 微服务 1:1 映射
通信协议 wujie bus 事件定义 API 接口规范 事件名 / 接口路径统一命名
类型系统 @cmclink/types Swagger/OpenAPI 自动生成 TS 类型
API 层 @cmclink/api SDK RESTful API 实现 SDK 自动生成
权限体系 前端按钮/菜单权限 后端接口权限 权限码统一定义
国际化 前端翻译资源 后端错误码翻译 翻译 Key 统一管理
监控告警 前端性能/错误上报 后端 APM 全链路 TraceID 打通
CI/CD 前端构建部署 后端构建部署 统一流水线、环境管理

3.2 前后端类型自动同步方案

后端 Swagger/OpenAPI 定义
         │
         ▼
    openapi-typescript / swagger-typescript-api
         │
         ▼
  @cmclink/types(自动生成 TS 类型)
         │
         ▼
  @cmclink/api(自动生成 API SDK)
         │
         ▼
    各子应用直接消费

工具选型

  • openapi-typescript:从 OpenAPI 3.0 生成 TypeScript 类型
  • swagger-typescript-api:从 Swagger 生成完整的 API Client

四、AI 编程能力沉淀

4.1 为什么基础架构要考虑 AI

AI 编程(Copilot、Cursor、Windsurf 等)已成为开发者日常工具。公共资源的质量直接决定 AI 生成代码的质量

AI 编程痛点 根因 基础架构解法
AI 生成的代码风格不统一 缺乏项目级规范上下文 .windsurf/rules/ 规范文件
AI 不了解业务组件 API 组件文档缺失或分散 组件库 + JSDoc + 示例
AI 重复造轮子 不知道已有公共函数 @cmclink/utils + @cmclink/hooks
AI 生成的接口调用不对 不了解后端 API 结构 @cmclink/api 类型化 SDK
AI 无法理解项目架构 架构文档不完善 架构决策记录(ADR)

4.2 Agent Skills 沉淀

将项目规范和最佳实践沉淀为 AI Agent 可消费的 Skills:

.windsurf/
├── rules/                    # 已有:27 个专项规范
│   ├── core.mdc
│   ├── vue3-component-standards.mdc
│   ├── typescript-standards.mdc
│   └── ...
├── workflows/                # 工作流定义
│   ├── create-component.md   # 新建组件工作流
│   ├── create-api-module.md  # 新建 API 模块工作流
│   ├── create-page.md        # 新建页面工作流
│   └── migrate-child-app.md  # 子应用迁入工作流
└── skills/                   # AI Skills 定义(规划中)
    ├── cmclink-ui.md         # UI 组件库使用指南
    ├── cmclink-api.md        # API SDK 使用指南
    └── cmclink-patterns.md   # 业务模式最佳实践

Skills 示例 — 新建 CRUD 页面

---
description: 创建标准 CRUD 页面(列表 + 新增 + 编辑 + 删除)
---

1.`@cmclink/types` 中定义实体类型
2.`@cmclink/api` 中定义接口
3. 使用 `CmcTable` + `CmcSearch` + `CmcForm` 组合
4. 使用 `useCrud` hook 管理状态
5. 使用 `usePermission` 控制按钮权限

4.3 MCP Server 能力规划

MCP(Model Context Protocol) 让 AI Agent 能够直接访问项目资源:

MCP 能力 说明 价值
组件文档查询 AI 查询 @cmclink/ui 组件 Props/Slots/Events 生成代码直接使用正确的组件 API
API 接口查询 AI 查询后端接口定义和参数 生成的接口调用代码类型正确
字典数据查询 AI 查询业务字典(状态码、类型码) 生成代码使用正确的枚举值
权限码查询 AI 查询按钮/菜单权限码 生成的权限判断代码准确
代码模板生成 AI 基于模板生成标准化页面 新页面开发效率 ×3

MCP Server 架构

┌──────────────────────────────────────────────┐
│              AI Agent (Windsurf/Cursor)        │
│                                                │
│  "帮我创建一个客户管理的 CRUD 页面"              │
└──────────────────┬─────────────────────────────┘
                   │ MCP Protocol
┌──────────────────┴─────────────────────────────┐
│            @cmclink/mcp-server                  │
│                                                 │
│  ┌─────────────┐  ┌──────────────────────────┐ │
│  │ 组件文档资源 │  │ API 接口资源              │ │
│  │ (Resources) │  │ (Resources)              │ │
│  └─────────────┘  └──────────────────────────┘ │
│  ┌─────────────┐  ┌──────────────────────────┐ │
│  │ 代码生成工具 │  │ 字典/权限查询工具         │ │
│  │ (Tools)     │  │ (Tools)                  │ │
│  └─────────────┘  └──────────────────────────┘ │
└─────────────────────────────────────────────────┘

五、实施路线图

5.1 短期(1~2 个月)— 基础沉淀

优先级 任务 产出 负责
P0 提取 @cmclink/utils 工具函数包 基础架构
P0 提取 @cmclink/hooks 通用 Composables 包 基础架构
P0 提取 @cmclink/types 共享类型定义包 基础架构 + 后端
P1 完善 .windsurf/rules/ AI 规范文件 基础架构
P1 创建 .windsurf/workflows/ 标准工作流 基础架构

5.2 中期(3~4 个月)— 组件化

优先级 任务 产出 负责
P0 搭建 @cmclink/ui 组件库 CmcTable / CmcForm / CmcSearch 基础架构
P0 搭建 @cmclink/api SDK 统一 API 调用层 基础架构 + 后端
P1 搭建 @cmclink/biz 业务组件库 客户选择器等业务组件 基础架构 + 业务
P1 组件文档站(VitePress) 在线文档 + 示例 基础架构
P2 OpenAPI → TypeScript 自动生成 类型自动同步流水线 基础架构 + 后端

5.3 长期(5~6 个月)— AI 赋能

优先级 任务 产出 负责
P1 @cmclink/mcp-server AI Agent 资源服务 基础架构
P1 AI Skills 沉淀 组件/API/模式使用指南 基础架构
P2 代码模板生成器 标准化页面脚手架 基础架构
P2 全链路 TraceID 打通 前后端监控联动 基础架构 + 后端

六、预期收益

6.1 效率提升

场景 当前耗时 治理后耗时 提升
新建 CRUD 页面 4~8 小时 1~2 小时 4x
修复跨子应用 Bug 改 7 处 改 1 处 7x
新子应用接入 2~3 天 半天 5x
后端接口变更适配 改 7 个子应用 改 1 个 SDK 7x
AI 生成代码可用率 ~30% ~80% 2.7x

6.2 质量提升

  • 一致性:所有子应用使用相同的组件和交互模式
  • 可维护性:公共代码单点维护,变更自动传播
  • 类型安全:前后端类型自动同步,编译期发现问题
  • AI 友好:规范化的代码库让 AI 生成更准确的代码

6.3 团队赋能

  • 新人上手:标准化组件 + 文档 + AI Skills → 快速产出
  • 跨团队协作:公共组件库是团队间的共同语言
  • 技术影响力:沉淀的基础设施可对外输出

七、风险与缓解

风险 影响 缓解措施
公共包变更影响所有子应用 回归范围大 Changesets 版本管理 + 自动化测试
业务组件抽象不当 过度抽象或不够通用 先在 2 个子应用验证,再推广
AI Skills 维护成本 文档过时 与代码同仓库,CI 检查文档同步
团队推广阻力 业务团队不愿迁移 渐进式迁移,新页面优先使用

附录:packages 目录规划

packages/
├── micro-bridge/       # ✅ 已有 — 微前端通信 SDK
├── micro-bootstrap/    # ✅ 已有 — 子应用启动器
├── vite-config/        # ✅ 已有 — 统一 Vite 配置
├── tsconfig/           # ✅ 已有 — 统一 TS 配置
├── ui/                 # 📋 规划 — 基础 UI 组件库
├── biz/                # 📋 规划 — 业务组件库
├── hooks/              # 📋 规划 — 通用 Composables
├── utils/              # 📋 规划 — 工具函数库
├── api/                # 📋 规划 — API SDK
├── types/              # 📋 规划 — 共享类型定义
├── eslint-config/      # 📋 规划 — 统一 ESLint 配置
└── mcp-server/         # 📋 规划 — AI Agent MCP 服务

微前端 — wujie(无界)集成设计文档

一、背景与选型

1.1 现状问题

主应用 cmclink-web-micro-main 采用 iframe 过渡方案 加载 7 个子应用,存在以下痛点:

问题 影响
7 个 <iframe> 硬编码在 App.vue 新增子应用需改 App.vue + router + tabs.ts 三处
每个 iframe 独立加载完整 Vue + Element Plus 内存和带宽 ×7,首屏慢
postMessage 通信无类型约束 调试困难,事件名拼写错误无感知
弹窗无法突破 iframe 边界 Element Plus 的 Dialog/MessageBox 被裁切
reloadIframe() 暴力刷新 子应用状态全部丢失
URL 不同步 刷新页面后子应用路由丢失

1.2 方案对比

维度 qiankun micro-app wujie iframe(当前)
Vue 3 + Vite 兼容 ⚠️ 需插件
JS 沙箱强度 Proxy(有逃逸风险) iframe 沙箱 iframe 沙箱(最强) 天然隔离
CSS 隔离 动态样式表 样式隔离 iframe 级别 天然隔离
keep-alive ❌ 不支持 原生 alive 模式
弹窗突破容器
子应用改造量 大(导出生命周期) 最小
从 iframe 迁移成本 最低
维护活跃度 ⚠️ 停滞 ✅ 京东 ✅ 腾讯

1.3 选型结论

选择 wujie(无界),核心理由:

  1. 从 iframe 迁移成本最低<iframe> 标签 1:1 替换为 <WujieVue>
  2. 隔离性最强 — iframe 沙箱是浏览器原生级别,零逃逸风险
  3. 原生 alive 模式 — 子应用切换时状态完整保留(当前 7 个 iframe 全挂载就是为了保活)
  4. 子应用几乎零改造 — 不要求导出 bootstrap/mount/unmount 生命周期函数

二、整体架构

2.1 系统架构图

┌─────────────────────────────────────────────────────────────┐
│                    主应用 @cmclink/main                      │
│                  (cmclink-web-micro-main)                     │
│                                                              │
│  ┌──────────────┐  ┌──────────────────────────────────────┐ │
│  │ LayoutHeader │  │         wujie bus (事件总线)           │ │
│  └──────────────┘  └──────────┬───────────────────────────┘ │
│  ┌──────────────┐             │                              │
│  │  SiderMenu   │             │ $on / $emit                  │
│  └──────────────┘             │                              │
│  ┌────────────────────────────┼──────────────────────────┐  │
│  │              App.vue 子应用容器                         │  │
│  │                            │                           │  │
│  │  ┌─────────┐ ┌─────────┐ ┌┴────────┐ ┌─────────┐    │  │
│  │  │WujieVue │ │WujieVue │ │WujieVue │ │  ...    │    │  │
│  │  │  mkt    │ │  doc    │ │commerce │ │ (x7)    │    │  │
│  │  │:alive   │ │:alive   │ │-finance │ │         │    │  │
│  │  └─────────┘ └─────────┘ └─────────┘ └─────────┘    │  │
│  └───────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────┘

2.2 子应用列表

子应用 name 路由前缀 开发端口 说明
营销 mkt /mkt 3001 营销产品线
业财 commerce-finance /commerce-finance 3002 业财产品线
单证 doc /doc 3003 单证产品线
操作线 operation /operation 3004 操作产品线
通用 general /general 3005 公共产品线
公共 common /common 3006 基础数据
运营后台 ibs-manage /ibs-manage 3007 运营管理

子应用注册表统一维护在 packages/micro-bridge/src/registry.ts

2.3 Monorepo 目录结构

微前端/
├── apps/
│   ├── cmclink-web-micro-main/    # @cmclink/main — 真正的主应用(基座)
│   ├── doc/                        # @cmclink/doc — 单证子应用
│   ├── ibs-manage/                 # @cmclink/ibs-manage — 运营后台子应用
│   └── main/                       # ⚠️ 旧主应用(待手动删除,见第八章)
├── packages/
│   ├── micro-bridge/               # @cmclink/micro-bridge — 通信 SDK + 注册表
│   ├── micro-bootstrap/            # @cmclink/micro-bootstrap — 子应用启动器
│   ├── vite-config/                # @cmclink/vite-config — 统一 Vite 配置
│   └── tsconfig/                   # @cmclink/tsconfig — 统一 TS 配置
├── pnpm-workspace.yaml
├── turbo.json
└── package.json

三、主应用集成详解

3.1 依赖安装

// apps/cmclink-web-micro-main/package.json
{
  "dependencies": {
    "wujie-vue3": "^1.0.22",
    "@cmclink/micro-bridge": "workspace:*"
  }
}

3.2 插件注册

// src/plugins/wujie.ts
import WujieVue from 'wujie-vue3'
import type { App } from 'vue'

export function setupWujie(app: App) {
  app.use(WujieVue)
}
// src/main.ts(关键行)
import { setupWujie } from "@/plugins/wujie"
// ...
setupWujie(app)
app.mount("#app")

3.3 App.vue — 子应用容器

改造前(7 个硬编码 iframe):

<iframe :src="'/mkt/?_t=' + now" name="mkt" v-show="route.path === '/mkt'" />
<iframe :src="'/doc/?_t=' + now" name="doc" v-show="route.path === '/doc'" />
<!-- ... 重复 7 次 -->

改造后(基于注册表动态渲染):

<WujieVue
  v-for="app in microAppRegistry"
  :key="app.name"
  v-show="route.path === app.activeRule"
  :name="app.name"
  :url="getAppUrl(app)"
  :alive="true"
  :props="{ token: userStore.token, userInfo: userStore.userInfo }"
  width="100%"
  height="100%"
/>

关键属性说明

属性 说明
:name app.name 子应用唯一标识,wujie 内部用于实例管理
:url getAppUrl(app) 子应用入口 URL,优先读环境变量
:alive true alive 模式:子应用切换时不销毁,保留完整状态
:props { token, userInfo } 直接传递数据给子应用(替代 postMessage)
v-show route.path === app.activeRule 控制显示/隐藏,配合 alive 实现 keep-alive

URL 解析逻辑

const getAppUrl = (app: MicroAppConfig): string => {
  // 环境变量命名规则:VITE_{APP_NAME}_APP_URL(大写,连字符转下划线)
  // 例如:VITE_MKT_APP_URL, VITE_COMMERCE_FINANCE_APP_URL
  const envKey = `VITE_${app.name.toUpperCase().replace(/-/g, '_')}_APP_URL`
  const envUrl = (import.meta.env as Record<string, string>)[envKey]
  return envUrl || app.entry  // 兜底使用注册表中的 entry
}

3.4 AuthenticatedLayout.vue — 事件监听

改造前(iframe postMessage):

import { listenFromSubApp, MESSAGE_TYPE } from "@/utils/iframe-bridge"
onMounted(() => {
  listenFromSubApp((data: any) => {
    if (data.type === MESSAGE_TYPE.TO_ROUTE) { ... }
    if (data.type === MESSAGE_TYPE.ROUTE_CHANGE) { ... }
  })
})

改造后(wujie bus):

import { bus } from "wujie"
onMounted(() => {
  bus.$on("TO_ROUTE", (data: any) => {
    tabsStore.menuClick({ appName: data.appName, path: data.path, ... })
  })
  bus.$on("ROUTE_CHANGE", (data: any) => {
    tabsStore.updateQuery(data)
  })
  bus.$on("ASSETS_404", (data: any) => {
    ElMessageBox.confirm(...)
  })
  bus.$on("CLOSE_ALL_TABS", (data: any) => {
    tabsStore.removeTab(data?.appName)
  })
})

3.5 tabs.ts — 主应用向子应用通信

改造前

import { sendToSubApp, reloadIframe } from "@/utils/iframe-bridge"
sendToSubApp(tab.appName, { type: NOTICE_TYPE.ROUTER_CHANGE, payload: {...} })
reloadIframe(tab.appName)

改造后

import { bus } from "wujie"
bus.$emit("ROUTER_CHANGE_TO_CHILD", { appName, route, query })
bus.$emit("REFRESH_CHILD", { appName: tab.appName })

四、通信协议设计

4.1 事件总线(wujie bus)

wujie 内置了一个全局事件总线 bus,主应用和子应用共享同一个 bus 实例。

主应用                          子应用
  │                               │
  │  bus.$emit("事件名", data)  ──→│  bus.$on("事件名", handler)
  │                               │
  │  bus.$on("事件名", handler) ←──│  bus.$emit("事件名", data)
  │                               │

4.2 事件清单

子应用 → 主应用

事件名 触发场景 payload 结构 主应用处理
TO_ROUTE 子应用请求跳转到某个路由 { appName, path, query, name } tabsStore.menuClick()
ROUTE_CHANGE 子应用内部路由变更 { appName, path } tabsStore.updateQuery() 同步 URL
ASSETS_404 子应用静态资源加载失败 { appName } 弹窗提示用户刷新
CLOSE_ALL_TABS 子应用请求关闭自己的 tab { appName } tabsStore.removeTab()

主应用 → 子应用

事件名 触发场景 payload 结构 子应用处理
ROUTER_CHANGE_TO_CHILD 主应用 tab 切换/菜单点击 { appName, route, query } 子应用内部路由跳转
CLOSE_ALL_TAB_TO_CHILD 主应用关闭子应用 tab { appName } 子应用清理状态
REFRESH_CHILD 用户点击刷新按钮 { appName } 子应用重新加载当前页

4.3 props 直传(补充通道)

除了 bus 事件,wujie 还支持通过 :props 直接向子应用传递数据:

<!-- 主应用 -->
<WujieVue :props="{ token: userStore.token, userInfo: userStore.userInfo }" />
// 子应用中获取
const props = (window as any).__WUJIE?.props
const token = props?.token

适用场景:token、用户信息等初始化数据,不需要事件驱动的静态数据。


五、子应用侧适配方案

5.1 改造范围

子应用需要将 iframe-bridge.ts 中的 postMessage 通信替换为 wujie bus.$emit

涉及文件(以 doc 子应用为例):

文件 当前用法 改造方案
src/utils/iframe-bridge.ts notifyMainApp()postMessage 新建 wujie-bridge.ts 替代
src/router/index.ts notifyMainApp(MESSAGE_TYPE.ROUTE_CHANGE, ...) 改 import 路径即可
src/App.vue getPathFromParent() 读父窗口 URL 改为 bus 监听 ROUTER_CHANGE_TO_CHILD
src/main.ts errorCheck()postMessage 报告 404 setupErrorCheck() 用 bus

5.2 改造步骤

Step 1:创建 wujie-bridge.ts(替代 iframe-bridge.ts)

// src/utils/wujie-bridge.ts
/**
 * @description wujie 子应用通信桥接器
 * @author yaowb
 * @date 2026-02-06
 */

// wujie 子应用环境下,bus 挂载在 window.__WUJIE 上
function getWujieBus() {
  return (window as any).__WUJIE?.bus
}

/** 是否在 wujie 子应用环境中 */
export function isInWujie(): boolean {
  return !!(window as any).__WUJIE
}

/** 获取主应用传递的 props */
export function getWujieProps(): Record<string, any> {
  return (window as any).__WUJIE?.props || {}
}

/** 向主应用发送事件(保持与 iframe-bridge 相同的函数签名) */
export function notifyMainApp(type: string, payload: any) {
  const bus = getWujieBus()
  if (bus) {
    bus.$emit(type, payload)
  } else {
    console.warn('[wujie-bridge] Not in wujie environment, skip emit:', type)
  }
}

/** 监听主应用发来的事件 */
export function onMainAppEvent(type: string, handler: (data: any) => void) {
  const bus = getWujieBus()
  if (bus) {
    bus.$on(type, handler)
  }
}

/** 移除事件监听 */
export function offMainAppEvent(type: string, handler: (data: any) => void) {
  const bus = getWujieBus()
  if (bus) {
    bus.$off(type, handler)
  }
}

/** 资源 404 错误检测 */
export function setupErrorCheck(appName: string) {
  if (!isInWujie()) return
  window.addEventListener('error', (event) => {
    if (event.target instanceof Element) {
      const tagName = event.target.tagName.toUpperCase()
      if (tagName === 'SCRIPT' || tagName === 'LINK') {
        notifyMainApp('ASSETS_404', { appName })
      }
    }
  }, true)
}

export const MESSAGE_TYPE = {
  TO_ROUTE: 'TO_ROUTE',
  ROUTE_CHANGE: 'ROUTE_CHANGE',
  ASSETS_404: 'ASSETS_404',
  CLOSE_ALL_TABS: 'CLOSE_ALL_TABS',
}

Step 2:改造 router/index.ts

// 改造前
import { notifyMainApp, MESSAGE_TYPE } from '@/utils/iframe-bridge'

// 改造后(函数签名不变,只换 import 路径)
import { notifyMainApp, MESSAGE_TYPE } from '@/utils/wujie-bridge'

// 业务代码完全不用改
router.afterEach((to) => {
  notifyMainApp(MESSAGE_TYPE.ROUTE_CHANGE, {
    appName: import.meta.env.VITE_APP_NAME,
    path: to.fullPath
  })
})

关键设计wujie-bridge.ts 保持与 iframe-bridge.ts 相同的 notifyMainApp() 函数签名和 MESSAGE_TYPE 常量,子应用只需替换 import 路径,业务代码零改动

Step 3:改造 App.vue(路由同步)

// 改造前:从父窗口 URL 读取 childPath
import { getPathFromParent } from '@/utils/iframe-bridge'
const childPath = getPathFromParent()

// 改造后:监听主应用的路由指令
import { onMainAppEvent } from '@/utils/wujie-bridge'
onMainAppEvent('ROUTER_CHANGE_TO_CHILD', (data) => {
  if (data.appName === import.meta.env.VITE_APP_NAME) {
    router.push({ path: data.route, query: data.query })
  }
})

Step 4:改造 main.ts(错误检测)

// 改造前
import { errorCheck } from '@/utils/iframe-bridge'
errorCheck()

// 改造后
import { setupErrorCheck } from '@/utils/wujie-bridge'
setupErrorCheck(import.meta.env.VITE_APP_NAME)

5.3 兼容性策略

子应用需要同时支持 wujie 模式独立运行模式(开发调试时直接访问子应用端口)。wujie-bridge.ts 已内置兼容:

export function notifyMainApp(type: string, payload: any) {
  const bus = getWujieBus()
  if (bus) {
    bus.$emit(type, payload)  // wujie 环境
  } else {
    console.warn('[wujie-bridge] Not in wujie, skip:', type)  // 独立运行,静默跳过
  }
}

5.4 改造检查清单(每个子应用)

  • 创建 src/utils/wujie-bridge.ts
  • src/router/index.tsimport 路径改为 wujie-bridge
  • src/App.vuegetPathFromParent()onMainAppEvent('ROUTER_CHANGE_TO_CHILD')
  • src/main.tserrorCheck()setupErrorCheck()
  • 搜索所有 iframe-bridge 引用,确认全部替换
  • 独立运行验证(直接访问子应用端口)
  • wujie 模式验证(通过主应用加载)
  • 删除旧的 src/utils/iframe-bridge.ts

六、子应用预加载策略

6.1 wujie preloadApp API

wujie 提供 preloadApp() 方法,可以在用户访问前预热子应用,减少首次加载白屏时间。

import { preloadApp } from 'wujie'

// 预加载指定子应用(只加载 HTML/JS/CSS,不渲染)
preloadApp({ name: 'doc', url: '/doc/' })

6.2 推荐策略

// src/plugins/wujie.ts(增强版)
import WujieVue from 'wujie-vue3'
import { preloadApp } from 'wujie'
import type { App } from 'vue'
import { microAppRegistry } from '@cmclink/micro-bridge'

export function setupWujie(app: App) {
  app.use(WujieVue)
}

/**
 * 预加载子应用(登录成功后调用)
 * 策略:
 *   - 高频子应用(doc、mkt):立即预加载
 *   - 其他子应用:延迟 3 秒后预加载,避免抢占主应用资源
 */
export function preloadChildApps() {
  const highPriority = ['doc', 'mkt']
  const lowPriority = microAppRegistry
    .filter(app => !highPriority.includes(app.name))

  // 高优先级:立即预加载
  highPriority.forEach(name => {
    const app = microAppRegistry.find(a => a.name === name)
    if (app) {
      preloadApp({ name: app.name, url: app.entry })
    }
  })

  // 低优先级:延迟预加载
  setTimeout(() => {
    lowPriority.forEach(app => {
      preloadApp({ name: app.name, url: app.entry })
    })
  }, 3000)
}

6.3 调用时机

// AuthenticatedLayout.vue — 用户登录成功后
import { preloadChildApps } from '@/plugins/wujie'

onMounted(() => {
  preloadChildApps()
  // ... 其他初始化
})

6.4 预加载效果预估

指标 无预加载 有预加载
子应用首次切换白屏 1-3 秒 < 500ms
主应用首屏影响 高优先级 +200ms,低优先级无感
内存占用 按需加载 预热后常驻(alive 模式本身就常驻)

七、路由同步设计

7.1 当前方案

主应用路由与子应用路由的映射关系:

主应用 URL: /micro-main/doc?childPath=/order/list
                          |
子应用内部路由: /order/list

7.2 路由同步流程

用户点击菜单
    │
    ▼
主应用 router.push('/doc')
    │
    ▼
App.vue v-show 切换显示 doc 子应用
    │
    ▼
tabs.ts bus.$emit('ROUTER_CHANGE_TO_CHILD', { appName: 'doc', route: '/order/list' })
    │
    ▼
子应用 bus.$on('ROUTER_CHANGE_TO_CHILD') → router.push('/order/list')
    │
    ▼
子应用 router.afterEach → bus.$emit('ROUTE_CHANGE', { appName: 'doc', path: '/order/list' })
    │
    ▼
主应用 bus.$on('ROUTE_CHANGE') → router.replace({ query: { childPath: '/order/list' } })

7.3 wujie sync 路由同步模式(深度优化方向)

wujie 内置了路由同步能力,可以通过 sync 属性开启:

<WujieVue
  :name="app.name"
  :url="getAppUrl(app)"
  :alive="true"
  :sync="true"   <!-- 开启路由同步 -->
/>

开启后,子应用的路由变更会自动同步到主应用 URL 的 query 参数中:

主应用 URL: /micro-main/doc?doc=/order/list&doc-query=xxx

注意sync 模式与当前手动 childPath 方案有冲突,建议在 Phase 3 中评估后再开启。当前阶段保持手动同步方案,确保平稳过渡。


八、清理旧 apps/main

8.1 背景

apps/main 是之前基于 @micro-zoe/micro-app 框架搭建的主应用原型,不是真正的生产主应用。真正的主应用是 apps/cmclink-web-micro-main

8.2 差异对比

维度 apps/main(旧) apps/cmclink-web-micro-main(真)
package name main-app @cmclink/main
微前端方案 @micro-zoe/micro-app iframe → wujie
子应用数量 3(marketing/doc/ibs-manage) 7(完整业务线)
业务代码 简化版 完整生产代码
状态 ⚠️ 待清理 ✅ 正式使用

8.3 清理步骤

# 1. 确认 cmclink-web-micro-main 正常运行
pnpm --filter @cmclink/main dev

# 2. 删除旧主应用
rm -rf apps/main

# 3. 更新 turbo.json(如有 filter 引用 main-app 的地方)

# 4. pnpm install 重新解析 workspace
pnpm install

⚠️ 注意:删除前请确认 apps/main 中的 MicroAppContainer.vue@cmclink/micro-bridge 集成代码等有价值的内容已迁移到 cmclink-web-micro-main


九、CSS 方案统一路线

9.1 现状

应用 CSS 方案 版本 问题
主应用 Tailwind CSS 4 ^4.1.14 ✅ 无问题
doc 子应用 UnoCSS 0.56.5 ❌ 不兼容 Vite 7
ibs-manage 子应用 UnoCSS 0.56.5 ❌ 不兼容 Vite 7

9.2 推荐方案

统一迁移到 Tailwind CSS 4,理由:

  • 主应用已使用 Tailwind CSS 4,统一后减少认知负担
  • Tailwind CSS 4 原生支持 Vite 7
  • UnoCSS 0.56.5 的 Vite 插件不兼容 Vite 7(peer dependency 冲突)

9.3 迁移步骤(每个子应用)

# 1. 卸载 UnoCSS
pnpm --filter @cmclink/doc remove unocss @unocss/vite @unocss/preset-uno

# 2. 安装 Tailwind CSS 4
pnpm --filter @cmclink/doc add tailwindcss @tailwindcss/vite

# 3. 替换 vite.config.ts 中的插件
#    UnoCSS() → tailwindcss()

# 4. 创建 CSS 入口文件
#    @import "tailwindcss";

# 5. 逐步替换 UnoCSS 专有语法(如 attributify 模式)

9.4 风险评估

风险 影响 缓解措施
UnoCSS attributify 语法无对应 需手动改为 class 写法 全局搜索 un- 前缀
UnoCSS 自定义 rules 需转为 Tailwind 插件 逐个评估,大部分有等价写法
迁移期间样式回归 页面样式可能错乱 逐页面验证,保留 UnoCSS 作为过渡

十、实施路线图

Phase 1 ✅ 已完成:主应用 wujie 集成
├── ✅ cmclink-web-micro-main 纳入 monorepo
├── ✅ App.vue iframe → WujieVue 动态渲染
├── ✅ AuthenticatedLayout.vue 通信改造
├── ✅ tabs.ts 通信改造
├── ✅ 子应用注册表更新(7 个子应用)
└── ✅ pnpm install + 启动验证

Phase 2 ✅ 已完成:子应用侧适配(2026-02-06)
├── ✅ 创建 wujie-bridge.ts 替代 iframe-bridge.ts(doc + ibs-manage)
├── ✅ 改造 router/index.ts — import 路径替换,业务代码零改动
├── ✅ 改造 App.vue — getPathFromParent 来源替换
├── ✅ 改造 main.ts / service.ts / linkCpf.vue — 所有引用替换
└── ✅ pnpm install 验证通过

Phase 3 ✅ 已完成:深度优化(2026-02-06)
├── ✅ 子应用预加载策略(preloadChildApps 高/低优先级分级)
├── ✅ wujie sync 路由同步评估(当前手动方案已满足,sync 留待后续)
├── 通信层类型安全增强(后续迭代)
└── 性能监控与错误上报(后续迭代)

Phase 4 ✅ 已完成:CSS 统一(2026-02-06)
├── ✅ doc 子应用 UnoCSS → Tailwind CSS 4
├── ✅ ibs-manage 子应用 UnoCSS → Tailwind CSS 4
├── ✅ package.json 依赖替换(+2 -60 packages)
├── ✅ stylelintrc.json 规则更新
└── 其他子应用迁入时直接使用 Tailwind CSS 4

Phase 5 📋 规划中:公共资源沉淀与 AI 编程能力
├── 详见 doc/前端架构治理演进规划.md

附录 A:wujie 核心概念速查

概念 说明
alive 模式 子应用实例常驻内存,切换时不销毁。适合多 tab 场景
bus 全局事件总线,主子应用共享。bus.$on / bus.$emit
props 主应用通过 :props 向子应用传递数据,子应用通过 window.__WUJIE.props 读取
preloadApp 预加载子应用资源(HTML/JS/CSS),不渲染 DOM
sync 路由同步模式,子应用路由自动映射到主应用 URL query
degrade 降级模式,当浏览器不支持 Proxy 时自动降级为 iframe

附录 B:常用命令

# 启动主应用
pnpm --filter @cmclink/main dev

# 启动主应用 + 单证子应用
pnpm --filter @cmclink/main --filter @cmclink/doc dev

# 全量启动
pnpm dev

# 构建
pnpm build

# 增量构建(只构建变更的应用)
pnpm build:affected

附录 C:新增子应用接入指南

新增一个子应用只需 3 步:

1. 注册表添加配置packages/micro-bridge/src/registry.ts):

{
  name: 'new-app',
  entry: '/new-app/',
  activeRule: '/new-app',
  port: 3008,
}

2. 主应用路由添加占位src/router/index.ts):

{
  path: '/new-app',
  name: 'NewApp',
  component: { render: () => null },
  meta: { name: '新应用', appName: 'new-app' },
}

3. tabs.ts 添加子应用路径和 tab 信息

// childPathList 添加
'/new-app'

// appList 添加
{ name: '新应用', nameEn: 'New App', appName: 'new-app', route: '/new-app', show: false }

App.vue 中的 <WujieVue> 会自动基于注册表渲染,无需修改

# 🚀 极致性能:Vue3 全球化项目图片资源优化实战指南

摘要:针对全球化场景(跨国延迟、弱网环境)下的图片加载痛点,本文提供了一套基于 Vue 3 + Vite + TypeScript 的全链路解决方案。从构建时的自动压缩,到运行时的智能组件封装,再到 CSS 背景图的“盲区”攻克,三位一体,拒绝理论空谈,直接上代码实战。

🌏 一、背景与痛点:为什么图片优化是重中之重?

在全球化业务中,图片资源往往占据页面体积的 60% 以上。面临的核心挑战包括:

  • 物理距离远:跨国 RTT(往返时延)高,图片加载慢导致白屏。
  • 网络环境杂:弱网、丢包率高,大图加载极易失败。
  • LCP 考核严:图片通常是 LCP(最大内容绘制)元素,直接影响 Core Web Vitals 评分和 SEO。

我们的目标:在不牺牲视觉质量的前提下,将图片体积压缩 40%-80%,并将首屏加载速度提升 30% 以上


🛠️ 二、构建层:零侵入的自动化压缩流水线

最有效的优化是 “不让未经压缩的图片上线”。我们利用 Vite 插件在构建阶段自动完成格式转换和无损压缩。

1. 核心工具

引入 vite-plugin-image-optimizer,基于 Sharp 和 SVGO 引擎。

2. 实战配置 (vite.config.ts)

// build/plugins.ts
import { ViteImageOptimizer } from 'vite-plugin-image-optimizer'

export default defineConfig({
  plugins: [
    ViteImageOptimizer({
      test: /\.(jpe?g|png|gif|tiff|webp|svg|avif)$/i,
      svg: {
        multipass: true,
        plugins: [
          {
            name: 'removeViewBox',
            active: false
          },
          {
            name: 'removeDimensions',
            active: true
          },
        ],
      },
      png: { quality: 80 },
      jpeg: { quality: 80 },
      jpg: { quality: 80 },
      webp: { lossless: true },
      avif: { lossless: true },
    }),
  ]
})

💡 收益:开发同学无需关心图片格式,设计给的 PNG/JPG 原图,打包后自动变成压缩后的版本,体积平均减少 50%。


🧩 三、组件层:智能封装 OptimizedImage

为了让业务开发“无感”使用优化策略,我们将复杂度封装在组件内部。

1. 核心能力

  • 自动降级:利用 <picture> 标签,优先加载 AVIF/WebP,老旧浏览器回退到 JPG。
  • 骨架屏占位:加载中显示 Loading/占位色,防止布局抖动 (CLS)。
  • CDN 动态参数:自动拼接宽、高、质量参数。

2. 组件源码 (src/components/OptimizedImage/index.vue)

<script setup lang="ts">
import { computed, ref } from 'vue'

interface Props {
  src: string
  useCdn?: boolean
  // ...其他 Props
}

const props = withDefaults(defineProps<Props>(), { useCdn: false })

// 自动生成多格式源
const sources = computed(() => {
  if (!props.useCdn || !props.src?.startsWith('http')) return []
  const sep = props.src.includes('?') ? '&' : '?'
  return [
    {
      srcset: `${props.src}${sep}format=avif`,
      type: 'image/avif'
    },
    {
      srcset: `${props.src}${sep}format=webp`,
      type: 'image/webp'
    },
  ]
})
</script>

<template>
  <div class="optimized-image-container">
    <picture v-if="sources.length">
      <source
        v-for="(s, i) in sources"
        :key="i"
        :srcset="s.srcset"
        :type="s.type"
      >
      <img :src="src" loading="lazy">
    </picture>
    <!-- 降级/普通图片 -->
    <img v-else :src="src" loading="lazy">
  </div>
</template>

3. 业务使用

<OptimizedImage src="banner.png" width="800" height="400" use-cdn />

🎨 四、攻克盲区:CSS 背景图优化策略

CSS background-image 是优化的“死角”,因为它不支持 loading="lazy"<picture>。我们通过以下手段攻克:

1. 格式降级:image-set()

利用 CSS 原生语法实现格式选择。

.hero-bg {
  /* 兜底 */
  background-image: url('bg.jpg');
  /* 现代浏览器优先 */
  background-image: image-set(
    url('bg.avif') type('image/avif'),
    url('bg.webp') type('image/webp'),
    url('bg.jpg') type('image/jpeg')
  );
}

2. 智能懒加载:useBackgroundLazy Hook

首屏不可见的背景图,坚决不加载。我们封装了一个 Vue Hook。

源码 (src/composables/ui/useBackgroundLazy.ts)

import type { Ref } from 'vue'
import { useIntersectionObserver } from '@vueuse/core'
import { ref } from 'vue'

export function useBackgroundLazy(
  targetRef: Ref<HTMLElement | null | undefined>,
  options: IntersectionObserverInit = { rootMargin: '100px' }
) {
  const isVisible = ref(false)
  const { stop } = useIntersectionObserver(
    targetRef,
    ([{ isIntersecting }]) => {
      if (isIntersecting) {
        isVisible.value = true
        stop()
      }
    },
    options
  )
  return isVisible
}

使用示例

<script setup>
import { useBackgroundLazy } from '@/composables/ui/useBackgroundLazy'

const bgRef = ref(null)
const isVisible = useBackgroundLazy(bgRef)
</script>

<template>
  <div ref="bgRef" class="lazy-bg" :class="{ visible: isVisible }">
    ...
  </div>
</template>

<style scoped>
.lazy-bg {
  background-color: #f0f0f0;
}
.lazy-bg.visible {
  background-image: url('heavy-bg.jpg');
}
</style>

⚡ 五、关键路径渲染:LCP 救星

针对首屏最大的那张图(LCP 元素),我们要“特权”对待。

1. 提升优先级 (fetchpriority)

告诉浏览器:这张图最重要,插队加载!

<img src="hero-banner.jpg" fetchpriority="high" loading="eager" />

2. 预加载 (preload)

在 HTML 解析前就提前建立连接并下载。

<link rel="preload" as="image" href="hero-banner.webp" />

🌐 六、网络层:协议选择与 CDN 策略深度解析

网络传输是图片加载的“高速公路”。针对不同基础设施条件,我们提供 进阶版 (HTTP/3)标准版 (HTTP/2) 两套方案,并对比其优劣。

1. 协议选择:HTTP/3 vs HTTP/2

特性 HTTP/2 (标准版) HTTP/3 (进阶版) 核心差异
底层协议 TCP UDP (QUIC) H3 解决了 TCP 的“队头阻塞”问题
弱网表现 丢包时会导致整条连接等待,性能急剧下降 丢包仅影响单个流,其余流正常传输,弱网极大优势
连接建立 3 RTT (TCP+TLS) 0-1 RTT (大幅缩短建连时间)
兼容性 98%+ 浏览器支持 需浏览器 + 服务端/CDN 双向支持
适用场景 绝大多数常规 Web 项目 全球化、移动端、弱网环境重灾区

✅ 方案 A:极致性能 (HTTP/3 + QUIC)

  • 适用:已使用 Cloudflare, AWS CloudFront, 阿里云 CDN 等支持 QUIC 的现代 CDN 服务商。
  • 配置:在 CDN 控制台开启 HTTP/3 (with QUIC) 选项。
  • 收益:在跨国高延迟(RTT > 200ms)或丢包率 > 1% 的环境下,图片加载速度提升 20% - 50%

✅ 方案 B:稳健兼容 (HTTP/2 + 域名分片废弃)

  • 适用:内部私有云或老旧 CDN 不支持 UDP/QUIC。
  • 关键调整
    • 开启 HTTP/2:必须开启,利用多路复用。
    • 废弃域名分片:在 H2/H3 时代,不要再把图片分散到 img1.domain.com, img2.domain.com。多域名会导致多余的 DNS 解析和 TCP 建连,反而降低多路复用效率。保持单一域名(如 assets.domain.com)是最佳实践。

2. CDN 智能策略:Edge Image Manipulation

不要让后端服务器处理图片!利用 CDN 的边缘计算能力。

  • 即时处理 (On-the-fly):URL 传参控制。
    • https://cdn.com/img.jpg?width=400&format=webp
    • 利弊:灵活性极高,但首次访问需回源处理,有轻微延迟(随后即被 CDN 缓存)。
  • 自动格式转换 (Auto-Format)
    • CDN 检查请求头 Accept: image/avif, image/webp
    • 源站只有一张 JPG,CDN 自动转为 AVIF/WebP 返回给支持的浏览器。
    • 利弊:开发零感知,完全透明,强烈推荐。

3. 缓存策略:Immutable

对于带 Hash 的静态资源(如 Vite 打包出的 banner.8a7d9f.png),应设置“永久”缓存。

# Nginx 配置示例
location ~* \.(?:png|jpg|jpeg|gif|webp|avif|svg)$ {
    # 1年有效期,且声明内容不可变(浏览器完全无需发请求验证)
    add_header Cache-Control "public, max-age=31536000, immutable";
}

📊 七、量化验证:拒绝“感觉变快了”

我们需要可复现、可执行的数据来证明优化效果。

1. 实验室数据 (Lab Data) - 开发阶段自测

工具:Chrome DevTools > Lighthouse

  • 操作
    1. 打开 Chrome 隐身模式。
    2. F12 -> Lighthouse -> 选择 "Mobile" (模拟弱网) 或 "Desktop"。
    3. 点击 "Analyze page load"。
  • 核心关注指标
    • LCP (Largest Contentful Paint): 应 < 2.5s。
    • Total Blocking Time (TBT): 图片解码是否阻塞主线程。

2. 真实用户数据 (RUM) - 生产环境监控

仅靠实验室数据是不够的,我们需要脚本自动收集真实加载情况。

✅ 自动化脚本 (复制到控制台运行或集成到监控 SDK)

// 性能监控脚本:计算 LCP 和图片资源耗时
const observer = new PerformanceObserver((list) => {
  const entries = list.getEntries()
  entries.forEach((entry) => {
    // 1. 捕捉 LCP
    if (entry.entryType === 'largest-contentful-paint') {
      console.log(`🚀 [LCP] 耗时: ${entry.startTime.toFixed(2)}ms`, entry)
      if (entry.url) console.log(`   LCP 资源: ${entry.url}`)
    }
    // 2. 捕捉图片资源加载详情
    if (entry.entryType === 'resource' && entry.initiatorType === 'img') {
      const isCache = entry.transferSize === 0 // 缓存命中
      const protocol = entry.nextHopProtocol // h2 或 h3
      console.log(`�️ [Image] ${entry.name.split('/').pop()}`)
      console.log(`   - 耗时: ${entry.duration.toFixed(2)}ms`)
      console.log(`   - 协议: ${protocol}`)
      console.log(`   - 体积: ${(entry.encodedBodySize / 1024).toFixed(2)}KB`)
      console.log(`   - 缓存: ${isCache ? '✅ HIT' : '❌ MISS'}`)
    }
  })
})

observer.observe({
  type: 'largest-contentful-paint',
  buffered: true
})
observer.observe({
  type: 'resource',
  buffered: true
})

3. 验证步骤 (SOP)

  1. 基准测试 (Baseline)
    • 关闭所有优化开关(Vite 插件、组件回退到普通 img)。
    • 使用 Chrome Network 面板 "Fast 3G" 模拟弱网。
    • 记录 LCP 时间和 Network 面板的 Transferred 总大小。
  2. 实施优化
    • 启用 vite-plugin-image-optimizer
    • 部署 HTTP/3 CDN。
    • 替换 OptimizedImage 组件。
  3. 对比测试
    • 同样环境(Fast 3G)再次测量。
    • 验收标准
      • 图片总传输体积减少 > 40%
      • LCP 时间减少 > 30%
      • Network 面板 Protocol 列显示 h3h2

结语:性能优化没有银弹,只有对细节的极致追求。通过上述方案,我们建立了一套可维护、自动化的图片治理体系,为全球用户提供丝滑的浏览体验。

❌