浅谈前端项目中的 Clean Architecture

随着前端项目规模的不断增长,"如何组织代码"这个问题变得越来越重要。
不再只是简单的 CRUD 页面,我们开始面对复杂的状态管理、多数据源、
频繁变更的业务逻辑——这时候,一个好的架构就是项目的生命线。

Clean Architecture(整洁架构)由 Robert C. Martin(Uncle Bob)提出,
核心理念是关注点分离依赖反转
虽然它最初是为后端设计的,但前端同样可以从中受益。

Clean Architecture 的核心原则

简单来说,Clean Architecture 把代码分成几层,每一层都有明确的职责,
依赖关系从外向内——外层依赖内层,内层对外层一无所知。

  • 实体(Entities): 最内层,包含业务核心概念和规则,不依赖任何框架。
  • 用例(Use Cases): 应用特定的业务逻辑,编排实体的使用流程。
  • 接口适配(Interface Adapters): 把用例的输出适配到外部框架(如 React/Vue)。
  • 框架与驱动(Frameworks & Drivers): 最外层,UI 框架、API 客户端、数据库等。

关键原则: 依赖规则(Dependency Rule)—— 源代码的依赖只能指向内层,
内层永远不知道外层的任何信息。

在前端项目中如何实践

以一个典型的 React 项目为例,我们可以这样组织目录结构:

src/
├── core/                 # 最内层:业务实体
│   ├── entities/
│   │   └── User.ts
│   └── value-objects/
│       └── Email.ts
├── application/          # 用例层
│   ├── use-cases/
│   │   ├── CreateUser.ts
│   │   └── GetUserProfile.ts
│   └── ports/            # 接口定义
│       ├── UserRepository.ts
│       └── AuthService.ts
├── infrastructure/       # 外部实现
│   ├── api/
│   │   └── UserApi.ts
│   └── storage/
│       └── LocalStorage.ts
├── presentation/         # UI 层
│   ├── components/
│   ├── pages/
│   └── hooks/
└── di/                   # 依赖注入
    └── container.ts

这个结构的好处是什么?

  1. 业务逻辑独立: coreapplication 层的代码
    完全不依赖 React,可以单独测试。
  2. 替换外部依赖简单: 今天用 REST API,明天想换 GraphQL?
    只需要在 infrastructure 层替换实现,核心逻辑不用动。
  3. 测试友好: 用例层的测试不需要模拟 DOM 或浏览器 API,
    纯 JavaScript 就能跑。

一个简单的例子

假设我们有一个"获取用户信息"的功能。传统的做法可能直接在组件里调用 API:

// ❌ Bad: 业务逻辑直接写在 UI 层
function UserProfile({ userId }) {
  const [user, setUser] = useState(null)

  useEffect(() => {
    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(setUser)
  }, [userId])

  // ...
}

用 Clean Architecture 的思路改写:

// core/entities/User.ts
class User {
  constructor(
    public readonly id: string,
    public readonly name: string,
    public readonly email: Email
  ) {}
}

// application/ports/UserRepository.ts
interface UserRepository {
  findById(id: string): Promise<User>
}

// application/use-cases/GetUserProfile.ts
class GetUserProfile {
  constructor(private userRepo: UserRepository) {}

  async execute(userId: string): Promise<User> {
    const user = await this.userRepo.findById(userId)
    if (!user) throw new Error('User not found')
    return user
  }
}

// infrastructure/api/UserApi.ts
class UserApi implements UserRepository {
  async findById(id: string): Promise<User> {
    const res = await fetch(`/api/users/${id}`)
    const data = await res.json()
    return new User(data.id, data.name, new Email(data.email))
  }
}

注意事项

虽然 Clean Architecture 有很多优点,但也需要注意不要过度设计:

  • 小项目不必强求: 如果只是一个简单的 CRUD 页面,
    分层反而增加了复杂度。等业务逻辑复杂后再"重构进去"也不迟。
  • 团队共识很重要: 架构需要整个团队认同和理解。
    如果只有一个人遵守规则,其他人直接跳过分层,反而会更混乱。
  • 灵活调整: 不必照搬后端的四层结构。
    前端可以简化为三层(领域层、应用层、展示层),根据自己的场景裁剪。

总结

Clean Architecture 提供了一种有价值的设计思路,但并不是万能的银弹。
它的核心价值在于让业务逻辑保持干净和可测试,这在项目规模变大时的收益非常明显。

关键在于:什么时候用、用多少,需要根据项目的实际情况来判断。
架构设计本身就是一个不断权衡的过程。