前端 Clean Architecture 架构详解:从理论到 Todo 项目落地
一、概述整洁架构Clean Architecture由Robert C. Martin“Uncle Bob”提出是一种以“业务逻辑中心化、外部依赖解耦”为核心的软件架构设计方法。它通过分层设计 单向依赖规则将业务逻辑与框架、UI、数据源等外部元素隔离确保核心逻辑独立于技术实现。在前端应用中这一架构能帮助开发者构建易维护、可测试、能长期迭代的代码结构 —— 尤其适用于业务复杂、多端适配的大型项目比如企业级后台管理系统、跨 Web / 小程序 / 桌面端的应用可通过核心逻辑复用降低跨端开发成本。二、架构设计2.1 核心原则Clean Architecture遵循“内层不依赖外层外层依赖内层”的单向依赖规则前端场景下可适配为更贴合开发习惯的三层核心结构对应经典分层的核心职责前端适配分层对应经典 Clean Architecture 分层核心职责领域层Domain LayerEntities Use Cases核心封装企业级业务规则、定义核心业务模型与操作契约代码独立于任何框架 / 平台接口适配层Interface Adapter LayerInterface Adapters负责数据格式转换如API数据 → 领域模型、封装数据源实现API/ 本地存储是领域层与外部依赖的“桥梁”表示层Presentation LayerFrameworks 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():PromiseTodo[];getTodoById(id:string):PromiseTodo|null;createTodo(todo:OmitTodo,id|createdAt|updatedAt):PromiseTodo;updateTodo(todo:Todo):PromiseTodo;deleteTodo(id:string):Promisevoid;}3.3 通用 API 客户端统一接口请求封装API请求工具隔离具体请求库如axios的依赖// common/api/api-client.tsexportinterfaceApiClient{getT(url:string):PromiseT;postT(url:string,data:any):PromiseT;putT(url:string,data:any):PromiseT;delete(url:string):Promisevoid;}// 基于axios的实现importaxiosfromaxios;exportclassAxiosApiClientimplementsApiClient{privateinstanceaxios.create({baseURL:import.meta.env.VITE_API_BASE_URL});asyncgetT(url:string):PromiseT{constresawaitthis.instance.get(url);returnres.data;}asyncpostT(url:string,data:any):PromiseT{constresawaitthis.instance.post(url,data);returnres.data;}asyncputT(url:string,data:any):PromiseT{constresawaitthis.instance.put(url,data);returnres.data;}asyncdelete(url:string):Promisevoid{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:OmitTodo,id|createdAt|updatedAt):OmitTodoApiEntity,id|created_at|updated_at{return{title:todo.title,completed:todo.completed};}asyncgetTodos():PromiseTodo[]{constentitiesawaitthis.apiClient.getTodoApiEntity[](/todos);returnentities.map(this.mapToDomain);}asynccreateTodo(todo:OmitTodo,id|createdAt|updatedAt):PromiseTodo{constentitythis.mapToEntity(todo);constcreatedEntityawaitthis.apiClient.postTodoApiEntity(/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{// 注入数据源实际项目可通过依赖注入工具管理privatedatasourcenewTodoApiDatasource(newAxiosApiClient());asyncgetTodos():PromiseTodo[]{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():PromiseTodo[]{try{consttodosawaitthis.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):PromiseTodo{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}fromreact;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;// 初始化仓储与用例consttodoRepositorynewTodoRepositoryImpl();constgetTodosUseCasenewGetTodosUseCase(todoRepository);constcreateTodoUseCasenewCreateTodoUseCase(todoRepository);exportfunctionuseTodoViewModel(){const[todos,setTodos]useStateTodo[]([]);const[loading,setLoading]useStateboolean(false);const[error,setError]useStatestring|null(null);const[newTodoTitle,setNewTodoTitle]useStatestring();// 获取Todo列表constfetchTodosuseCallback(async(){setLoading(true);setError(null);try{constdataawaitgetTodosUseCase.execute();setTodos(data);}catch(err){setError(errinstanceofError?err.message:未知错误);}finally{setLoading(false);}},[]);// 创建TodoconsthandleCreateTodouseCallback(async(){if(!newTodoTitle.trim()){setError(标题不能为空);return;}setLoading(true);setError(null);try{constnewTodoawaitcreateTodoUseCase.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.tsximportReactfromreact;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}}h2Todo 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}/{loadingtodos.length0?(p加载中.../p):todos.length0?(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并非前端项目的“银弹”而是应对复杂业务的高效工具。它通过分层设计将“稳定的业务逻辑” 与 “多变的技术实现” 解耦让前端项目在长期迭代中保持可维护性。落地时需避免“为架构而架构”根据项目规模灵活调整粒度用工具减少重复代码结合团队技术栈做适配改造 —— 最终让架构服务于业务而非约束业务。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2412142.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!