正在加载,请稍候…

前端架构模式:特性切片、组件库与可扩展代码库

扩展前端代码库:特性切片设计、组件库架构、状态管理决策、微前端、设计令牌以及防止意大利面条式代码的模式。

前端架构模式:特性切片、组件库与可扩展代码库

当“直接构建”不再奏效

小型前端不需要架构。一个 components/ 文件夹和一个 pages/ 文件夹就能带你走很远。但随着团队和代码库的增长,缺乏有意识的架构会带来累积的痛苦:循环导入、所有权模糊、组件膨胀到 2000 行、“这个逻辑属于哪个 store 模块?”

前端架构的目标不是制造官僚主义——而是提供护栏,让正确的决策变得容易。

前端架构模式:特性切片、组件库与可扩展代码库示意图

“组件和工具函数”的问题

最常见的文件夹结构也是最难扩展的:

src/
├── components/    ← 所有 UI 相关
├── hooks/         ← 所有自定义 hooks
├── utils/         ← 辅助函数
├── store/         ← 状态管理
├── types/         ← TypeScript 类型
└── pages/         ← 页面/路由

问题所在:

  • 单个 components/ 文件夹可能有 200+ 个文件——没有指导说明哪些组件依赖什么
  • 跨组件导入导致循环:UserCard 导入 ProductCard 又导入 UserCard
  • 缺乏就近原则:UserCard.tsx、useUser.ts、userTypes.ts、userStore.ts 分散在四个不同的文件夹
  • “全局”和“特性特定”代码之间没有明确边界

特性切片设计(FSD)

FSD 基于代码的用途提供一致的结构:

src/
├── app/            # 应用级设置:providers、router、全局样式
├── pages/          # 路由级组件(组合特性)
├── widgets/        # 大型复合块(Header、Sidebar、Feed)
├── features/       # 具有业务价值的用户交互
│   ├── auth/
│   ├── user-profile/
│   └── product-cart/
├── entities/       # 业务实体(User、Product、Order)
│   ├── user/
│   ├── product/
│   └── order/
└── shared/         # 可复用:UI 工具包、工具函数、API 客户端
    ├── ui/
    ├── api/
    ├── lib/
    └── config/

依赖规则:层只能从下面的层导入。pageswidgetsfeatures 导入。featuresentitiesshared 导入。shared 不从上面导入任何内容。

// ✅ 有效导入(向下):
// pages/ProductPage.tsx 从 widgets/ProductCard 导入
// features/auth/LoginForm.tsx 从 entities/user 导入
// entities/user/api.ts 从 shared/api 导入

// ❌ 无效导入(向上)——这些是 bug:
// shared/ui/Button.tsx 从 features/auth 导入  ← 绝对不行
// entities/user 从 features/auth 导入          ← 绝对不行

前端架构模式:特性切片、组件库与可扩展代码库示意图

特性切片结构

每个特性切片都是自包含的:

features/auth/
├── ui/                    # 特定于 auth 的 UI 组件
│   ├── LoginForm.tsx
│   ├── RegisterForm.tsx
│   └── AuthProvider.tsx
├── model/                 # 状态、类型、hooks
│   ├── authStore.ts       # Zustand/Redux slice
│   ├── useAuth.ts         # 自定义 hooks
│   └── types.ts
├── api/                   # 该特性的 API 调用
│   └── authApi.ts
└── index.ts               # 公共 API——只导出其他模块需要的内容
// features/auth/index.ts — 显式公共 API
export { LoginForm } from './ui/LoginForm'
export { AuthProvider } from './ui/AuthProvider'
export { useAuth } from './model/useAuth'
export type { User, AuthState } from './model/types'

// 所有未在此处导出的内容都是该特性的私有内容
// 其他特性不能直接导入 AuthStore 内部

设计令牌:一致 UI 的基础

// tokens.ts — 视觉属性的单一事实来源
export const tokens = {
  color: {
    // 原始值
    gray: {
      50: '#F9FAFB',
      100: '#F3F4F6',
      500: '#6B7280',
      900: '#111827',
    },
    blue: {
      500: '#3B82F6',
      700: '#1D4ED8',
    },
    red: {
      500: '#EF4444',
    },
    
    // 语义(基于用途——令牌的实际含义)
    text: {
      primary: '#111827',    // gray-900
      secondary: '#6B7280',  // gray-500
      disabled: '#D1D5DB',
      inverse: '#FFFFFF',
      error: '#EF4444',
    },
    background: {
      default: '#FFFFFF',
      subtle: '#F9FAFB',
      elevated: '#FFFFFF',
    },
    border: {
      default: '#E5E7EB',
      strong: '#9CA3AF',
      focus: '#3B82F6',
    },
    interactive: {
      primary: '#3B82F6',
      primaryHover: '#2563EB',
      primaryActive: '#1D4ED8',
    },
  },
  
  spacing: {
    1: '4px',
    2: '8px',
    3: '12px',
    4: '16px',
    6: '24px',
    8: '32px',
    12: '48px',
    16: '64px',
  },
  
  typography: {
    fontFamily: {
      sans: "'Inter', system-ui, sans-serif",
      mono: "'Fira Code', 'Consolas', monospace",
    },
    fontSize: {
      xs: '12px',
      sm: '14px',
      base: '16px',
      lg: '18px',
      xl: '20px',
      '2xl': '24px',
      '3xl': '30px',
    },
    fontWeight: {
      regular: 400,
      medium: 500,
      semibold: 600,
      bold: 700,
    },
    lineHeight: {
      tight: 1.25,
      normal: 1.5,
      relaxed: 1.75,
    },
  },
  
  borderRadius: {
    sm: '4px',
    md: '8px',
    lg: '12px',
    full: '9999px',
  },
  
  shadow: {
    sm: '0 1px 2px rgba(0, 0, 0, 0.05)',
    md: '0 4px 6px -1px rgba(0, 0, 0, 0.1)',
    lg: '0 10px 15px -3px rgba(0, 0, 0, 0.1)',
  },
}

// CSS 自定义属性(从令牌自动生成)
export function generateCSSVariables(): string {
  return `
    :root {
      --color-text-primary: ${tokens.color.text.primary};
      --color-text-secondary: ${tokens.color.text.secondary};
      --color-interactive-primary: ${tokens.color.interactive.primary};
      --spacing-4: ${tokens.spacing[4]};
      /* ... */
    }
  `
}

前端架构模式:特性切片、组件库与可扩展代码库示意图

组件 API 设计

// ❌ 隐式 API——不清楚哪些是有效的
interface ButtonProps {
  variant?: string      // 有效值是什么?
  size?: string         // 同样的问题
  color?: string        // 无限组合——维护噩梦
  onClick?: () => void
  disabled?: boolean
  className?: string    // 逃生口,让你破坏任何东西
}

// ✅ 显式、受限的 API
interface ButtonProps {
  variant: 'primary' | 'secondary' | 'ghost' | 'danger'
  size?: 'sm' | 'md' | 'lg'
  onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void
  disabled?: boolean
  loading?: boolean
  type?: 'button' | 'submit' | 'reset'
  // 没有 className——改用 variant 系统
  // 没有 style——强制使用设计系统
  // 只有 children: React.ReactNode
  children: React.ReactNode
}

function Button({ variant, size = 'md', disabled, loading, ...props }: ButtonProps) {
  return (
    <button
      className={buttonVariants({ variant, size })} // cva 或 class-variance-authority
      disabled={disabled || loading}
      {...props}
    >
      {loading ? <Spinner /> : props.children}
    </button>
  )
}

状态架构决策框架

服务器状态(来自 API)                → React Query / SWR
  - 缓存、后台重新获取、乐观更新

本地 UI 状态                         → useState + useReducer
  - 表单状态、模态框打开/关闭、仅 UI 的切换

共享客户端状态(跨组件)              → Zustand / Jotai
  - 用户偏好、购物车、选中项
  - 应该很少见——大多数状态是局部的

URL 状态                              → URL 搜索参数
  - 筛选值、分页、排序顺序
  - 好处:可分享链接、返回按钮

表单状态                             → React Hook Form / Formik
  - 复杂表单与验证

全局服务器会话                       → React Context(很少)
  - 当前用户、功能标志
  - 仅当涉及 SSR 时
// 示例:正确的状态放置
function ProductList() {
  // 服务器状态:React Query 管理缓存、加载、错误
  const { data: products, isLoading } = useQuery({
    queryKey: ['products', filters],
    queryFn: () => fetchProducts(filters),
  })
  
  // URL 状态:筛选条件在页面刷新后保留且可分享
  const [searchParams, setSearchParams] = useSearchParams()
  const filters = {
    category: searchParams.get('category') ?? 'all',
    sort: searchParams.get('sort') ?? 'newest',
    page: Number(searchParams.get('page') ?? 1),
  }
  
  // 本地 UI 状态:面板打开/关闭不需要 URL
  const [filtersOpen, setFiltersOpen] = useState(false)
  
  // 全局状态:购物车是共享的,但来自 Zustand
  const addToCart = useCartStore(state => state.addItem)
  
  // ...
}

微前端:何时考虑

微前端将大型前端拆分为独立可部署的片段,每个片段由不同团队拥有:

// Module Federation (Webpack/Vite)
// 宿主应用(shell)在运行时加载远程应用

// webpack.config.js (host)
new ModuleFederationPlugin({
  remotes: {
    checkout: 'checkout@https://checkout.example.com/remoteEntry.js',
    catalog: 'catalog@https://catalog.example.com/remoteEntry.js',
  },
})

// 在宿主中使用:
const CheckoutFlow = React.lazy(() => import('checkout/CheckoutFlow'))
const ProductCatalog = React.lazy(() => import('catalog/ProductCatalog'))

何时使用微前端:

  • 同一代码库有 10+ 名前端开发者
  • 团队需要独立的部署流水线
  • 不同部分使用不同框架
  • 存在清晰、稳定的领域边界

何时不使用:

  • 1-3 人团队(协调开销 > 收益)
  • 没有清晰的领域边界
  • 性能是问题(多个 bundle 加载)
  • 你正在启动一个绿地项目

最重要的架构决策是在代码库增长之前建立清晰的规则。将架构改造到 100,000 行代码库中比从一开始就采用合理的约定要痛苦得多。

→ 使用 JSON Viewer 探索和导航复杂的 JSON 结构。