普通视图

发现新文章,点击刷新页面。
昨天以前首页

前端架构治理演进规划

2026年2月6日 18:54

一、背景与目标

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(无界)集成设计文档

2026年2月6日 18:44

一、背景与选型

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 全球化项目图片资源优化实战指南

2026年2月5日 15:11

摘要:针对全球化场景(跨国延迟、弱网环境)下的图片加载痛点,本文提供了一套基于 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

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

❌
❌