前端分层架构实战:DDD 与 Clean Architecture 在大型业务系统中的落地路径与项目实践

引言

在某电商后台管理系统的迭代中,我们曾陷入典型的前端业务膨胀困境:修改 "订单拦截规则" 的状态校验逻辑时,需要同时调整 5 个 关联组件的代码 ------ 业务逻辑散落在组件的 setupmethods 中,耦合严重;后续扩展至小程序端 时,核心业务逻辑无法复用,需重新编写 60% 的代码;新成员接手时,需花 1 周 才能理清 "拦截规则从查询到展示" 的全链路逻辑。

这些问题的核心是 "业务逻辑与技术实现的耦合"领域驱动设计(DDD)与整洁架构(Clean Architecture) 为解决这些问题提供了思路 ------ 通过分层解耦 ,将 "稳定的业务规则" 与 "多变的技术工具(框架、UI 组件)" 分离,让前端系统具备长期可维护性与可扩展性

本文结合实际项目实践,详解这两种架构在前端的落地路径。

一、前端 DDD 分层架构:从理论到实际场景

在前端语境下,DDD 分层架构可映射为更具体的代码组织模式,各层对应前端开发中的实际职责

层级 前端落地场景 核心职责
UI 层 Vue / React 组件、UI 库(如 Element Plus 渲染用户界面、响应输入操作,是用户与系统交互的入口与结果载体
控制层 组件交互逻辑(如 VueComposition APIReact 的自定义 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 设计核心价值

这种 "抽象接口 + 通用继承 + 扩展重写" 的设计,完美适配大型前端系统的业务迭代需求

  1. 高复用性:共性逻辑(参数格式化、基础查询)仅需写一次,所有场景复用
  2. 易扩展性 :新增场景(如 "临时拦截池" )时,只需新建 TempInterceptionPoolUseCase 继承DefaultUseCase,重写个性化方法即可
  3. 可维护性 :业务逻辑分层清晰,通用逻辑的修改(如分页参数变更)仅需改 DefaultUseCase,就会同步到所有扩展场景
  4. 符合开闭原则 :扩展新场景时 "不修改原有代码,只新增代码",降低迭代风险

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 的转换是全局统一的结构适配 ,而 ModelView 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 方案落地实现了

  1. 业务 - 框架解耦 :实现核心业务逻辑与 Vue / React 框架的解耦,确保技术栈切换时实现 100% 代码复用
  2. 维护效率提升 :业务规则变更仅需修改领域层代码,维护时间大幅缩短
  3. 跨端复用率PC 后台的拦截池逻辑在小程序中 100% 复用,新终端开发周期显著压缩
  4. 测试覆盖率 :核心业务逻辑单元测试覆盖率提高线上 bug 率明显下降

该方案的核心价值是 "适配业务复杂度" ------ 既避免了 "照搬后端架构" 的过度设计,又通过分层解耦解决了大型前端系统的维护痛点,为长期迭代提供了坚实的架构基础

相关推荐
. . . . .9 小时前
shadcn组件库
前端
2501_944711439 小时前
JS 对象遍历全解析
开发语言·前端·javascript
bobuddy10 小时前
射频收发机架构简介
架构·射频工程
桌面运维家10 小时前
vDisk考试环境IO性能怎么优化?VOI架构实战指南
架构
发现一只大呆瓜10 小时前
虚拟列表:支持“向上加载”的历史消息(Vue 3 & React 双版本)
前端·javascript·面试
css趣多多10 小时前
ctx 上下文对象控制新增 / 编辑表单显示隐藏的逻辑
前端
_codemonster10 小时前
Vue的三种使用方式对比
前端·javascript·vue.js
寻找奶酪的mouse10 小时前
30岁技术人对职业和生活的思考
前端·后端·年终总结
梦想很大很大10 小时前
使用 Go + Gin + Fx 构建工程化后端服务模板(gin-app 实践)
前端·后端·go
We་ct11 小时前
LeetCode 56. 合并区间:区间重叠问题的核心解法与代码解析
前端·算法·leetcode·typescript