引言
在某电商后台管理系统的迭代中,我们曾陷入典型的前端业务膨胀困境:修改 "订单拦截规则" 的状态校验逻辑时,需要同时调整 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 层(纯展示与交互触发)
typescript
<!-- 拦截池列表组件 -->
<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']);
// 仅触发业务操作,不处理逻辑
const handleEdit = (id: string) => emit('openEditModal', id);
// 组件挂载时触发查询
onMounted(() => getRuleList());
</script>
2. 控制层(交互与业务的衔接)
typescript
// 拦截池交互逻辑(Vue Composition API)
import { ref } from 'vue';
import { InterceptionPoolUseCase } from './usecase';
export function useInterceptionPoolController() {
const loading = ref(false);
const usecase = new InterceptionPoolUseCase();
// 调度业务逻辑,处理视图状态
const getRuleList = async () => {
loading.value = true;
await usecase.getList();
loading.value = false;
};
return { loading, getRuleList, ruleList: usecase.ruleList };
}
3. 领域层(核心业务规则)
typescript
// 实体:拦截规则(封装业务规则)
export class InterceptionRule {
constructor(
public id: string,
public name: string,
public condition: string
) {
// 业务规则:规则名称不能为空
if (!name.trim()) throw new Error('规则名称不可为空');
}
}
// 服务:拦截池业务操作
export class InterceptionPoolService {
// 业务逻辑:过滤已过期的规则
filterExpiredRules(rules: InterceptionRule[]): InterceptionRule[] {
return rules.filter(rule => rule.condition.includes('expire:false'));
}
}
4. 基础层(通用技术能力)
typescript
// API请求工具
import axios from 'axios';
export class ApiRepository {
async get<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 组件库,可参考 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 的分层,确保各层职责不越界
bash
├── 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 层设计
typescript
// model/interception-pool.ts
// 1. Entity:后端接口返回的原始类型(与API协议一致)
export interface InterceptionRuleEntity {
rule_id: string; // 后端字段:规则ID
rule_name: string; // 后端字段:规则名称
rule_condition: string;// 后端字段:规则条件
expire_flag: string; // 后端字段:过期标识
}
// 2. Model:前端UI使用的类型(适配组件展示)
export interface InterceptionRuleModel {
id: string; // 前端字段:规则ID(统一命名)
name: string; // 前端字段:规则名称
condition: string; // 前端字段:规则条件
isExpired: boolean; // 前端字段:是否过期(布尔值,便于UI判断)
}
// 3. 数据转换函数(Entity → Model)
export const 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 层设计
typescript
// repository/interception-pool.ts
import axios from 'axios';
import { InterceptionRuleEntity, InterceptionRuleModel, mapRuleEntityToModel } from '../model/interception-pool';
// 接口参数类型
export interface QueryInterceptionRulesParams {
pageNum: number;
pageSize: number;
type?: string; // 规则类型(如 sip 仓/普通仓)
}
export class InterceptionPoolRepository {
// 基础URL
private baseUrl = '/api/interception-rules';
// 查询拦截规则列表
async queryRules(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);
throw new Error('查询拦截规则失败,请重试');
}
}
// 创建拦截规则
async createRule(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 层的 "能力清单",确保所有用例都实现统一的核心方法,避免场景扩展时出现能力缺失
typescript
// usecase/interception-pool.ts
// 对应类图中的「InterceptionPoolUseCase」抽象接口
export interface InterceptionPoolUseCase {
/**
* 处理请求参数(共性/个性参数格式化)
* @param params 前端传入的原始参数
* @returns 格式化后的接口请求参数
*/
processParams(params: any): any;
/**
* 获取拦截规则列表(核心业务操作)
* @param params 前端传入的查询参数
* @returns 处理后的 UI 模型列表
*/
getList(params: any): Promise<InterceptionRuleModel[]>;
}
核心价值 :通过接口约束,保证所有拦截池场景(普通仓 / Sip 仓 / 临时仓)都具备 "参数处理 + 列表查询" 的核心能力,统一业务操作的调用方式
2. 通用 UseCase:封装共性逻辑
DefaultUseCase 对应类图中的通用实现,负责封装所有场景共享的逻辑,避免重复代码
typescript
// 对应类图中的「DefaultUseCase」
export class DefaultInterceptionPoolUseCase implements InterceptionPoolUseCase {
// 对应类图中的「+repository: Repository」:依赖Repository层
protected repository: InterceptionPoolRepository;
constructor() {
this.repository = new InterceptionPoolRepository();
}
/**
* 通用参数处理:格式化分页/通用筛选条件
* 对应类图中的「+processParams」
*/
processParams(params: any): any {
// 共性逻辑:统一分页参数命名(前端page→后端page_num)
return {
page_num: params.page || 1,
page_size: params.pageSize || 10,
keyword: params.keyword || ''
};
}
/**
* 通用列表查询:参数处理→调用接口→数据转换→基础业务逻辑
* 对应类图中的「+getList」
*/
async getList(params: any): Promise<InterceptionRuleModel[]> {
try {
// 步骤1:调用通用参数处理
const formattedParams = this.processParams(params);
// 步骤2:调用Repository层获取接口数据
const ruleModels = await this.repository.queryRules(formattedParams);
// 步骤3:通用业务逻辑:过滤空名称的无效规则
return ruleModels.filter(rule => rule.name.trim());
} catch (error) {
// 共性异常处理:统一业务层面的错误提示
console.error('获取拦截规则列表失败(通用逻辑):', error);
throw new Error('查询规则失败,请检查网络后重试');
}
}
}
共性逻辑封装点
- 参数格式化:统一分页参数、通用筛选条件的处理
- 接口调用 :复用 Repository 层的查询逻辑
- 基础业务规则:过滤无效数据、统一异常提示
3. 扩展 UseCase:实现个性化场景
SipWhUseCase 对应类图中的扩展实现,继承通用 UseCase 的共性逻辑,重写个性化方法 ,适配特定场景(如 Sip 仓)
typescript
// 对应类图中的「SipWhUseCase」
export class SipWhUseCase extends DefaultInterceptionPoolUseCase {
/**
* 重写参数处理:添加 Sip 仓专属筛选条件
* 对应类图中的「+ processParams」(重写)
*/
processParams(params: any): any {
// 复用父类的通用参数处理逻辑
const baseParams = super.processParams(params);
// 个性化逻辑:强制添加「type: 'sip'」的筛选条件
return {
...baseParams,
rule_type: 'sip' // Sip 仓专属参数
};
}
/**
* 重写列表查询:补充 Sip 仓专属业务逻辑
* 对应类图中的「+ getList」(扩展)
*/
async getList(params: any): Promise<InterceptionRuleModel[]> {
try {
// 复用父类的「参数处理 → 接口调用 → 基础过滤」逻辑
const baseRules = await super.getList(params);
// 个性化业务逻辑:过滤 Sip 仓的跨境规则
return baseRules.filter(rule => !rule.condition.includes('cross_border: true'));
} catch (error) {
// 个性化异常提示:区分 Sip 仓场景的错误
console.error('获取 Sip 仓拦截规则失败:', error);
throw new Error('Sip 仓规则查询失败,请联系管理员');
}
}
}
扩展逻辑点
- 重写
processParams:添加 Sip 仓专属参数rule_type: 'sip' - 扩展
getList:在通用逻辑基础上,新增 "过滤跨境规则" 的个性化业务
4. 用例工厂:场景与 UseCase 的映射
为了让上层(控制层)无需关心 UseCase 的实例化细节,通过用例工厂 实现 "场景 → UseCase" 的自动映射
typescript
// usecase/usecase-factory.ts
export class InterceptionPoolUseCaseFactory {
/**
* 根据场景类型创建对应的UseCase实例
* @param scene 业务场景(default/sip)
* @returns 对应场景的UseCase实例
*/
static create(scene: 'default' | 'sip'): InterceptionPoolUseCase {
switch (scene) {
case 'sip':
return new SipWhUseCase(); // 场景对应Sip仓UseCase
default:
return new DefaultInterceptionPoolUseCase(); // 默认对应通用UseCase
}
}
}
4.3.3 实际业务场景中的调用
在控制层(Controller )中,通过用例工厂选择对应场景的 UseCase ,实现业务逻辑的 "按需调用"
typescript
// controller.ts
import { InterceptionPoolUseCaseFactory } from './usecase/usecase-factory';
export function useInterceptionPoolController(scene: 'default' | 'sip' = 'default') {
const loading = ref(false);
// 通过工厂获取对应场景的UseCase实例
const usecase = InterceptionPoolUseCaseFactory.create(scene);
const ruleList = ref<InterceptionRuleModel[]>([]);
const getRuleList = 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 层实现)
typescript
<!-- 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处理后的通用业务Model
const { ruleList } = useInterceptionPoolController('default');
// Presenter层核心职责:Model → View Model(轻量展示适配,嵌入UI层)
// 1. 时间戳格式化:业务Model中是时间戳,View Model中是格式化后的字符串
const viewRuleList = computed(() =>
ruleList.value.map(rule => ({
...rule, // 继承通用业务Model的所有字段
createTimeFormat: new Date(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 工具函数(非物理层)
typescript
// interception-pool/presenter-utils.ts 仅做展示适配的工具函数(Presenter层职责)
import { InterceptionRuleModel } from '../model/interception-pool';
// 定义组件专属的View Model(仅服务于渲染,无任何业务字段)
export interface InterceptionRuleViewModel {
id: string;
name: string;
nameTag: string; // 展示专属:规则名称+过期标签(如"测试规则[已过期]")
conditionDesc: string; // 展示专属:格式化后的规则条件
createTime: string; // 展示专属:格式化后的创建时间
operateBtnStatus: boolean; // 展示专属:操作按钮是否禁用(根据过期状态)
}
// Presenter核心方法:Model → View Model(纯展示适配,无业务逻辑)
export const 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 = new Date(model.createTime).toLocaleString('zh-CN');
// 展示适配4:根据过期状态判断操作按钮是否禁用
const operateBtnStatus = model.isExpired;
return {
id: model.id,
name: model.name,
nameTag,
conditionDesc,
createTime,
operateBtnStatus
};
};
步骤 2:UI 层引入工具函数实现渲染
typescript
<!-- 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 工具)
typescript
// src/common/presenter/format-utils.ts 全局展示适配工具
/**
* 时间戳格式化(全局通用展示适配)
* @param timestamp 时间戳(业务Model通用字段)
* @param format 格式化类型
* @returns 格式化后的时间字符串(View Model展示字段)
*/
export const formatTimestamp = (timestamp: number, format: 'date' | 'datetime' = 'datetime'): string => {
if (!timestamp) return '-';
const date = new Date(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展示字段)
*/
export const 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 率明显下降
该方案的核心价值是 "适配业务复杂度" ------ 既避免了 "照搬后端架构" 的过度设计,又通过分层解耦解决了大型前端系统的维护痛点,为长期迭代提供了坚实的架构基础