当前位置: 首页 > news >正文

前端 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 领域模型:封装核心业务规则

领域模型是业务逻辑的核心,独立于任何框架,仅封装业务规则

// domain/model/todo.tsexportinterfaceTodo{id:string;title:string;completed:boolean;createdAt:Date;updatedAt:Date;}exportclassTodoModelimplementsTodo{constructor(publicid:string,publictitle:string,publiccompleted:boolean,publiccreatedAt:Date,publicupdatedAt:Date){// 业务规则:Todo标题不能为空if(!title.trim())thrownewError("Todo标题不能为空");}// 业务操作:切换完成状态(自动更新时间)toggleComplete():TodoModel{returnnewTodoModel(this.id,this.title,!this.completed,this.createdAt,newDate());}// 业务操作:更新标题(自动更新时间)updateTitle(newTitle:string):TodoModel{returnnewTodoModel(this.id,newTitle,this.completed,this.createdAt,newDate());}}

3.2 仓储接口:定义数据操作契约

仓储接口是领域层与数据层的 “契约”,定义数据操作的方法,不涉及具体实现

// domain/repository/todo-repository.tsimport{Todo}from"../model/todo";exportinterfaceTodoRepository{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)的依赖

// common/api/api-client.tsexportinterfaceApiClient{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的实现importaxiosfrom"axios";exportclassAxiosApiClientimplementsApiClient{privateinstance=axios.create({baseURL:import.meta.env.VITE_API_BASE_URL});asyncget<T>(url:string):Promise<T>{constres=awaitthis.instance.get(url);returnres.data;}asyncpost<T>(url:string,data:any):Promise<T>{constres=awaitthis.instance.post(url,data);returnres.data;}asyncput<T>(url:string,data:any):Promise<T>{constres=awaitthis.instance.put(url,data);returnres.data;}asyncdelete(url:string):Promise<void>{awaitthis.instance.delete(url);}}

3.4 API 数据源:实现数据格式转换

数据源负责与外部存储(API / 本地存储)交互,并将原始数据转换为领域模型

// data/datasource/api/entity/todo-api-entity.ts// API返回的原始数据类型exportinterfaceTodoApiEntity{id:string;title:string;completed:boolean;created_at:string;updated_at:string;}
// data/datasource/api/todo-api-datasource.tsimport{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";exportclassTodoApiDatasourceimplementsTodoRepository{constructor(privateapiClient:ApiClient){}// API 数据 → 领域模型privatemapToDomain(entity:TodoApiEntity):Todo{returnnewTodoModel(entity.id,entity.title,entity.completed,newDate(entity.created_at),newDate(entity.updated_at));}// 领域模型 → API 入参privatemapToEntity(todo:Omit<Todo,"id"|"createdAt"|"updatedAt">):Omit<TodoApiEntity,"id"|"created_at"|"updated_at">{return{title:todo.title,completed:todo.completed};}asyncgetTodos():Promise<Todo[]>{constentities=awaitthis.apiClient.get<TodoApiEntity[]>("/todos");returnentities.map(this.mapToDomain);}asynccreateTodo(todo:Omit<Todo,"id"|"createdAt"|"updatedAt">):Promise<Todo>{constentity=this.mapToEntity(todo);constcreatedEntity=awaitthis.apiClient.post<TodoApiEntity>("/todos",entity);returnthis.mapToDomain(createdEntity);}// 其他方法(getTodoById/updateTodo/deleteTodo)省略...}

3.5 仓储实现:关联数据源

仓储实现类负责实例化数据源,对外提供统一的操作入口(符合依赖倒置原则)

// data/repository/todo-repository-impl.tsimport{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";exportclassTodoRepositoryImplimplementsTodoRepository{// 注入数据源(实际项目可通过依赖注入工具管理)privatedatasource=newTodoApiDatasource(newAxiosApiClient());asyncgetTodos():Promise<Todo[]>{returnthis.datasource.getTodos();}// 其他方法(createTodo / updateTodo / deleteTodo)省略...}

3.6 用例:封装应用级业务逻辑

用例对应独立的业务操作,调用仓储接口实现业务流程,不依赖具体数据源

// usecase/get-todos.tsimport{Todo}from"../domain/model/todo";import{TodoRepository}from"../domain/repository/todo-repository";exportclassGetTodosUseCase{constructor(privatetodoRepository:TodoRepository){}asyncexecute():Promise<Todo[]>{try{consttodos=awaitthis.todoRepository.getTodos();// 应用级业务逻辑:按创建时间倒序排列returntodos.sort((a,b)=>b.createdAt.getTime()-a.createdAt.getTime());}catch(error){console.error("获取Todo列表失败:",error);thrownewError("无法加载Todo列表,请稍后重试");}}}
// usecase/create-todo.tsimport{Todo}from"../domain/model/todo";import{TodoRepository}from"../domain/repository/todo-repository";exportclassCreateTodoUseCase{constructor(privatetodoRepository:TodoRepository){}asyncexecute(title:string):Promise<Todo>{try{if(!title.trim())thrownewError("Todo标题不能为空");returnawaitthis.todoRepository.createTodo({title,completed:false});}catch(error){console.error("创建Todo失败:",error);throwerror;}}}

3.7 视图模型:连接用例与 UI

视图模型负责管理 UI 状态、调用用例,是表示层与领域层的 “衔接器”

// presentation/viewmodel/todo-viewmodel.tsimport{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";// 初始化仓储与用例consttodoRepository=newTodoRepositoryImpl();constgetTodosUseCase=newGetTodosUseCase(todoRepository);constcreateTodoUseCase=newCreateTodoUseCase(todoRepository);exportfunctionuseTodoViewModel(){const[todos,setTodos]=useState<Todo[]>([]);const[loading,setLoading]=useState<boolean>(false);const[error,setError]=useState<string|null>(null);const[newTodoTitle,setNewTodoTitle]=useState<string>("");// 获取Todo列表constfetchTodos=useCallback(async()=>{setLoading(true);setError(null);try{constdata=awaitgetTodosUseCase.execute();setTodos(data);}catch(err){setError(errinstanceofError?err.message:"未知错误");}finally{setLoading(false);}},[]);// 创建TodoconsthandleCreateTodo=useCallback(async()=>{if(!newTodoTitle.trim()){setError("标题不能为空");return;}setLoading(true);setError(null);try{constnewTodo=awaitcreateTodoUseCase.execute(newTodoTitle);setTodos(prev=>[newTodo,...prev]);setNewTodoTitle("");}catch(err){setError(errinstanceofError?err.message:"创建失败");}finally{setLoading(false);}},[newTodoTitle]);// 初始化加载useEffect(()=>{fetchTodos();},[fetchTodos]);return{todos,loading,error,newTodoTitle,setNewTodoTitle,handleCreateTodo,refetch:fetchTodos,};}

3.8 视图组件:纯 UI 渲染与交互

视图组件仅负责渲染UI、触发交互,不包含任何业务逻辑

// presentation/components/todo/TodoList.tsximportReactfrom"react";import{useTodoViewModel}from"../../viewmodel/todo-viewmodel";import{TodoForm}from"./TodoForm";import{TodoItem}from"./TodoItem";exportfunctionTodoList(){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并非前端项目的“银弹”,而是应对复杂业务的高效工具。它通过分层设计将“稳定的业务逻辑” 与 “多变的技术实现” 解耦,让前端项目在长期迭代中保持可维护性。
落地时需避免“为架构而架构”:根据项目规模灵活调整粒度,用工具减少重复代码,结合团队技术栈做适配改造 —— 最终让架构服务于业务,而非约束业务。

http://www.jsqmd.com/news/478193/

相关文章:

  • 如何用Material-UI打造专业级海洋数据监测界面:从入门到精通
  • Kubernetes集群优化利器:Descheduler深度使用指南
  • 计算机原理
  • wan2.1-vae生产环境监控:Prometheus+Grafana搭建GPU温度/显存/请求延迟看板
  • 5分钟从零搭建LLM应用:Chainlit可视化低代码开发全攻略
  • 详解模型训练原理(梯度下降法)
  • 上海宠物口腔溃疡诊疗医生选择需要注意什么,猫咪牙结石/猫咪洗牙/狗狗拔牙/宠物口腔溃疡诊疗,宠物口腔溃疡诊疗医生怎么选择 - 品牌推荐师
  • 造相-Z-Image-Turbo LoRA多风格生成:古风仕女/现代都市/赛博朋克人像效果展示
  • 如何在NVIDIA Jetson平台快速部署Intel RealSense深度相机:完整实战指南
  • lychee-rerank-mm效果实测:中英文混合查询词下模型语义理解能力验证
  • MGeo中文地址解析实战:地址文本脱敏(门牌号掩码/敏感词过滤)
  • GLM-4-9B-Chat-1M镜像价值:开源可审计+1M上下文+多语言+Function Call全栈支持
  • GLM-4v-9b保姆级教程:WebUI中上传多图+跨图引用问答实操演示
  • 分布式理论
  • 圣女司幼幽-造相Z-Turbo提示词迭代方法论:从初稿→优化→定稿的5轮打磨流程
  • 电商供应链履约中台架构与业务全流程解析
  • 福建猫咪绝育哪里好?这些服务周到的专家可参考,宠物眼科/狗狗青光眼引流阀手术/猫咪义眼植入,宠物绝育专家推荐排行榜单 - 品牌推荐师
  • Qwen3-TTS-Tokenizer-12Hz开源大模型教程:651MB模型文件完整性校验SHA256方法
  • 题解:洛谷 B3835 [GESP202303 一级] 每月天数
  • IE浏览器强势回归,极客私藏ie下载站亲测有效
  • PyTorch 深度学习开发 常见疑难报错与解决方案汇总
  • Qwen3-ForcedAligner技术精讲:清音刻墨对齐算法在低信噪比下的鲁棒性设计
  • CasRel关系抽取模型效果展示:学术论文参考文献中‘作者-引用-论文’关系网络构建
  • Python 潮流周刊#142:Python 性能优化的进阶之路
  • InstructPix2Pix惊艳案例:‘Add vintage film effect’胶片滤镜生成效果
  • 前端技术核心领域与实践方向
  • 探究Redis + Caffeine两级缓存架构
  • AIGlasses_for_navigation部署教程:华为昇腾910B适配AscendCL加速指南
  • 灵感画廊入门必看:SDXL 1.0提示词工程从‘指令式’到‘文学式’跃迁
  • MusePublic Art Studio实操手册:从输入描述到保存高清作品完整流程