跳到主要内容前端分层架构实战:DDD 与 Clean Architecture 落地实践 | 极客日志TypeScript大前端
前端分层架构实战:DDD 与 Clean Architecture 落地实践
引言 在某电商后台管理系统的迭代中,我们曾陷入典型的前端业务膨胀困境:修改'订单拦截规则'的状态校验逻辑时,需要同时调整 5 个关联组件的代码 —— 业务逻辑散落在组件的 setup 或 methods 中,耦合严重;后续扩展至小程序端时,核心业务逻辑无法复用,需重新编写 60% 的代码;新成员接手时,需花 1 周才能理清'拦截规则从查询到展示'的全链路逻辑。 这些问题的核心是'业务逻辑与技术实现…
人间过客32K 浏览 引言
在某电商后台管理系统的迭代中,我们曾陷入典型的前端业务膨胀困境:修改'订单拦截规则'的状态校验逻辑时,需要同时调整 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']);
// 仅触发业务操作,不处理逻辑
const handleEdit = (id: string) => emit('openEditModal', id);
// 组件挂载时触发查询
onMounted(() => getRuleList());
</script>
2. 控制层(交互与业务的衔接)
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. 领域层(核心业务规则)
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. 基础层(通用技术能力)
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 —— 既适配当前技术栈,又能以轻量改造成本落地,同时规避长期维护风险。
三、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
│ │ ├── model
│ │ │ └── interception-pool.ts
│ │ ├── repository
│ │ │ ├── interception-pool.ts
│ │ │ └── types.ts
│ │ ├── usecase
│ │ │ ├── interception-pool.ts
│ │ │ ├── interception-pool-for-sip.ts
│ │ │ └── usecase-factory.ts
│ │ ├── controller.ts
│ │ ├── index.vue
│ │ └── README.md
4.1 Model 层(实体层)
对应 Clean Architecture 的实体层(Entities),是核心业务规则的载体,区分'接口实体(Entity)'和'UI 模型(Model)'两类数据类型
- Entity:后端接口返回的原始数据类型,与后端协议强绑定
- Model:前端 UI 组件使用的数据类型,可根据展示需求调整字段格式 / 命名
代码示例:Model 层设计
export interface InterceptionRuleEntity {
rule_id: string;
rule_name: string;
rule_condition: string;
expire_flag: string;
}
export interface InterceptionRuleModel {
id: string;
name: string;
condition: string;
isExpired: boolean;
}
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 层设计
import axios from 'axios';
import { InterceptionRuleEntity, InterceptionRuleModel, mapRuleEntityToModel } from '../model/interception-pool';
export interface QueryInterceptionRulesParams {
pageNum: number;
pageSize: number;
type?: string;
}
export class InterceptionPoolRepository {
private baseUrl = '/api/interception-rules';
async queryRules(params: QueryInterceptionRulesParams): Promise<InterceptionRuleModel[]> {
try {
const formattedParams = {
page_num: params.pageNum,
page_size: params.pageSize,
type: params.type
};
const res = await axios.get<InterceptionRuleEntity[]>(this.baseUrl, { params: formattedParams });
return res.data.map(mapRuleEntityToModel);
} catch (error) {
console.error('查询拦截规则失败:', error);
throw new Error('查询拦截规则失败,请重试');
}
}
async createRule(rule: Omit<InterceptionRuleModel, 'id'>): Promise<void> {
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 层的'能力清单',确保所有用例都实现统一的核心方法,避免场景扩展时出现能力缺失
export interface InterceptionPoolUseCase {
processParams(params: any): any;
getList(params: any): Promise<InterceptionRuleModel[]>;
}
核心价值:通过接口约束,保证所有拦截池场景(普通仓 / Sip 仓 / 临时仓)都具备'参数处理 + 列表查询'的核心能力,统一业务操作的调用方式。
2. 通用 UseCase:封装共性逻辑
DefaultUseCase 对应类图中的通用实现,负责封装所有场景共享的逻辑,避免重复代码
export class DefaultInterceptionPoolUseCase implements InterceptionPoolUseCase {
protected repository: InterceptionPoolRepository;
constructor() {
this.repository = new InterceptionPoolRepository();
}
processParams(params: any): any {
return {
page_num: params.page || 1,
page_size: params.pageSize || 10,
keyword: params.keyword || ''
};
}
async getList(params: any): Promise<InterceptionRuleModel[]> {
try {
const formattedParams = this.processParams(params);
const ruleModels = await this.repository.queryRules(formattedParams);
return ruleModels.filter(rule => rule.name.trim());
} catch (error) {
console.error('获取拦截规则列表失败(通用逻辑):', error);
throw new Error('查询规则失败,请检查网络后重试');
}
}
}
共性逻辑封装点
- 参数格式化:统一分页参数、通用筛选条件的处理
- 接口调用:复用 Repository 层的查询逻辑
- 基础业务规则:过滤无效数据、统一异常提示
3. 扩展 UseCase:实现个性化场景
SipWhUseCase 对应类图中的扩展实现,继承通用 UseCase 的共性逻辑,重写个性化方法,适配特定场景(如 Sip 仓)
export class SipWhUseCase extends DefaultInterceptionPoolUseCase {
processParams(params: any): any {
const baseParams = super.processParams(params);
return { ...baseParams, rule_type: 'sip' };
}
async getList(params: any): Promise<InterceptionRuleModel[]> {
try {
const baseRules = await super.getList(params);
return baseRules.filter(rule => !rule.condition.includes('cross_border: true'));
} catch (error) {
console.error('获取 Sip 仓拦截规则失败:', error);
throw new Error('Sip 仓规则查询失败,请联系管理员');
}
}
}
扩展逻辑点
- 重写
processParams:添加 Sip 仓专属参数 rule_type: 'sip'
- 扩展
getList:在通用逻辑基础上,新增'过滤跨境规则'的个性化业务
4. 用例工厂:场景与 UseCase 的映射
为了让上层(控制层)无需关心 UseCase 的实例化细节,通过用例工厂实现'场景 → UseCase'的自动映射
export class InterceptionPoolUseCaseFactory {
static create(scene: 'default' | 'sip'): InterceptionPoolUseCase {
switch (scene) {
case 'sip':
return new SipWhUseCase();
default:
return new DefaultInterceptionPoolUseCase();
}
}
}
4.3.3 实际业务场景中的调用
在控制层(Controller)中,通过用例工厂选择对应场景的 UseCase,实现业务逻辑的'按需调用'
import { InterceptionPoolUseCaseFactory } from './usecase/usecase-factory';
export function useInterceptionPoolController(scene: 'default' | 'sip' = 'default') {
const loading = ref(false);
const usecase = InterceptionPoolUseCaseFactory.create(scene);
const ruleList = ref<InterceptionRuleModel[]>([]);
const getRuleList = async (params: any) => {
loading.value = true;
try {
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 处理后的通用业务 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 工具函数(非物理层)
import { InterceptionRuleModel } from '../model/interception-pool';
export interface InterceptionRuleViewModel {
id: string;
name: string;
nameTag: string;
conditionDesc: string;
createTime: string;
operateBtnStatus: boolean;
}
export const mapRuleModelToViewModel = (model: InterceptionRuleModel): InterceptionRuleViewModel => {
const nameTag = model.isExpired ? `${model.name}[已过期]` : model.name;
const conditionDesc = model.condition ? model.condition.replace(/&/g, ',').replace(/=/g, ':') : '无规则条件';
const createTime = new Date(model.createTime).toLocaleString('zh-CN');
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 工具)
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}`;
};
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 率明显下降
该方案的核心价值是'适配业务复杂度'—— 既避免了'照搬后端架构'的过度设计,又通过分层解耦解决了大型前端系统的维护痛点,为长期迭代提供了坚实的架构基础
微信扫一扫,关注极客日志
微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
相关免费在线工具
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
- Markdown转HTML
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
- HTML转Markdown
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
- JSON 压缩
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
- JSON美化和格式化
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online