一、概述
整洁架构 Clean Architecture 由 Robert C. Martin("Uncle Bob") 提出,是一种以 "业务逻辑中心化、外部依赖解耦" 为核心的软件架构设计方法。它通过分层设计 + 单向依赖规则,将业务逻辑与框架、UI 、数据源等外部元素隔离,确保核心逻辑独立于技术实现。
在前端应用中,这一架构能帮助开发者构建易维护、可测试、能长期迭代 的代码结构 ------ 尤其适用于业务复杂、多端适配 的大型项目:比如企业级后台管理系统、跨 Web / 小程序 / 桌面端的应用 ,可通过核心逻辑复用降低跨端开发成本。
二、架构设计
2.1 核心原则
Clean Architecture 遵循 "内层不依赖外层,外层依赖内层" 的单向依赖规则,前端场景下可适配为更贴合开发习惯的三层核心结构(对应经典分层的核心职责):
| 前端适配分层 | 对应经典 Clean Architecture 分层 | 核心职责 |
|---|---|---|
| 领域层(Domain Layer) | Entities + Use Cases(核心) | 封装企业级业务规则、定义核心业务模型与操作契约,代码独立于任何框架 / 平台 |
| 接口适配层(Interface Adapter Layer) | Interface Adapters | 负责数据格式转换(如 API 数据 → 领域模型)、封装数据源实现(API / 本地存储),是领域层 与外部依赖的 "桥梁" |
| 表示层(Presentation Layer) | Frameworks & Drivers | 处理 UI 渲染、用户交互,依赖前端框架(React / Vue )、状态管理库等,仅与接口适配层交互 |
2.2 目录结构
Clean Architecture 的前端落地采用 "按层分目录、按业务聚合模块" 的结构,以 Todo 业务为例
项目根目录/
├── src/
│ ├── domain/ # 领域层:核心业务逻辑
│ │ ├── model/ # 业务模型:封装核心规则
│ │ │ └── todo.ts
│ │ └── repository/ # 仓储接口:定义数据操作契约
│ │ └── todo-repository.ts
│ ├── data/ # 接口适配层:数据源实现
│ │ ├── datasource/ # 数据源:封装不同存储方式
│ │ │ ├── api/ # API 数据源
│ │ │ │ ├── entity/ # API 原始数据类型
│ │ │ │ │ └── todo-api-entity.ts
│ │ │ │ └── todo-api-datasource.ts
│ │ │ └── local-storage/ # 本地存储数据源(示例)
│ │ │ └── todo-local-datasource.ts
│ │ └── repository/ # 仓储实现:关联数据源
│ │ └── todo-repository-impl.ts
│ ├── usecase/ # 用例层:应用级业务逻辑(归属领域层)
│ │ ├── get-todos.ts
│ │ ├── create-todo.ts
│ │ ├── update-todo.ts
│ │ └── delete-todo.ts
│ ├── presentation/ # 表示层:UI 与交互
│ │ ├── components/ # 业务组件
│ │ │ └── todo/
│ │ │ ├── TodoForm.tsx
│ │ │ ├── TodoItem.tsx
│ │ │ └── TodoList.tsx
│ │ └── viewmodel/ # 视图模型:连接用例与 UI
│ │ └── todo-viewmodel.ts
│ └── common/ # 通用工具
│ └── api/
│ └── api-client.ts # 统一 API 客户端
三、代码示例(Todo 业务场景)
3.1 领域模型:封装核心业务规则
领域模型是业务逻辑的核心,独立于任何框架,仅封装业务规则
typescript
// domain/model/todo.ts
export interface Todo {
id: string;
title: string;
completed: boolean;
createdAt: Date;
updatedAt: Date;
}
export class TodoModel implements Todo {
constructor(
public id: string,
public title: string,
public completed: boolean,
public createdAt: Date,
public updatedAt: Date
) {
// 业务规则:Todo标题不能为空
if (!title.trim()) throw new Error("Todo标题不能为空");
}
// 业务操作:切换完成状态(自动更新时间)
toggleComplete(): TodoModel {
return new TodoModel(
this.id,
this.title,
!this.completed,
this.createdAt,
new Date()
);
}
// 业务操作:更新标题(自动更新时间)
updateTitle(newTitle: string): TodoModel {
return new TodoModel(
this.id,
newTitle,
this.completed,
this.createdAt,
new Date()
);
}
}
3.2 仓储接口:定义数据操作契约
仓储接口是领域层与数据层的 "契约",定义数据操作的方法,不涉及具体实现
typescript
// domain/repository/todo-repository.ts
import { Todo } from "../model/todo";
export interface TodoRepository {
getTodos(): Promise<Todo[]>;
getTodoById(id: string): Promise<Todo | null>;
createTodo(todo: Omit<Todo, "id" | "createdAt" | "updatedAt">): Promise<Todo>;
updateTodo(todo: Todo): Promise<Todo>;
deleteTodo(id: string): Promise<void>;
}
3.3 通用 API 客户端:统一接口请求
封装 API 请求工具,隔离具体请求库(如 axios)的依赖
typescript
// common/api/api-client.ts
export interface ApiClient {
get<T>(url: string): Promise<T>;
post<T>(url: string, data: any): Promise<T>;
put<T>(url: string, data: any): Promise<T>;
delete(url: string): Promise<void>;
}
// 基于axios的实现
import axios from "axios";
export class AxiosApiClient implements ApiClient {
private instance = axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL });
async get<T>(url: string): Promise<T> {
const res = await this.instance.get(url);
return res.data;
}
async post<T>(url: string, data: any): Promise<T> {
const res = await this.instance.post(url, data);
return res.data;
}
async put<T>(url: string, data: any): Promise<T> {
const res = await this.instance.put(url, data);
return res.data;
}
async delete(url: string): Promise<void> {
await this.instance.delete(url);
}
}
3.4 API 数据源:实现数据格式转换
数据源负责与外部存储(API / 本地存储)交互,并将原始数据转换为领域模型
typescript
// data/datasource/api/entity/todo-api-entity.ts
// API返回的原始数据类型
export interface TodoApiEntity {
id: string;
title: string;
completed: boolean;
created_at: string;
updated_at: string;
}
typescript
// data/datasource/api/todo-api-datasource.ts
import { ApiClient } from "../../../../common/api/api-client";
import { TodoApiEntity } from "./entity/todo-api-entity";
import { Todo, TodoModel } from "../../../../domain/model/todo";
import { TodoRepository } from "../../../../domain/repository/todo-repository";
export class TodoApiDatasource implements TodoRepository {
constructor(private apiClient: ApiClient) {}
// API 数据 → 领域模型
private mapToDomain(entity: TodoApiEntity): Todo {
return new TodoModel(
entity.id,
entity.title,
entity.completed,
new Date(entity.created_at),
new Date(entity.updated_at)
);
}
// 领域模型 → API 入参
private mapToEntity(todo: Omit<Todo, "id" | "createdAt" | "updatedAt">): Omit<TodoApiEntity, "id" | "created_at" | "updated_at"> {
return { title: todo.title, completed: todo.completed };
}
async getTodos(): Promise<Todo[]> {
const entities = await this.apiClient.get<TodoApiEntity[]>("/todos");
return entities.map(this.mapToDomain);
}
async createTodo(todo: Omit<Todo, "id" | "createdAt" | "updatedAt">): Promise<Todo> {
const entity = this.mapToEntity(todo);
const createdEntity = await this.apiClient.post<TodoApiEntity>("/todos", entity);
return this.mapToDomain(createdEntity);
}
// 其他方法(getTodoById/updateTodo/deleteTodo)省略...
}
3.5 仓储实现:关联数据源
仓储实现类负责实例化数据源,对外提供统一的操作入口(符合依赖倒置原则)
typescript
// data/repository/todo-repository-impl.ts
import { TodoRepository } from "../../domain/repository/todo-repository";
import { TodoApiDatasource } from "../datasource/api/todo-api-datasource";
import { AxiosApiClient } from "../../common/api/api-client";
// 可切换为本地存储数据源
// import { TodoLocalDatasource } from "../datasource/local-storage/todo-local-datasource";
export class TodoRepositoryImpl implements TodoRepository {
// 注入数据源(实际项目可通过依赖注入工具管理)
private datasource = new TodoApiDatasource(new AxiosApiClient());
async getTodos(): Promise<Todo[]> {
return this.datasource.getTodos();
}
// 其他方法(createTodo / updateTodo / deleteTodo)省略...
}
3.6 用例:封装应用级业务逻辑
用例对应独立的业务操作,调用仓储接口实现业务流程,不依赖具体数据源
typescript
// usecase/get-todos.ts
import { Todo } from "../domain/model/todo";
import { TodoRepository } from "../domain/repository/todo-repository";
export class GetTodosUseCase {
constructor(private todoRepository: TodoRepository) {}
async execute(): Promise<Todo[]> {
try {
const todos = await this.todoRepository.getTodos();
// 应用级业务逻辑:按创建时间倒序排列
return todos.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
} catch (error) {
console.error("获取Todo列表失败:", error);
throw new Error("无法加载Todo列表,请稍后重试");
}
}
}
typescript
// usecase/create-todo.ts
import { Todo } from "../domain/model/todo";
import { TodoRepository } from "../domain/repository/todo-repository";
export class CreateTodoUseCase {
constructor(private todoRepository: TodoRepository) {}
async execute(title: string): Promise<Todo> {
try {
if (!title.trim()) throw new Error("Todo标题不能为空");
return await this.todoRepository.createTodo({ title, completed: false });
} catch (error) {
console.error("创建Todo失败:", error);
throw error;
}
}
}
3.7 视图模型:连接用例与 UI
视图模型负责管理 UI 状态、调用用例 ,是表示层与领域层的 "衔接器"
typescript
// presentation/viewmodel/todo-viewmodel.ts
import { useState, useEffect, useCallback } from "react";
import { Todo } from "../../domain/model/todo";
import { GetTodosUseCase } from "../../usecase/get-todos";
import { CreateTodoUseCase } from "../../usecase/create-todo";
import { TodoRepositoryImpl } from "../../data/repository/todo-repository-impl";
// 初始化仓储与用例
const todoRepository = new TodoRepositoryImpl();
const getTodosUseCase = new GetTodosUseCase(todoRepository);
const createTodoUseCase = new CreateTodoUseCase(todoRepository);
export function useTodoViewModel() {
const [todos, setTodos] = useState<Todo[]>([]);
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<string | null>(null);
const [newTodoTitle, setNewTodoTitle] = useState<string>("");
// 获取Todo列表
const fetchTodos = useCallback(async () => {
setLoading(true);
setError(null);
try {
const data = await getTodosUseCase.execute();
setTodos(data);
} catch (err) {
setError(err instanceof Error ? err.message : "未知错误");
} finally {
setLoading(false);
}
}, []);
// 创建Todo
const handleCreateTodo = useCallback(async () => {
if (!newTodoTitle.trim()) {
setError("标题不能为空");
return;
}
setLoading(true);
setError(null);
try {
const newTodo = await createTodoUseCase.execute(newTodoTitle);
setTodos(prev => [newTodo, ...prev]);
setNewTodoTitle("");
} catch (err) {
setError(err instanceof Error ? err.message : "创建失败");
} finally {
setLoading(false);
}
}, [newTodoTitle]);
// 初始化加载
useEffect(() => {
fetchTodos();
}, [fetchTodos]);
return {
todos,
loading,
error,
newTodoTitle,
setNewTodoTitle,
handleCreateTodo,
refetch: fetchTodos,
};
}
3.8 视图组件:纯 UI 渲染与交互
视图组件仅负责渲染 UI、触发交互,不包含任何业务逻辑
typescript
// presentation/components/todo/TodoList.tsx
import React from "react";
import { useTodoViewModel } from "../../viewmodel/todo-viewmodel";
import { TodoForm } from "./TodoForm";
import { TodoItem } from "./TodoItem";
export function TodoList() {
const {
todos,
loading,
error,
newTodoTitle,
setNewTodoTitle,
handleCreateTodo,
handleToggleTodo,
handleDeleteTodo,
refetch,
} = useTodoViewModel();
return (
<div style={{ maxWidth: "600px", margin: "20px auto", padding: "16px" }}>
<h2>Todo List</h2>
{error && (
<div style={{ color: "red", margin: "8px 0" }}>
{error}
<button onClick={refetch} style={{ marginLeft: "8px" }}>重试</button>
</div>
)}
<TodoForm
newTodoTitle={newTodoTitle}
setNewTodoTitle={setNewTodoTitle}
onSubmit={handleCreateTodo}
loading={loading}
/>
{loading && todos.length === 0 ? (
<p>加载中...</p>
) : todos.length === 0 ? (
<p>暂无Todo,添加一个吧~</p>
) : (
<ul style={{ listStyle: "none", padding: 0 }}>
{todos.map((todo) => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={handleToggleTodo}
onDelete={handleDeleteTodo}
loading={loading}
/>
))}
</ul>
)}
</div>
);
}
四、架构分析
4.1 代码组织特点
- 视图层 "无逻辑" :组件仅负责渲染与交互触发,业务逻辑全部封装在 ViewModel 中,组件可复用性极高
- 用例层 "业务明确" :每个用例对应一个独立操作(如 GetTodos / CreateTodo),可单独测试、组合实现复杂场景
- 领域层 "独立稳定" :TodoModel 封装核心业务规则,不依赖任何框架,技术栈切换时可直接复用
- 数据层 "可替换" :切换数据源(API → 本地存储)只需修改仓储实现 ,无需改动领域层 / 用例层代码
4.2 架构优势
- 易维护性 :修改 "Todo 标题长度限制" 只需调整 TodoModel ,无需改动 UI / 数据源
- 高可测试性 :领域模型可通过单元测试验证业务规则,用例层可 Mock 仓储接口测试流程
- 多端适配友好 :表示层 可单独适配 Web / 小程序 / 桌面端 ,核心逻辑 100% 复用
- 技术栈无关 :从 React 切换到 Vue 时,仅需重写表示层,业务逻辑完全保留
4.3 架构劣势与应对方案
- 初期复杂度高 :简单项目无需分层;应对:仅在核心业务模块采用,简单模块用传统方式实现
- 模板代码较多 :数据转换、仓储接口存在重复;应对 :用代码生成器(如 Plop)自动生成基础代码
- 学习成本高 :团队需理解分层思想;应对 :编写架构文档、提供示例代码,通过 Code Review 统一规范
五、适用场景
5.1 适合使用的场景
- 企业级后台管理系统 (电商订单管理、CRM):业务复杂、需长期迭代
- 多端适配应用 (Web + 小程序 + Electron):需复用核心逻辑,降低跨端成本
- 高测试要求应用(金融类前端):需保证业务规则正确性
- 大型团队协作项目:需统一规范,降低协作耦合
5.2 不适合使用的场景
- 简单静态页面(个人博客、产品介绍页):业务逻辑少,分层冗余
- 快速原型项目(黑客马拉松作品):需快速迭代,无需长期维护
- 小型个人项目:开发效率优先,无需复杂架构
六、最佳实践建议
- 依赖注入简化实例管理 :用 InversifyJS / tsyringe 管理 Repository、UseCase 实例,避免手动创建的耦合
- 结合状态管理库 :用 Redux / Pinia 将 ViewModel 状态提升为全局状态,适配多组件共享场景
- 渐进式落地:先在核心业务模块采用,验证价值后再扩展,避免一次性重构风险
- 保持分层纯度:禁止跨层调用(如视图层直接调用数据源),严格遵循单向依赖规则
七、总结
Clean Architecture 并非前端项目的 "银弹" ,而是应对复杂业务的高效工具。它通过分层设计将 "稳定的业务逻辑" 与 "多变的技术实现 " 解耦,让前端项目在长期迭代中保持可维护性。
落地时需避免 "为架构而架构":根据项目规模灵活调整粒度,用工具减少重复代码,结合团队技术栈做适配改造 ------ 最终让架构服务于业务,而非约束业务。