前端分层架构实战:DDD 与 Clean Architecture 在大型业务系统中的落地路径与项目实践
引言
在某电商后台管理系统的迭代中,我们曾陷入典型的前端业务膨胀困境:修改 “订单拦截规则” 的状态校验逻辑时,需要同时调整 5 个关联组件的代码 —— 业务逻辑散落在组件的 setup 或 methods 中,耦合严重;后续扩展至小程序端时,核心业务逻辑无法复用,需重新编写 60% 的代码;新成员接手时,需花 1 周才能理清 “拦截规则从查询到展示” 的全链路逻辑。
这些问题的核心是 “业务逻辑与技术实现的耦合”。领域驱动设计(DDD)与整洁架构(Clean Architecture) 为解决这些问题提供了思路 —— 通过分层解耦,将 “稳定的业务规则” 与 “多变的技术工具(框架、UI 组件)” 分离,让前端系统具备长期可维护性与可扩展性。
本文结合实际项目实践,详解这两种架构在前端的落地路径。
一、前端 DDD 分层架构:从理论到实际场景
在前端语境下,DDD 分层架构可映射为更具体的代码组织模式,各层对应前端开发中的实际职责
| 层级 | 前端落地场景 | 核心职责 |
|---|---|---|
| UI 层 | Vue / React 组件、UI 库(如 Element Plus) | 渲染用户界面、响应输入操作,是用户与系统交互的入口与结果载体 |
| 控制层 | 组件交互逻辑(如 Vue 的 Composition API、React 的自定义 Hooks) | 管理事件流、绑定视图与业务模型、解析数据,承担 “用户操作→业务逻辑” 的调度 |
| 领域层 | TypeScript 业务模型、业务函数 | 封装业务规则:用 “实体 / 值对象” 定义业务概念,用 “服务” 实现业务操作 |
| 基础层 | 工具库(axios、localStorage 等) | 提供 API 请求、持久化存储、导出工具等通用技术能力,支撑上层业务 |
各层代码示例
1. UI 层(纯展示与交互触发)
<!-- 拦截池列表组件 --><template><el-table :data="ruleList" border><el-table-column prop="name" label="规则名称"/><el-table-column label="操作"><template #default="scope"><el-button @click="handleEdit(scope.row.id)">编辑</el-button></template></el-table-column></el-table></template><script setup lang="ts">import{ useInterceptionPoolController }from'./controller';const{ ruleList, getRuleList, loading }=useInterceptionPoolController();const emit =defineEmits(['openEditModal']);// 仅触发业务操作,不处理逻辑consthandleEdit=(id:string)=>emit('openEditModal', id);// 组件挂载时触发查询onMounted(()=>getRuleList());</script>2. 控制层(交互与业务的衔接)
// 拦截池交互逻辑(Vue Composition API)import{ ref }from'vue';import{ InterceptionPoolUseCase }from'./usecase';exportfunctionuseInterceptionPoolController(){const loading =ref(false);const usecase =newInterceptionPoolUseCase();// 调度业务逻辑,处理视图状态constgetRuleList=async()=>{ loading.value =true;await usecase.getList(); loading.value =false;};return{ loading, getRuleList, ruleList: usecase.ruleList };}3. 领域层(核心业务规则)
// 实体:拦截规则(封装业务规则)exportclassInterceptionRule{constructor(public id:string,public name:string,public condition:string){// 业务规则:规则名称不能为空if(!name.trim())thrownewError('规则名称不可为空');}}// 服务:拦截池业务操作exportclassInterceptionPoolService{// 业务逻辑:过滤已过期的规则filterExpiredRules(rules: InterceptionRule[]): InterceptionRule[]{return rules.filter(rule => rule.condition.includes('expire:false'));}}4. 基础层(通用技术能力)
// API请求工具import axios from'axios';exportclassApiRepository{asyncget<T>(url:string, params: Record<string,any>):Promise<T>{const res =await axios.get(url,{ params });return res.data;}}二、架构方案选型:平衡成本与收益
针对前端 DDD 的落地方案,结合项目规模和技术栈(Vue + React),做了量化对比:
| 方案类型 | 核心优势 | 核心劣势 | 前端适配性 | 改造成本 |
|---|---|---|---|---|
| Vue / React + Remesh | 遵循 DDD、支持 CQRS / 事件驱动 | 代码繁琐、异步交互复杂、需升级框架 / 适配 | 中(需框架适配) | 高(框架升级 + 思维重构) |
| Vue + BLL 架构 | 事件驱动、无需升级框架 | UI 与业务逻辑易混淆 | 高(无需升级) | 中(需适配事件驱动思维) |
| Vue / React + Clean Architecture | 框架无关、易测试、分层清晰 | 存在模板代码、学习曲线较陡 | 高(轻量适配) | 低(轻量改造,核心逻辑复用) |
最终选型:Vue / React + Clean Architecture —— 既适配当前技术栈,又能以轻量改造成本落地,同时规避长期维护风险
这个方案需接入 React 组件库,可参考 Vue 项目渐进式迁移 React:组件库接入与跨框架协同技术方案
三、Clean Architecture 设计理念
Clean Architecture 核心是 “业务逻辑内聚,外部依赖外移”,其环形分层结构在前端中可映射为更具体的职责,确保核心业务不受技术工具的约束:

这张图的核心规则是 “内层不依赖外层”:从内到外依次是 “Entities(核心业务)→ Use Cases(应用逻辑)→ Repositories / Presenters(接口适配)→ 最外层的 Device / DB / API / UI(框架与工具)”,确保核心业务逻辑不被外部工具绑定
3.1 核心原则
- 框架无关:业务逻辑不依赖 “Vue 的 ref” 或 “React 的 setState” 等,仅在 UI 层使用框架能力
- 可测试:核心业务逻辑(如 InterceptionRule 的名称校验)可脱离组件,用 Jest 独立测试
- 多端 / 存储适配:扩展至小程序时仅需替换 UI 层,核心逻辑 100% 复用;存储方案可从 localStorage 切换为 IndexedDB,无需修改业务代码
3.2 架构层次
| 层次 | 前端实现内容 | 代码示例 |
|---|---|---|
| 实体层(Entities) | TypeScript 业务模型 + 规则 | class InterceptionRule { /* 业务规则校验 */ } |
| 用例层(Use Cases) | 业务操作函数 | async getInterceptionRules() { /* 调用API+业务逻辑 */ } |
| 接口适配器层(Repositories / Presenters) | 数据格式转换、API 封装 | const mapRule = (raw: RawRule) => ({ id: raw.rule_id, name: raw.rule_name }) |
| 框架驱动层(Device / DB / API / UI) | Vue / React 组件、UI 库、axios | <el-table :data="ruleList" /> |
3.3 数据流向
前端请求的全链路流程:
用户操作组件 → Controller(交互调度) → UseCase(业务逻辑) → Repository(API 请求) → 接口数据 → UseCase(业务处理) → UI 组件(渲染) 各层依赖关系遵循 “外层依赖内层”:
View ← Presenter ← UseCase ← Repositories ← Model 四、目录结构设计
以下是 “拦截池” 业务模块的目录结构,严格对应 Clean Architecture 的分层,确保各层职责不越界
├── modules │ ├── exception-flow # 业务场景:异常流程 │ │ ├── interception-pool # 模块名:拦截池 │ │ │ ├── components # UI 层:模块内组件(对应框架驱动层) │ │ │ ├── model # 实体层:业务模型(Entity / Service) │ │ │ │ ├── interception-pool.ts # 拦截规则实体 + 服务 │ │ │ ├── repository # 接口适配器层:API 封装 + 数据转换 │ │ │ │ ├── interception-pool.ts # API 请求方法 │ │ │ │ ├── types.ts # 接口类型定义 │ │ │ ├── usecase # 用例层:业务操作(对应图 2) │ │ │ │ ├── interception-pool.ts # 通用用例 │ │ │ │ ├── interception-pool-for-sip.ts # Sip 仓扩展用例 │ │ │ │ ├── usecase-factory.ts # 用例工厂(映射场景) │ │ │ ├── controller.ts # 控制层:交互逻辑调度 │ │ │ ├── index.vue # 模块入口:组装组件与逻辑 │ │ │ ├── README.md # 模块使用文档4.1 Model 层(实体层)
对应 Clean Architecture 的实体层(Entities),是核心业务规则的载体,区分 “接口实体(Entity)” 和 “UI 模型(Model)” 两类数据类型
- Entity:后端接口返回的原始数据类型,与后端协议强绑定
- Model:前端 UI 组件使用的数据类型,可根据展示需求调整字段格式 / 命名
代码示例:Model 层设计
// model/interception-pool.ts// 1. Entity:后端接口返回的原始类型(与API协议一致)exportinterfaceInterceptionRuleEntity{ rule_id:string;// 后端字段:规则ID rule_name:string;// 后端字段:规则名称 rule_condition:string;// 后端字段:规则条件 expire_flag:string;// 后端字段:过期标识}// 2. Model:前端UI使用的类型(适配组件展示)exportinterfaceInterceptionRuleModel{ id:string;// 前端字段:规则ID(统一命名) name:string;// 前端字段:规则名称 condition:string;// 前端字段:规则条件 isExpired:boolean;// 前端字段:是否过期(布尔值,便于UI判断)}// 3. 数据转换函数(Entity → Model)exportconst mapRuleEntityToModel =(entity: InterceptionRuleEntity): InterceptionRuleModel =>({ id: entity.rule_id, name: entity.rule_name, condition: entity.rule_condition, isExpired: entity.expire_flag ==='true'});核心价值
通过类型分离与转换,隔离后端协议变更对前端 UI 的影响 —— 若后端字段 rule_id 改为 id,仅需修改 mapRuleEntityToModel 函数,无需调整 UI 组件
4.2 Repository 层(接口适配器层)
对应 Clean Architecture 的接口适配器层,是前端与后端 API 的 “桥梁”,核心职责是封装 API 请求逻辑,屏蔽接口细节对上层的影响
- 封装 API 请求方法(GET / POST / PUT / DELETE)
- 处理请求参数格式化(如:分页参数、时间格式)
- 统一处理接口异常(如:401 / 500 错误)
- 转换接口返回数据(Entity → Model)
代码示例:Repository 层设计
// repository/interception-pool.tsimport axios from'axios';import{ InterceptionRuleEntity, InterceptionRuleModel, mapRuleEntityToModel }from'../model/interception-pool';// 接口参数类型exportinterfaceQueryInterceptionRulesParams{ pageNum:number; pageSize:number; type?:string;// 规则类型(如 sip 仓/普通仓)}exportclassInterceptionPoolRepository{// 基础URLprivate baseUrl ='/api/interception-rules';// 查询拦截规则列表asyncqueryRules(params: QueryInterceptionRulesParams):Promise<InterceptionRuleModel[]>{try{// 1. 格式化请求参数(统一分页参数命名)const formattedParams ={ page_num: params.pageNum, page_size: params.pageSize, type: params.type };// 2. 发送API请求const res =await axios.get<InterceptionRuleEntity[]>(this.baseUrl,{ params: formattedParams });// 3. 转换数据格式(Entity → Model)return res.data.map(mapRuleEntityToModel);}catch(error){// 4. 统一异常处理console.error('查询拦截规则失败:', error);thrownewError('查询拦截规则失败,请重试');}}// 创建拦截规则asynccreateRule(rule: Omit<InterceptionRuleModel,'id'>):Promise<void>{// 转换Model → Entity(适配后端接口)const entity ={ rule_name: rule.name, rule_condition: rule.condition, expire_flag: rule.isExpired ?'true':'false'};await axios.post(this.baseUrl, entity);}}核心价值
UseCase 层无需关心 API 的具体路径、参数格式,仅需调用 Repository 的方法,实现 “业务逻辑与接口细节解耦”
4.3 UseCase 层:复用与扩展的设计
对应 Clean Architecture 的用例层,该层采用 “抽象接口 + 继承复用” 的设计模式(符合开闭原则),通过 “定义契约 - 封装共性 - 扩展个性” 的分层逻辑,实现业务逻辑的复用与场景扩展的解耦
4.3.1 类图与设计模式解析

类图对应 “接口抽象 + 继承复用” 的设计,各元素的核心职责如下
InterceptionPoolUseCase(抽象接口):定义拦截池业务的核心能力契约,规范所有用例必须实现的方法DefaultUseCase(通用实现):继承抽象接口,封装所有场景的共性逻辑(如:参数格式化、基础数据查询、通用业务规则)SipWhUseCase(扩展实现):继承通用实现,重写个性化逻辑(如:Sip 仓的特殊参数、专属业务规则)
4.3.2 分层设计细节
1. 抽象接口:定义核心能力契约
抽象接口是 UseCase 层的 “能力清单”,确保所有用例都实现统一的核心方法,避免场景扩展时出现能力缺失
// usecase/interception-pool.ts// 对应类图中的「InterceptionPoolUseCase」抽象接口exportinterfaceInterceptionPoolUseCase{/** * 处理请求参数(共性/个性参数格式化) * @param params 前端传入的原始参数 * @returns 格式化后的接口请求参数 */processParams(params:any):any;/** * 获取拦截规则列表(核心业务操作) * @param params 前端传入的查询参数 * @returns 处理后的 UI 模型列表 */getList(params:any):Promise<InterceptionRuleModel[]>;}核心价值:通过接口约束,保证所有拦截池场景(普通仓 / Sip 仓 / 临时仓)都具备 “参数处理 + 列表查询” 的核心能力,统一业务操作的调用方式
2. 通用 UseCase:封装共性逻辑
DefaultUseCase 对应类图中的通用实现,负责封装所有场景共享的逻辑,避免重复代码
// 对应类图中的「DefaultUseCase」exportclassDefaultInterceptionPoolUseCaseimplementsInterceptionPoolUseCase{// 对应类图中的「+repository: Repository」:依赖Repository层protected repository: InterceptionPoolRepository;constructor(){this.repository =newInterceptionPoolRepository();}/** * 通用参数处理:格式化分页/通用筛选条件 * 对应类图中的「+processParams」 */processParams(params:any):any{// 共性逻辑:统一分页参数命名(前端page→后端page_num)return{ page_num: params.page ||1, page_size: params.pageSize ||10, keyword: params.keyword ||''};}/** * 通用列表查询:参数处理→调用接口→数据转换→基础业务逻辑 * 对应类图中的「+getList」 */asyncgetList(params:any):Promise<InterceptionRuleModel[]>{try{// 步骤1:调用通用参数处理const formattedParams =this.processParams(params);// 步骤2:调用Repository层获取接口数据const ruleModels =awaitthis.repository.queryRules(formattedParams);// 步骤3:通用业务逻辑:过滤空名称的无效规则return ruleModels.filter(rule => rule.name.trim());}catch(error){// 共性异常处理:统一业务层面的错误提示console.error('获取拦截规则列表失败(通用逻辑):', error);thrownewError('查询规则失败,请检查网络后重试');}}}共性逻辑封装点
- 参数格式化:统一分页参数、通用筛选条件的处理
- 接口调用:复用 Repository 层的查询逻辑
- 基础业务规则:过滤无效数据、统一异常提示
3. 扩展 UseCase:实现个性化场景
SipWhUseCase 对应类图中的扩展实现,继承通用 UseCase 的共性逻辑,重写个性化方法,适配特定场景(如 Sip 仓)
// 对应类图中的「SipWhUseCase」exportclassSipWhUseCaseextendsDefaultInterceptionPoolUseCase{/** * 重写参数处理:添加 Sip 仓专属筛选条件 * 对应类图中的「+ processParams」(重写) */processParams(params:any):any{// 复用父类的通用参数处理逻辑const baseParams =super.processParams(params);// 个性化逻辑:强制添加「type: 'sip'」的筛选条件return{...baseParams, rule_type:'sip'// Sip 仓专属参数};}/** * 重写列表查询:补充 Sip 仓专属业务逻辑 * 对应类图中的「+ getList」(扩展) */asyncgetList(params:any):Promise<InterceptionRuleModel[]>{try{// 复用父类的「参数处理 → 接口调用 → 基础过滤」逻辑const baseRules =awaitsuper.getList(params);// 个性化业务逻辑:过滤 Sip 仓的跨境规则return baseRules.filter(rule =>!rule.condition.includes('cross_border: true'));}catch(error){// 个性化异常提示:区分 Sip 仓场景的错误console.error('获取 Sip 仓拦截规则失败:', error);thrownewError('Sip 仓规则查询失败,请联系管理员');}}}扩展逻辑点
- 重写
processParams:添加 Sip 仓专属参数rule_type: 'sip' - 扩展
getList:在通用逻辑基础上,新增 “过滤跨境规则” 的个性化业务
4. 用例工厂:场景与 UseCase 的映射
为了让上层(控制层)无需关心 UseCase 的实例化细节,通过用例工厂实现 “场景 → UseCase” 的自动映射
// usecase/usecase-factory.tsexportclassInterceptionPoolUseCaseFactory{/** * 根据场景类型创建对应的UseCase实例 * @param scene 业务场景(default/sip) * @returns 对应场景的UseCase实例 */staticcreate(scene:'default'|'sip'): InterceptionPoolUseCase {switch(scene){case'sip':returnnewSipWhUseCase();// 场景对应Sip仓UseCasedefault:returnnewDefaultInterceptionPoolUseCase();// 默认对应通用UseCase}}}4.3.3 实际业务场景中的调用
在控制层(Controller)中,通过用例工厂选择对应场景的 UseCase,实现业务逻辑的 “按需调用”
// controller.tsimport{ InterceptionPoolUseCaseFactory }from'./usecase/usecase-factory';exportfunctionuseInterceptionPoolController(scene:'default'|'sip'='default'){const loading =ref(false);// 通过工厂获取对应场景的UseCase实例const usecase = InterceptionPoolUseCaseFactory.create(scene);const ruleList =ref<InterceptionRuleModel[]>([]);constgetRuleList=async(params:any)=>{ loading.value =true;try{// 调用UseCase的getList方法(不同场景自动适配逻辑) ruleList.value =await usecase.getList(params);}finally{ loading.value =false;}};return{ loading, ruleList, getRuleList };}场景调用示例
- 普通仓页面:
useInterceptionPoolController('default')→ 调用通用 UseCase - Sip 仓页面:
useInterceptionPoolController('sip')→ 调用 Sip 仓扩展 UseCase
4.3.4 设计核心价值
这种 “抽象接口 + 通用继承 + 扩展重写” 的设计,完美适配大型前端系统的业务迭代需求
- 高复用性:共性逻辑(参数格式化、基础查询)仅需写一次,所有场景复用
- 易扩展性:新增场景(如 “临时拦截池”)时,只需新建
TempInterceptionPoolUseCase继承DefaultUseCase,重写个性化方法即可 - 可维护性:业务逻辑分层清晰,通用逻辑的修改(如分页参数变更)仅需改
DefaultUseCase,就会同步到所有扩展场景 - 符合开闭原则:扩展新场景时 “不修改原有代码,只新增代码”,降低迭代风险
4.4 Presenter 层:前端的简化实现
Presenter 层是 Clean Architecture 中领域层与 UI 层的专属展示适配器,核心定位是做 “业务数据到展示数据” 的最后一步转换,但前端因组件化、展示逻辑与视图强耦合的特性,未单独抽离物理层做实现,而是采用 “职责保留、逻辑分散” 的轻量化落地方式。
本节重点明确 Presenter 层的原生核心职责、与 Repository 层的本质区别,并详解前端场景下的简化实现方案。
4.4.1 原生定位与前端简化的核心原因
1. Clean Architecture 中的原生定位
在 Clean Architecture 环形分层中,Presenter 层属于接口适配器层的展示侧实现,位于 UseCase 层与 UI 层之间,核心职责是将 UseCase 层处理后的通用业务 Model 转换为完全贴合 UI 展示的 View Model,并屏蔽所有业务逻辑对 UI 层的影响,是纯展示维度的适配层
2. 前端未单独抽离实现的核心原因
结合前端组件化开发的特性,单独创建 Presenter 物理层(如新建 presenter 文件夹)会造成层级冗余、逻辑跳转成本高,因此采用轻量化实现,核心原因有三点
- 展示逻辑与 UI 强耦合:前端展示适配不仅是字段转换,还包含 “根据字段判断样式 / 显隐 / 按钮状态” 等与组件紧密绑定的逻辑,抽离后需频繁传参,降低开发效率
- 数据适配粒度差异:后端接口到前端通用 Model 的转换是全局统一的结构适配,而 Model 到 View Model 的转换是组件专属的展示适配,分散在组件内更贴合前端开发习惯
- 避免过度设计:多数中大型前端项目中,展示适配逻辑轻量且分散,单独抽离层会增加模板代码,违背 “轻量改造” 的架构初衷
4.4.2 与 Repository 层(接口适配器层)的核心区别
Presenter 层与 Repository 层虽同属适配器范畴,均承担 “数据转换” 职责,但二者的适配目标、转换阶段、职责边界有本质区别,也是前端架构中需明确的核心分层原则,具体区别如下表所示
| 对比维度 | 4.2 Repository 层(接口适配器层) | 4.4 Presenter 层(展示适配器层) |
|---|---|---|
| 核心适配目标 | 后端接口原始数据 → 前端通用业务 Model | 前端通用业务 Model → 前端组件专属 View Model |
| 数据转换阶段 | “接口请求后,业务逻辑处理前” 的转换 | “业务逻辑处理后,UI 渲染前” 的最终转换 |
| 数据处理维度 | 结构 / 命名适配(如后端 rule_id → 前端 id)、数据类型转换(如字符串标识 → 布尔值)、屏蔽后端协议差异 | 展示格式适配(如时间戳 → YYYY-MM-DD)、展示字段拼接(如姓名 + 工号)、贴合 UI 组件的个性化数据处理 |
| 职责边界 | 聚焦 “前端与后端的接口通信解耦”,转换后的数据可在全项目多组件复用 | 聚焦 “业务逻辑与 UI 展示的解耦”,转换后的数据仅在当前组件 / 页面复用 |
| 依赖关系 | 依赖后端接口协议,向上为 UseCase 层提供统一数据 | 依赖前端 UI 组件需求,向下从 UseCase 层获取业务数据 |
| 异常处理 | 包含接口请求异常、数据格式异常的统一处理 | 无异常处理,仅做纯数据格式 / 展示逻辑的转换 |
核心区分
- Repository 层解决 “后端数据怎么适配前端业务逻辑” 的问题,转换后的 Model 是前端业务层的通用数据
- Presenter 层解决 “前端业务数据怎么适配 UI 组件展示” 的问题,转换后的 View Model 是仅服务于渲染的专属数据
4.4.3 前端简化落地方案(职责保留 + 逻辑分散)
前端未单独抽离 Presenter 物理层,但完整保留其 “展示适配、解耦业务与 UI” 的核心职责,采用 “轻量工具函数 + 逻辑分散至对应层” 的落地方式,根据展示适配的复杂度,分为三种实现场景,均以 “拦截池” 业务为例做代码示例
场景 1:轻量展示适配(主流)→ 直接嵌入 UI 层 / 控制层
适用于单组件专属、逻辑简单的展示适配(如字段格式化、简单状态判断),直接在 UI 层(Vue / React 组件)或控制层中实现,是前端最常用的方式,代码示例(Vue 组件 UI 层实现)
<!-- components/InterceptionRuleList.vue 拦截池规则列表组件 --><template><el-table :data="viewRuleList" border stripe><el-table-column prop="id" label="规则ID" width="100"/><el-table-column prop="name" label="规则名称"><!-- 展示适配:根据是否过期标红 --><template #default="scope"><span :class="{ 'text-red-500': scope.row.isExpired }">{{ scope.row.name }}</span></template></el-table-column><!-- 展示适配:将原始条件字符串格式化为易读文本 --><el-table-column label="规则条件"><template #default="scope">{{formatCondition(scope.row.condition)}}</template></el-table-column><el-table-column prop="createTimeFormat" label="创建时间" width="180"/></el-table></template><script setup lang="ts">import{ ref, computed }from'vue';import{ useInterceptionPoolController }from'../controller';// 从控制层获取UseCase处理后的通用业务Modelconst{ ruleList }=useInterceptionPoolController('default');// Presenter层核心职责:Model → View Model(轻量展示适配,嵌入UI层)// 1. 时间戳格式化:业务Model中是时间戳,View Model中是格式化后的字符串const viewRuleList =computed(()=> ruleList.value.map(rule =>({...rule,// 继承通用业务Model的所有字段 createTimeFormat:newDate(rule.createTime).toLocaleString('zh-CN')// 展示专属字段})));// 2. 规则条件格式化:将后端原始字符串转换为易读文本(纯展示逻辑,与业务无关)const formatCondition =(condition:string):string=>{if(!condition)return'无规则条件';// 仅做展示格式转换,不涉及任何业务逻辑判断return condition.replace(/&/g,',').replace(/=/g,':');};</script><style scoped>.text-red-500{ color: #ef4444;}</style>场景 2:复杂展示适配 → 抽离独立 Presenter 工具函数
适用于适配逻辑复杂、需多次复用的场景(如多组件展示同一类数据、复杂的展示字段拼接 / 计算),不单独建层,而是在对应模块下创建轻量 Presenter 工具函数,实现展示适配逻辑的复用
步骤 1:创建模块内 Presenter 工具函数(非物理层)
// interception-pool/presenter-utils.ts 仅做展示适配的工具函数(Presenter层职责)import{ InterceptionRuleModel }from'../model/interception-pool';// 定义组件专属的View Model(仅服务于渲染,无任何业务字段)exportinterfaceInterceptionRuleViewModel{ id:string; name:string; nameTag:string;// 展示专属:规则名称+过期标签(如“测试规则[已过期]”) conditionDesc:string;// 展示专属:格式化后的规则条件 createTime:string;// 展示专属:格式化后的创建时间 operateBtnStatus:boolean;// 展示专属:操作按钮是否禁用(根据过期状态)}// Presenter核心方法:Model → View Model(纯展示适配,无业务逻辑)exportconst mapRuleModelToViewModel =(model: InterceptionRuleModel): InterceptionRuleViewModel =>{// 展示适配1:名称拼接过期标签const nameTag = model.isExpired ?`${model.name}[已过期]`: model.name;// 展示适配2:规则条件格式化const conditionDesc = model.condition ? model.condition.replace(/&/g,',').replace(/=/g,':'):'无规则条件';// 展示适配3:格式化创建时间const createTime =newDate(model.createTime).toLocaleString('zh-CN');// 展示适配4:根据过期状态判断操作按钮是否禁用const operateBtnStatus = model.isExpired;return{ id: model.id, name: model.name, nameTag, conditionDesc, createTime, operateBtnStatus };};步骤 2:UI 层引入工具函数实现渲染
<!-- components/InterceptionRuleList.vue --><template><el-table :data="viewRuleList" border stripe><el-table-column prop="nameTag" label="规则名称"/><el-table-column prop="conditionDesc" label="规则条件"/><el-table-column prop="createTime" label="创建时间"/><el-table-column label="操作"><template #default="scope"><el-button :disabled="scope.row.operateBtnStatus"@click="handleEdit(scope.row.id)">编辑</el-button></template></el-table-column></el-table></template><script setup lang="ts">import{ ref, computed }from'vue';import{ useInterceptionPoolController }from'../controller';// 引入Presenter工具函数import{ mapRuleModelToViewModel, InterceptionRuleViewModel }from'./presenter-utils';const{ ruleList }=useInterceptionPoolController('default');// 统一做Model → View Model的转换const viewRuleList =computed<InterceptionRuleViewModel[]>(()=> ruleList.value.map(model =>mapRuleModelToViewModel(model)));</script>步骤 3:跨模块通用展示适配 → 抽离公共 Presenter 工具
适用于全项目多模块复用的展示适配逻辑(如时间格式化、状态码转中文、金额单位转换),可在src/common/ 下创建公共 Presenter 工具,实现全局复用,本质仍是无物理层的工具化实现
代码示例(全局公共 Presenter 工具)
// src/common/presenter/format-utils.ts 全局展示适配工具/** * 时间戳格式化(全局通用展示适配) * @param timestamp 时间戳(业务Model通用字段) * @param format 格式化类型 * @returns 格式化后的时间字符串(View Model展示字段) */exportconst formatTimestamp =(timestamp:number, format:'date'|'datetime'='datetime'):string=>{if(!timestamp)return'-';const date =newDate(timestamp);const y = date.getFullYear();const m =(date.getMonth()+1).toString().padStart(2,'0');const d = date.getDate().toString().padStart(2,'0');const h = date.getHours().toString().padStart(2,'0');const min = date.getMinutes().toString().padStart(2,'0');return format ==='date'?`${y}-${m}-${d}`:`${y}-${m}-${d}${h}:${min}`;};/** * 布尔值状态转中文(全局通用展示适配) * @param status 布尔值(业务Model通用字段) * @param trueText 为true时的展示文本 * @param falseText 为false时的展示文本 * @returns 中文状态(View Model展示字段) */exportconst formatBoolStatus =(status:boolean, trueText:string, falseText:string):string=>{return status ? trueText : falseText;};4.4.4 简化实现的核心原则与价值
1. 核心原则:保留职责,不硬套层级
前端简化实现 Presenter 层的核心是 “不追求物理层的抽离,只保证职责的独立与解耦”,需遵循三个原则
- 展示适配与业务逻辑完全分离:Presenter 层的所有代码仅做展示格式转换,不包含任何业务判断 / 规则处理(如不能在 Presenter 中过滤 “过期规则”,该逻辑属于 UseCase 层业务逻辑)
- 适配逻辑贴近 UI:展示适配的代码尽可能在 “使用它的 UI 层 / 组件” 附近,减少跨层跳转的维护成本
- 单一职责:每个 Presenter 工具函数仅负责一类数据的展示转换,避免逻辑混杂
2. 核心价值
即使未单独抽离物理层,Presenter 层的简化实现仍能发挥 Clean Architecture 赋予的核心价值
- 解耦业务与展示:业务逻辑(UseCase 层)仅维护通用 Model,无需关心 UI 如何展示,UI 层修改展示格式时,不影响任何业务代码
- 降低维护成本:展示适配逻辑集中管理(或贴近 UI),修改展示需求(如时间格式从 YYYY-MM-DD 改为 MM/DD/YYYY)时,仅需调整 Presenter 适配逻辑,不涉及组件其他代码
- 提升组件复用性:通用业务 Model 可通过不同的 Presenter 适配逻辑,在不同组件中展示为不同的 View Model,实现 “一份业务数据,多端 / 多组件不同展示”
五、总结
通过 Vue / React + Clean Architecture 方案落地实现了
- 业务 - 框架解耦:实现核心业务逻辑与 Vue / React 框架的解耦,确保技术栈切换时实现 100% 代码复用
- 维护效率提升:业务规则变更仅需修改领域层代码,维护时间大幅缩短
- 跨端复用率:PC 后台的拦截池逻辑在小程序中 100% 复用,新终端开发周期显著压缩
- 测试覆盖率:核心业务逻辑单元测试覆盖率提高,线上 bug 率明显下降
该方案的核心价值是 “适配业务复杂度”—— 既避免了 “照搬后端架构” 的过度设计,又通过分层解耦解决了大型前端系统的维护痛点,为长期迭代提供了坚实的架构基础