前端分层架构实战: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 层(纯展示与交互触发)
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 的转换是全局统一的结构适配 ,而 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 方案落地实现了

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

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

相关推荐
郑州光合科技余经理几秒前
本地生活服务系统:预约状态和进度通知接口怎么拆
java·开发语言·前端·javascript·后端·php·生活
Peter-Code24 分钟前
Nginx 常用操作整理(前端)
服务器·前端·nginx
mldong31 分钟前
工作流引擎的委托代批:一张单,两个办理人
后端·架构
Joy T6 小时前
Agent 确定性上下文设计:从 Prompt 参数注入到能力分层
java·开发语言·微服务·架构·prompt·agent context
微订校园外卖平台9 小时前
同城外卖小程序开发:校园外卖系统四端架构与订单链路拆解
架构·外卖系统·本地生活·微订
m0_587383009 小时前
广州 24 小时自助健身房解决方案技术实现与实战指南
java·spring·小程序·架构·需求分析
牛战士coding9 小时前
AI Agent 如何接入真实业务系统?订单助手三层架构完整拆解
人工智能·架构
收放扳机10 小时前
PCB收放板设备机械手架构选型参考——三轴/四轴/六轴/蜘蛛手对比
人工智能·科技·架构·自动化·制造·pcb工艺
九卷10 小时前
企业级Agent系统整体技术架构介绍
ai·架构·大模型·llm·agent·智能体
Java后端的Ai之路10 小时前
06-React-Props
前端·react.js·前端框架