前端整洁架构(Clean Architecture)实战解析:从理论到 Todo 项目落地

一、概述

整洁架构 Clean ArchitectureRobert 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 代码组织特点

  1. 视图层 "无逻辑" :组件仅负责渲染与交互触发,业务逻辑全部封装在 ViewModel 中,组件可复用性极高
  2. 用例层 "业务明确" :每个用例对应一个独立操作(如 GetTodos / CreateTodo),可单独测试、组合实现复杂场景
  3. 领域层 "独立稳定"TodoModel 封装核心业务规则,不依赖任何框架,技术栈切换时可直接复用
  4. 数据层 "可替换" :切换数据源(API → 本地存储)只需修改仓储实现 ,无需改动领域层 / 用例层代码

4.2 架构优势

  1. 易维护性 :修改 "Todo 标题长度限制" 只需调整 TodoModel ,无需改动 UI / 数据源
  2. 高可测试性 :领域模型可通过单元测试验证业务规则,用例层可 Mock 仓储接口测试流程
  3. 多端适配友好表示层 可单独适配 Web / 小程序 / 桌面端 ,核心逻辑 100% 复用
  4. 技术栈无关 :从 React 切换到 Vue 时,仅需重写表示层,业务逻辑完全保留

4.3 架构劣势与应对方案

  1. 初期复杂度高 :简单项目无需分层;应对:仅在核心业务模块采用,简单模块用传统方式实现
  2. 模板代码较多 :数据转换、仓储接口存在重复;应对 :用代码生成器(如 Plop)自动生成基础代码
  3. 学习成本高 :团队需理解分层思想;应对 :编写架构文档、提供示例代码,通过 Code Review 统一规范

五、适用场景

5.1 适合使用的场景

  • 企业级后台管理系统 (电商订单管理、CRM):业务复杂、需长期迭代
  • 多端适配应用Web + 小程序 + Electron):需复用核心逻辑,降低跨端成本
  • 高测试要求应用(金融类前端):需保证业务规则正确性
  • 大型团队协作项目:需统一规范,降低协作耦合

5.2 不适合使用的场景

  • 简单静态页面(个人博客、产品介绍页):业务逻辑少,分层冗余
  • 快速原型项目(黑客马拉松作品):需快速迭代,无需长期维护
  • 小型个人项目:开发效率优先,无需复杂架构

六、最佳实践建议

  1. 依赖注入简化实例管理 :用 InversifyJS / tsyringe 管理 Repository、UseCase 实例,避免手动创建的耦合
  2. 结合状态管理库 :用 Redux / PiniaViewModel 状态提升为全局状态,适配多组件共享场景
  3. 渐进式落地:先在核心业务模块采用,验证价值后再扩展,避免一次性重构风险
  4. 保持分层纯度:禁止跨层调用(如视图层直接调用数据源),严格遵循单向依赖规则

七、总结

Clean Architecture 并非前端项目的 "银弹" ,而是应对复杂业务的高效工具。它通过分层设计将 "稳定的业务逻辑" 与 "多变的技术实现 " 解耦,让前端项目在长期迭代中保持可维护性。

落地时需避免 "为架构而架构":根据项目规模灵活调整粒度,用工具减少重复代码,结合团队技术栈做适配改造 ------ 最终让架构服务于业务,而非约束业务。

相关推荐
roman_日积跬步-终至千里3 小时前
【架构实战-Spring】动态数据源切换方案
架构
C澒3 小时前
Remesh 框架详解:基于 CQRS 的前端领域驱动设计方案
前端·架构·前端框架·状态模式
Charlie_lll4 小时前
学习Three.js–雪花
前端·three.js
onebyte8bits4 小时前
前端国际化(i18n)体系设计与工程化落地
前端·国际化·i18n·工程化
晚霞的不甘4 小时前
CANN 编译器深度解析:UB、L1 与 Global Memory 的协同调度机制
java·后端·spring·架构·音视频
C澒4 小时前
前端分层架构实战:DDD 与 Clean Architecture 在大型业务系统中的落地路径与项目实践
前端·架构·系统架构·前端框架
BestSongC4 小时前
行人摔倒检测系统 - 前端文档(1)
前端·人工智能·目标检测
Re.不晚4 小时前
MySQL进阶之战——索引、事务与锁、高可用架构的三重奏
数据库·mysql·架构
松☆5 小时前
深入理解CANN:面向AI加速的异构计算架构
人工智能·架构