多场景多角色前端架构方案:基于页面协议化与模块标准化的通用能力沉淀

一、概述

在多仓(如 TWH、TWHN、TWDC )、多角色(管理员、操作员、查看员)等的复杂业务场景中,前端开发面临 "技术栈割裂、业务差异化显著、通用能力复用难" 三大核心痛点

  • 技术栈层面Vue 存量系统与 React 新系统并行,通用能力(如列表查询、权限校验)重复开发,维护成本翻倍
  • 业务层面 :多场景差异(UI / 模型 / 规则)与业务逻辑耦合,单组件代码量常超 1000 行,新场景开发需重复拷贝
  • 架构层面UI、逻辑、数据混编,代码可维护性差,跨组件 / 跨框架复用成本高

本文档提出的 "页面协议化 + 模块标准化 + 分层架构" 方案,通过 SDK 抽象层、差异化配置协议、标准化模块设计、Presenter 分层,实现

  1. 跨技术栈复用 :一套核心逻辑支持 Vue / React 双框架
  2. 多场景零代码适配 :通过配置文件实现 UI / 权限 / 多语言的差异化
  3. 通用能力沉淀 :模块、PresenterUseCase 分层复用,降低重复开发工作量
  4. 代码复杂度收敛:减少组件逻辑代码量,提升维护效率

二、问题分析

2.1 技术栈差异:从 "割裂" 到 "协同" 的核心矛盾

当前前端技术生态呈现 "多框架共存、逐步迁移" 的现状

  • 存量系统 :基于 Vue 2 / Vue 3 开发,承载历史核心业务(如仓储管理、订单拦截)
  • 新增系统 :基于 React 开发,聚焦新业务场景(如智能分拣、数据分析)
  • 未来趋势React 为主要技术栈,Vue 项目需逐步迁移
核心痛点
  1. 通用能力重复开发 :同一套 "列表分页 + 导出" 逻辑,Vue 版本需维护 vue-table 组件,React 版本需重新开发 react-table 组件,两者逻辑一致但代码完全独立
  2. 组件复用壁垒 :仓储管理的 "仓选择器" 组件,Vue 版本基于 element-ui 开发,React 版本需基于 antd 重新实现,视觉和交互要求一致但代码无法复用
  3. 数据互通困难Vue 订单系统与 React 分拣系统需共享 "订单拦截规则" 数据,需开发两套接口适配逻辑,且状态同步存在延迟
解决方案核心思路

抽象 "框架无关的核心层""框架专属的适配层"

  • 核心层 :封装通用业务逻辑(如列表查询、权限校验),不依赖任何框架 API
  • 适配层 :为 Vue / React 分别封装框架专属的调用方式(如 Vue Hooks、React Hooks
  • 通过 SDK 统一对外暴露能力,实现 "一套核心逻辑,双框架复用"

2.2 业务场景:从 "差异化" 到 "标准化" 的平衡难题

多场景差异覆盖 "UI 展示、数据模型、业务规则" 三个维度,是业务个性化与架构标准化的核心矛盾点,具体表现如下

差异类型 示例场景 核心诉求 占比
UI 差异(模型 + 规则相同) TWH 仓需展示 "服务代码" 字段,TWDC 仓隐藏该字段 无需修改代码,通过配置快速切换 UI 形态 40%
模型差异(UI + 规则相同) 机器分拣规则:TWH 仓数据字段为 rule_id,其他仓为 id 统一 UI / 规则,仅切换数据模型 2%
规则差异(UI + 模型相同) 订单审核:TWH 仓需 3 级审核,其他仓仅需 1 级 统一 UI / 模型,仅切换业务逻辑 5%
双差异(UI + 模型均不同) 订单拦截池:TWDC 仓需展示 "跨境规则" 字段(UI 差异),且接口返回的 "拦截原因" 字段格式为数组(其他仓为字符串)(模型差异) 配置 + 多模型组合适配 13%
三差异(UI + 模型 + 规则均不同) 智能调度:不同仓的调度算法、展示字段、数据结构均不同 全维度配置 + 多 UseCase 组合 5%
无差异(UI + 模型均相同 基础数据字典:所有仓的展示 / 模型 / 规则完全相同 直接复用通用模块 35%
核心痛点
  • 差异化逻辑与业务逻辑硬编码耦合 ,导致模块臃肿(如单个组件中充斥 if 判断)
  • 通用能力分散在各模块中,新场景开发需重复拷贝,效率低下
  • 多场景配置分散在代码各处,修改时需逐个查找,易遗漏、易出错
解决方案核心思路
  • "页面协议化" 沉淀静态差异(UI / 权限 / 多语言)
  • "模块标准化" 封装动态差异(数据模型 / 业务规则)
  • "UseCase 工厂" 实现差异逻辑的动态映射,避免硬编码

2.3 代码组织:从 "混乱" 到 "整洁" 的架构诉求

现有开发模式中,代码存在 "UI 交互、业务逻辑、数据请求" 的问题

  1. 逻辑混乱Vue 组件的 methods 中同时包含表格渲染逻辑、权限判断、接口请求
  2. 文件臃肿 :复杂业务组件代码量超 1000 行,维护时需反复定位逻辑位置
  3. 复用困难:通用逻辑(如列表分页、加载状态管理)未抽离,不同模块重复编写
  4. 测试缺失 :因 UI、逻辑、数据耦合,单元测试难以覆盖核心逻辑,仅能依赖手工测试,回归测试成本高
解决方案核心思路

引入 Presenter 层,遵循 "整洁架构" 思想,实现 "View(UI)→ Presenter(逻辑)→ UseCase(业务)→ Repository(数据)" 的分层解耦

  • 每层职责单一,仅与相邻层交互
  • 核心业务逻辑(UseCase) 不依赖外层(View / Presenter),保证可复用性
  • 数据层(Repository) 屏蔽数据源差异,业务层无需关注接口细节

三、关键方案设计(深度解析)

3.1 SDK 抽象:跨框架通用能力的核心载体

SDK 是实现跨框架复用的核心,设计遵循 "核心层框架无关、适配层框架专属" 的原则,整体架构基于 "整洁架构" 思想,确保核心逻辑的稳定性与可复用性

3.1.1 SDK 架构分层(基于整洁架构)
typescript 复制代码
@clean-arch/
├── core/                  // 核心层:框架无关,承载通用业务能力(核心)
│   ├── usecase/           // 用例层:封装通用业务逻辑(如列表查询、导出)
│   │   ├── abs-table-usecase.ts  // 抽象列表用例(定义分页、搜索通用逻辑)
│   │   ├── export-usecase.ts     // 抽象导出用例(定义导出权限、格式通用逻辑)
│   │   └── permission-usecase.ts // 抽象权限用例(定义权限校验通用逻辑)
│   ├── repository/        // 仓储层:定义数据访问契约(接口)
│   │   ├── abs-repository.ts     // 抽象仓储接口(屏蔽axios/fetch差异)
│   │   ├── http-adapter.ts       // HTTP 请求适配器(统一请求格式)
│   │   └── mock-adapter.ts       // Mock 适配器(开发环境数据模拟)
│   ├── model/             // 模型层:定义通用数据结构(TypeScript 接口)
│   │   ├── table.ts       // 列表通用模型(分页、数据结构)
│   │   └── permission.ts  // 权限通用模型(权限码、角色)
│   └── common/            // 通用工具:框架无关的工具函数
│       ├── permission.ts  // 权限校验(checkPermission)
│       ├── format.ts      // 数据格式化(日期、金额)
│       └── error.ts       // 异常处理(统一错误码、提示)
├── vue-adapter/           // Vue 适配层:将核心能力适配为 Vue 可用形态
│   ├── vue-moduler/       // Vue 模块渲染器(Renderer)
│   ├── vue-presenter/     // Vue Presenter 封装(兼容 Vue 2/3 响应式)
│   └── hooks/             // Vue Hooks 封装(useTable、usePermission)
├── react-adapter/         // React 适配层:将核心能力适配为 React 可用形态
│   ├── react-moduler/     // React 模块渲染器
│   ├── react-presenter/   // React Presenter 封装(兼容 Hooks 响应式)
│   └── hooks/             // React Hooks 封装(useTable、usePermission)
└── package.json           // SDK 发布配置(支持按需引入)
3.1.2 核心层实现:通用列表 UseCase

核心层逻辑完全不依赖任何框架 API ,仅聚焦业务逻辑,通过抽象类定义 "契约",具体实现由业务模块补充

typescript 复制代码
// core/usecase/abs-table-usecase.ts
import { formatSearchParams } from '../common/format';
import { TableParams, TableResult } from '../model/table';

/**
 * 列表通用用例抽象类
 * 定义列表分页、搜索、刷新的通用逻辑,具体接口实现由业务仓储层完成
 * @template Row 列表行数据类型
 * @template Params 搜索参数类型
 */
export abstract class AbsTableUsecase<Row = any, Params = Record<string, any>> {
  /**
   * 抽象方法:获取列表数据(由业务仓储层实现)
   * @param params 分页+搜索参数
   * @returns 列表数据+分页信息
   */
  abstract fetchTable(params: TableParams<Params>): Promise<TableResult<Row>>;

  /**
   * 通用逻辑:处理搜索参数(过滤空值、格式化参数)
   * @param params 原始搜索参数
   * @returns 处理后的参数
   */
  processSearchParams(params: Params): Params {
    // 过滤 undefined/null/空字符串
    const filteredParams = formatSearchParams(params);
    // 通用参数格式化(如日期范围转换为时间戳)
    return this.formatSpecialParams(filteredParams);
  }

  /**
   * 扩展点:业务自定义参数格式化(由子类重写)
   * @param params 过滤后的参数
   * @returns 格式化后的参数
   */
  protected formatSpecialParams(params: Params): Params {
    return params; // 默认无处理
  }

  /**
   * 通用逻辑:刷新列表(重置页码+重新请求)
   * @param params 搜索参数
   * @returns 列表数据
   */
  async refreshTable(params: Params): Promise<TableResult<Row>> {
    const processedParams = this.processSearchParams(params);
    return this.fetchTable({
      ...processedParams,
      current: 1, // 重置页码为1
      pageSize: 10,
    });
  }
}
3.1.3 适配层设计:Vue / React 通用能力映射

适配层的核心职责是将核心层的框架无关能力,转换为当前框架的原生使用方式

Vue 适配层示例(useTableUsecase Hook)
typescript 复制代码
// vue-adapter/hooks/use-table-usecase.ts
import { ref, watch, onMounted } from 'vue';
import { AbsTableUsecase } from '@clean-arch/core';
import { TableParams, TableResult } from '@clean-arch/core/model/table';

/**
 * Vue 列表通用Hook
 * 封装核心层AbsTableUsecase,适配Vue响应式
 * @param usecase 业务列表用例实例
 * @param defaultParams 默认搜索参数
 */
export function useTableUsecase<Row = any, Params = Record<string, any>>(
  usecase: AbsTableUsecase<Row, Params>,
  defaultParams: Params = {} as Params
) {
  // Vue响应式状态
  const dataSource = ref<Row[]>([]);
  const loading = ref(false);
  const pagination = ref({ current: 1, pageSize: 10, total: 0 });
  const searchParams = ref<Params>(defaultParams);

  /**
   * 加载列表数据
   */
  const fetchData = async () => {
    loading.value = true;
    try {
      const params: TableParams<Params> = {
        ...searchParams.value,
        current: pagination.value.current,
        pageSize: pagination.value.pageSize,
      };
      const res: TableResult<Row> = await usecase.fetchTable(params);
      dataSource.value = res.list;
      pagination.value.total = res.total;
    } catch (error) {
      console.error('列表加载失败:', error);
      dataSource.value = [];
      pagination.value.total = 0;
    } finally {
      loading.value = false;
    }
  };

  /**
   * 刷新列表(重置页码)
   */
  const refresh = async () => {
    pagination.value.current = 1;
    await fetchData();
  };

  /**
   * 更新搜索参数并刷新
   * @param params 新搜索参数
   */
  const updateSearchParams = async (params: Partial<Params>) => {
    searchParams.value = { ...searchParams.value, ...params };
    await refresh();
  };

  // 分页变化时自动加载
  watch(
    [() => pagination.value.current, () => pagination.value.pageSize],
    fetchData
  );

  // 组件挂载时加载初始数据
  onMounted(fetchData);

  // 返回给组件的状态和方法
  return {
    dataSource,
    loading,
    pagination,
    searchParams,
    fetchData,
    refresh,
    updateSearchParams,
  };
}
React 适配层示例(useTableUsecase Hook)
typescript 复制代码
// react-adapter/hooks/use-table-usecase.ts
import { useState, useEffect, useCallback } from 'react';
import { AbsTableUsecase } from '@clean-arch/core';
import { TableParams, TableResult } from '@clean-arch/core/model/table';

/**
 * React 列表通用Hook
 * 封装核心层AbsTableUsecase,适配React Hooks
 * @param usecase 业务列表用例实例
 * @param defaultParams 默认搜索参数
 */
export function useTableUsecase<Row = any, Params = Record<string, any>>(
  usecase: AbsTableUsecase<Row, Params>,
  defaultParams: Params = {} as Params
) {
  // React状态
  const [dataSource, setDataSource] = useState<Row[]>([]);
  const [loading, setLoading] = useState(false);
  const [pagination, setPagination] = useState({ current: 1, pageSize: 10, total: 0 });
  const [searchParams, setSearchParams] = useState<Params>(defaultParams);

  /**
   * 加载列表数据
   */
  const fetchData = useCallback(async () => {
    setLoading(true);
    try {
      const params: TableParams<Params> = {
        ...searchParams,
        current: pagination.current,
        pageSize: pagination.pageSize,
      };
      const res: TableResult<Row> = await usecase.fetchTable(params);
      setDataSource(res.list);
      setPagination(prev => ({ ...prev, total: res.total }));
    } catch (error) {
      console.error('列表加载失败:', error);
      setDataSource([]);
      setPagination(prev => ({ ...prev, total: 0 }));
    } finally {
      setLoading(false);
    }
  }, [usecase, searchParams, pagination]);

  /**
   * 刷新列表(重置页码)
   */
  const refresh = useCallback(async () => {
    setPagination(prev => ({ ...prev, current: 1 }));
  }, []);

  /**
   * 更新搜索参数并刷新
   * @param params 新搜索参数
   */
  const updateSearchParams = useCallback(async (params: Partial<Params>) => {
    setSearchParams(prev => ({ ...prev, ...params }));
    refresh();
  }, [refresh]);

  // 分页变化时自动加载
  useEffect(() => {
    fetchData();
  }, [fetchData]);

  // 刷新触发重新加载
  useEffect(() => {
    if (pagination.current === 1) {
      fetchData();
    }
  }, [refresh, fetchData]);

  // 返回给组件的状态和方法
  return {
    dataSource,
    loading,
    pagination,
    searchParams,
    fetchData,
    refresh,
    updateSearchParams,
  };
}
3.1.4 跨框架适配的最终架构

该架构分为三层,核心设计考量如下

  1. 数据层(Data Layer)
    • 职责:封装后端 API 、屏蔽数据来源差异(HTTP / Mock / 本地存储)
    • 设计考量:通过抽象仓储接口,业务层无需关注数据来源,可灵活切换真实接口 / Mock 数据,降低开发联调成本
  2. 领域层(Domain Layer)
    • 职责:承载通用业务逻辑(UseCase),是框架无关的核心层
    • 设计考量:核心逻辑与框架解耦,技术栈迁移时仅需修改适配层,核心代码无需变动
  3. 表现层(Presentation Layer)
    • 职责:通过 Presenter 适配 Vue / ReactUI 形态
    • 设计考量:遵循 "最小适配" 原则,仅适配响应式和调用方式,不修改核心逻辑
核心价值
  • 降低技术栈迁移成本React 迁移时,仅需开发 React 适配层,核心业务逻辑(领域层) 完全复用
  • 提升通用能力维护效率 :权限校验逻辑 bug 仅需修复核心层一次,双框架自动同步

3.2 页面协议化:差异化配置的标准化方案

页面协议化的核心是 "将静态差异(UI、权限、多语言)从代码中抽离为配置文件" ,通过 "默认配置 + 场景配置" 的叠加逻辑,实现多场景的差异化渲染,无需修改业务代码

3.2.1 协议设计原则
  1. 最小差异原则:仅配置不同场景的差异项,默认配置覆盖共性逻辑,减少配置冗余
  2. 类型安全原则 :配置结构遵循 JSON Schema 规范,支持构建时校验,避免配置错误
  3. 可扩展原则:支持配置嵌套、动态表达式,满足复杂场景需求
  4. 可读性原则:配置字段命名语义化,场景关键字与业务场景(如仓编码)保持一致
3.2.2 协议完整结构(以 "机器分拣规则页面" 为例)
typescript 复制代码
// index.config.ts 完整协议结构
import { ModuleId } from './constants';
import { MODULE_MAP } from './module-map';
import { Api } from './api';
import { getCurrentUserRole } from '@/utils/user';

/**
 * 页面协议配置
 * 结构说明:
 * - default:默认配置(所有场景共享)
 * - [场景关键字]:差异化场景配置(支持多场景合并,用逗号分隔)
 * - 配置优先级:场景配置 > 默认配置(场景配置会覆盖默认配置的同名项)
 */
const pageConfig = {
  // 默认配置(所有仓共享)
  default: {
    // 页面基础信息(所有场景共享)
    pageInfo: {
      title: '机器分拣规则配置', // 页面标题
      breadcrumb: ['配置管理', '分拣规则'], // 面包屑
      keepAlive: true, // 是否开启页面缓存
    },
    // 模块映射关系(页面需加载的模块)
    modules: [
      {
        id: ModuleId.AUTO_SORTING_RULE, // 模块唯一 ID(页面内唯一)
        ...MODULE_MAP.AutoSortingRule, // 模块基础信息(组件引入、模块名)
        visible: true, // 是否默认显示
        order: 1, // 模块渲染顺序(数字越小越靠前)
        cache: false, // 是否缓存模块实例
      },
      {
        id: ModuleId.RULE_LOG, // 日志模块
        ...MODULE_MAP.RuleLog,
        visible: (wh: string, role: string) => role === 'admin', // 仅管理员可见
        order: 2,
      },
    ],
    // 模块差异化配置(key 为模块 ID)
    data: {
      [ModuleId.AUTO_SORTING_RULE]: {
        // 模块 props 数据(控制 UI 展示)
        data: {
          showServiceCode: true, // 是否显示服务代码
          showSortingType: false, // 是否显示分拣类型
          showKanban: true, // 是否显示看板
          useCase: 'default', // 绑定的用例名称
          // 动态表达式:根据仓编码返回模板地址
          templateUrl: (currentWh: string) => 
            currentWh === 'TWH' ? Api.TEMPLATE_TWH : Api.TEMPLATE_DEFAULT,
          // 动态表达式:根据用户角色返回导出权限
          exportEnable: () => getCurrentUserRole() !== 'viewer',
        },
        // 模块权限配置(控制按钮显示/禁用)
        permission: {
          export: 'operation_config.auto_sorting_rule.export', // 导出权限码
          create: 'operation_config.auto_sorting_rule.create', // 创建权限码
          edit: 'operation_config.auto_sorting_rule.edit', // 编辑权限码
          delete: 'operation_config.auto_sorting_rule.delete', // 删除权限码
        },
        // 模块多语言配置(映射多语言文件的key)
        i18n: {
          title: 'auto_sorting_rule.title', 
          buttonCreate: 'auto_sorting_rule.button.create',
          buttonExport: 'auto_sorting_rule.button.export',
          emptyText: 'auto_sorting_rule.empty.text', // 空数据提示
        },
      },
    },
  },
  // TWH、TWHN 仓共享配置(覆盖默认配置)
  'TWH,TWHN': {
    data: {
      [ModuleId.AUTO_SORTING_RULE]: {
        data: {
          showServiceCode: false, // 隐藏服务代码字段
          showSortingType: true, // 显示分拣类型字段
          useCase: 'for-twh', // 绑定TWH专属用例
        },
        permission: {
          delete: false, // TWH 仓禁用删除权限(覆盖默认配置)
        },
        i18n: {
          title: 'auto_sorting_rule.title.twh', // TWH 仓专属标题
        },
      },
    },
  },
  // TWDC 仓单独配置
  'TWDC': {
    modules: [
      {
        id: ModuleId.AUTO_SORTING_RULE,
        visible: true,
        order: 1,
      },
      {
        id: ModuleId.RULE_LOG,
        visible: true, // TWDC 仓所有角色可见日志模块
        order: 2,
      },
    ],
    data: {
      [ModuleId.AUTO_SORTING_RULE]: {
        data: {
          showServiceCode: true,
          showCrossBorder: true, // TWDC 仓新增跨境字段(扩展默认配置)
          useCase: 'default',
        },
      },
    },
  },
};

export default pageConfig;
3.2.3 协议解析与渲染流程

页面协议的解析的核心是 "配置合并 + 模块注册 + 动态渲染" ,由 Module 装饰器和 Renderer 组件协同完成,流程如下

1. 配置合并工具(核心逻辑)
typescript 复制代码
// @clean-arch/core/common/config-merge.ts
import { deepMerge } from 'lodash';

/**
 * 合并默认配置与场景配置
 * @param defaultConfig 默认配置
 * @param sceneConfigs 场景配置(支持多场景)
 * @param currentScene 当前场景关键字
 * @returns 合并后的最终配置
 */
export function mergePageConfig(
  defaultConfig: Record<string, any>,
  sceneConfigs: Record<string, any>,
  currentScene: string
): Record<string, any> {
  // 拆分多场景(如"TWH,TWHN"拆分为["TWH", "TWHN"])
  const sceneList = currentScene.split(',').map(s => s.trim());
  // 收集所有匹配的场景配置
  const matchedConfigs = sceneList
    .filter(scene => sceneConfigs[scene])
    .map(scene => sceneConfigs[scene]);
  
  // 合并逻辑:默认配置 → 场景配置(后合并的覆盖先合并的)
  let finalConfig = deepMerge({}, defaultConfig);
  matchedConfigs.forEach(config => {
    finalConfig = deepMerge(finalConfig, config);
  });

  // 处理动态表达式(如visible函数)
  return resolveDynamicConfig(finalConfig, currentScene);
}

/**
 * 解析配置中的动态表达式
 * @param config 待解析配置
 * @param currentScene 当前场景
 * @returns 解析后的配置
 */
function resolveDynamicConfig(config: Record<string, any>, currentScene: string): Record<string, any> {
  // 递归遍历配置,执行函数类型的字段
  const resolveValue = (value: any) => {
    if (typeof value === 'function') {
      // 传递当前场景、用户角色等参数给动态函数
      const userRole = getCurrentUserRole();
      const currentWh = currentScene.split(',')[0]; // 取第一个仓编码
      return value(currentWh, userRole);
    }
    if (typeof value === 'object' && value !== null) {
      return resolveDynamicConfig(value, currentScene);
    }
    return value;
  };

  return Object.entries(config).reduce((acc, [key, value]) => {
    acc[key] = resolveValue(value);
    return acc;
  }, {} as Record<string, any>);
}
2. 渲染流程完整步骤
  1. 场景识别 :通过 getCurrentWh() 获取当前仓编码(如 "TWH"),作为场景关键字
  2. 配置加载 :页面入口文件导入 pageConfig,调用 mergePageConfig 合并默认配置与场景配置
  3. 模块注册Module 装饰器根据合并后的 modules 字段,自动注册模块组件到 Vue / React 实例
  4. 参数注入 :将 data 中的 permissioni18n 等配置注入对应模块的 props
  5. 动态渲染Renderer 组件根据模块 order 属性排序,按 visible 属性过滤,最终渲染模块
3.2.4 协议扩展能力:动态表达式与条件渲染
1. 动态表达式(仓 + 角色双维度控制)
typescript 复制代码
// 示例:仅 TWDC 仓的管理员可见"跨境规则配置"模块
modules: [
  {
    id: ModuleId.CROSS_BORDER_RULE,
    ...MODULE_MAP.CrossBorderRule,
    visible: (wh: string, role: string) => wh === 'TWDC' && role === 'admin',
    order: 3,
  },
]
2. 条件渲染(权限 + 场景双维度)
typescript 复制代码
// 示例:仅拥有"超级管理员"权限且 TWH 仓显示"规则模板管理"按钮
data: {
  showTemplateButton: () => {
    const hasSuperPermission = checkPermission('super.admin');
    const currentWh = getCurrentWh();
    return hasSuperPermission && currentWh === 'TWH';
  },
}
3. 配置继承(减少冗余)
typescript 复制代码
// 示例:TWHN 仓继承 TWH 仓的配置,仅修改部分字段
'TWH': { /* TWH 仓配置 */ },
'TWHN': {
  extend: 'TWH', // 继承 TWH 仓配置
  data: {
    [ModuleId.AUTO_SORTING_RULE]: {
      data: {
        showSortingType: false, // 仅修改此字段
      },
    },
  },
},

3.3 模块标准化:通用能力沉淀的最小单元

模块是通用能力沉淀的最小单元,通过 "目录规范、Schema 定义、分层解耦" 实现 "一次开发、多场景复用",同时支持差异化扩展

3.3.1 模块目录规范
typescript 复制代码
modules/auto-sorting-rule/  // 模块根目录(以业务名称命名)
├── index.vue               // Vue入口(View 层:仅负责 UI 渲染)
├── index.react.tsx         // React入口(View 层:可选,双框架支持)
├── schema.json             // 模块属性描述(JSON Schema:类型校验)
├── types.ts                // TypeScript 类型定义(从 Schema 自动生成)
├── fields.ts               // 字段配置(表格列、搜索项、表单字段)
├── presenter/              // Presenter 层:抽离 UI 逻辑(状态 + 行为)
│   └── index.ts            // Presenter 实现
├── usecase/                // 用例层:业务逻辑(按场景拆分)
│   ├── default.ts          // 默认用例(通用逻辑)
│   └── for-twh.ts          // TWH 仓专属用例(差异化逻辑)
├── repository/             // 仓储层:数据访问(按场景拆分)
│   ├── default.ts          // 默认仓储(通用 API)
│   └── for-twh.ts          // TWH 仓专属仓储(差异化 API)
└── usecase-map.ts          // 用例映射表(关联用例名称与实现)
目录 / 文件 核心职责 开发规范
index.vue / index.react.tsx UI 渲染,仅包含模板和简单事件触发 禁止包含业务逻辑,代码量不超过 200 行
schema.json 定义模块属性契约 必须包含所有对外暴露的 props,标注类型和默认值
types.ts 模块类型定义 必须从 schema.json 自动生成,禁止手动修改
fields.ts 定义表格 / 表单字段 字段配置需包含 label、key、type、rules 等通用属性
presenter/ 抽离 UI 逻辑 禁止直接调用 API,仅通过 UseCase 交互
usecase/ 封装业务逻辑 禁止包含 UI 相关代码,仅处理数据和规则
repository/ 封装数据请求 仅处理接口调用、数据转换,禁止包含业务逻辑
3.3.2 Schema 定义与类型生成
1. Schema 完整示例
typescript 复制代码
{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "AutoSortingRuleModule",
  "description": "机器分拣规则模块",
  "type": "object",
  "properties": {
    "data": {
      "type": "object",
      "description": "模块UI配置参数",
      "properties": {
        "showServiceCode": {
          "type": "boolean",
          "description": "是否显示服务代码字段",
          "default": true
        },
        "showSortingType": {
          "type": "boolean",
          "description": "是否显示分拣类型字段",
          "default": false
        },
        "showCrossBorder": {
          "type": "boolean",
          "description": "是否显示跨境规则字段",
          "default": false
        },
        "useCase": {
          "type": "string",
          "description": "绑定的用例名称",
          "enum": ["default", "for-twh"],
          "default": "default"
        },
        "exportURL": {
          "type": ["string", "function"],
          "description": "导出接口地址(支持动态函数)"
        }
      },
      "required": ["useCase"]
    },
    "permission": {
      "type": "object",
      "description": "模块权限配置",
      "properties": {
        "export": {
          "type": ["string", "boolean"],
          "description": "导出权限(字符串为权限码,布尔值为是否启用)"
        },
        "create": {
          "type": ["string", "boolean"],
          "description": "创建权限"
        },
        "edit": {
          "type": ["string", "boolean"],
          "description": "编辑权限"
        },
        "delete": {
          "type": ["string", "boolean"],
          "description": "删除权限"
        }
      },
      "default": {}
    },
    "i18n": {
      "type": "object",
      "description": "模块多语言配置",
      "properties": {
        "title": {
          "type": "string",
          "description": "模块标题多语言key"
        },
        "emptyText": {
          "type": "string",
          "description": "空数据提示多语言key",
          "default": "common.empty.text"
        }
      },
      "required": ["title"]
    }
  },
  "required": ["data", "i18n"]
}
2. 自动生成 TypeScript 类型

通过 json-schema-to-typescript 工具自动生成类型,避免手动维护的错误

typescript 复制代码
# 安装依赖
npm install json-schema-to-typescript --save-dev

# 添加package.json脚本
{
  "scripts": {
    "generate:types": "npx json2ts modules/**/schema.json -o modules/**/types.ts"
  }
}

# 执行生成命令
npm run generate:types

生成的 types.ts 示例

typescript 复制代码
/**
 * Machine sorting rule module
 * 自动生成,禁止手动修改
 */
export interface AutoSortingRuleModule {
  data: {
    showServiceCode?: boolean;
    showSortingType?: boolean;
    showCrossBorder?: boolean;
    useCase: 'default' | 'for-twh';
    exportURL?: string | ((...args: any[]) => string);
  };
  permission?: {
    export?: string | boolean;
    create?: string | boolean;
    edit?: string | boolean;
    delete?: string | boolean;
  };
  i18n: {
    title: string;
    emptyText?: string;
  };
}
3.3.3 多场景下的页面渲染全流程

该流程覆盖从 "用户访问""UI 渲染" 的完整链路,每个步骤的核心操作如下

1. 场景识别(路由层)
  • 操作:解析 URL 参数 / 用户登录信息,获取当前仓编码(如 "TWH")和用户角色
  • 输出:当前场景关键字(如 "TWH")、用户角色(如 "admin")
2. 配置加载(页面层)
  • 操作:导入页面配置文件,调用 mergePageConfig 合并默认配置与场景配置
  • 输出:当前场景的最终页面配置(模块列表、模块参数)
3. 模块组装(渲染层)
  • 操作:Renderer 组件根据配置加载模块,注入 data / permission / i18n 参数
  • 输出:模块实例(包含差异化参数)
4. 逻辑执行(模块层)
  • 操作:模块 Presenter 根据 useCase 参数,通过 UseCase 工厂加载对应业务逻辑,调用 Repository 获取数据
  • 输出:渲染所需的状态数据
5. UI 渲染(视图层)
  • 操作:模块 View 层接收 Presenter 的状态数据,渲染最终 UI
  • 输出:用户可见的页面
核心价值
  • 场景差异化完全通过配置实现,模块代码无需修改
  • 通用逻辑沉淀在模块内部,新场景仅需配置即可复用
  • 流程标准化,不同开发者开发的模块渲染逻辑一致,降低协作成本

3.4 改进:引入 Presenter 层

为解决 UI 与业务逻辑耦合的问题,在整洁架构基础上引入 Presenter 层(参考 MVP 架构),将与显示无关的逻辑从 View 中抽离,实现职责分层

3.4.1 引入 Presenter 层前的架构痛点

引入 Presenter 前,ViewUI 组件)直接与 UseCase (业务逻辑)、Repository (数据层)交互,导致 View 同时承载 UI 渲染、状态管理、业务逻辑 三重职责,最终造成组件臃肿、逻辑耦合(如 Vue 组件的 script 中混杂表格渲染、接口请求、权限判断)

引入前的核心问题(来自实际业务案例)
  • 代码臃肿:"订单拦截池" Vue 组件代码量达 1800 行,包含 UI 渲染、接口请求、审核规则判断等逻辑,新增 "跨境拦截规则" 时,需在组件中新增 200 行代码,易影响原有逻辑
  • 复用困难:"列表加载状态管理" 逻辑在 10 + 个模块中重复编写,每个模块的实现细节略有不同(如加载提示文字),维护成本高
  • 测试困难:因 UI 与业务逻辑耦合,单元测试需模拟 DOM、接口、业务规则,覆盖率仅 30%
3.4.2 引入 Presenter 层后的架构优化
引入后的分层职责边界
层级 核心职责 禁止操作
View(UI 组件) 仅负责 UI 渲染,触发 Presenter 的方法 禁止调用 API、处理业务规则、直接修改状态
Presenter 状态管理、交互逻辑、调用 UseCase 禁止直接操作 DOM 、调用 API 、包含 UI 样式逻辑
UseCase 业务规则处理、调用 Repository 禁止操作 UI 状态、直接操作 DOM
Repository 接口请求、数据转换 禁止处理业务规则、操作 UI 状态
核心价值
  • View 轻量化:"订单拦截池" 组件代码量从 1800 行降至 300 行,仅保留模板和事件触发
  • 逻辑复用:"列表加载状态管理" 逻辑沉淀到通用 Presenter,10 + 个模块直接复用
  • 测试高效:Presenter 层可独立测试,单元测试覆盖率提升至 80%
3.4.3 Presenter 核心实现(完整示例)
1. 基础 Presenter 类(框架无关)
typescript 复制代码
// @clean-arch/core/presenter/base-presenter.ts
import { EventEmitter } from 'events';

/**
 * 基础Presenter类(框架无关)
 * 实现状态管理、事件发布订阅
 * @template State Presenter状态类型
 */
export class BasePresenter<State = Record<string, any>> extends EventEmitter {
  protected state: State;

  constructor(initialState: State) {
    super();
    this.state = { ...initialState };
  }

  /**
   * 更新状态(支持函数式更新)
   * @param updater 状态更新函数/对象
   */
  setState(updater: ((prevState: State) => State) | Partial<State>): void {
    const newState = typeof updater === 'function' 
      ? updater({ ...this.state }) 
      : { ...this.state, ...updater };
    this.state = newState;
    // 发布状态变更事件
    this.emit('stateChange', this.state);
  }

  /**
   * 获取当前状态
   * @returns 当前状态
   */
  getState(): State {
    return { ...this.state };
  }

  /**
   * 销毁Presenter(清理事件监听)
   */
  destroy(): void {
    this.removeAllListeners();
  }
}
2. Vue 适配 Presenter(响应式封装
typescript 复制代码
// @clean-arch/vue-adapter/vue-presenter.ts
import { ref, watch } from 'vue';
import { BasePresenter } from '@clean-arch/core';

/**
 * Vue Presenter Hook
 * 将BasePresenter适配为Vue响应式
 * @param PresenterClass Presenter类
 * @param args Presenter构造函数参数
 * @returns 响应式状态和Presenter实例
 */
export function usePresenter<
  P extends BasePresenter<any>,
  Args extends any[] = any[]
>(PresenterClass: new (...args: Args) => P, ...args: Args) {
  // 创建Presenter实例
  const presenter = new PresenterClass(...args);
  // 响应式状态
  const state = ref(presenter.getState());

  // 监听状态变更,更新响应式状态
  presenter.on('stateChange', (newState) => {
    state.value = newState;
  });

  // 组件卸载时销毁Presenter
  watch(
    [],
    () => {},
    {
      onUnmounted() {
        presenter.destroy();
      },
    }
  );

  // 返回响应式状态和Presenter方法
  return {
    state,
    presenter,
    // 将Presenter的方法绑定到返回对象
    ...Object.getOwnPropertyNames(PresenterClass.prototype)
      .filter(name => name !== 'constructor' && typeof PresenterClass.prototype[name] === 'function')
      .reduce((acc, name) => {
        acc[name] = presenter[name].bind(presenter);
        return acc;
      }, {} as Record<string, Function>),
  };
}
3. 业务 Presenter 实现(分拣规则模块)
typescript 复制代码
// modules/auto-sorting-rule/presenter/index.ts
import { BasePresenter } from '@clean-arch/core';
import { usecaseFactory } from '@clean-arch/core/common/usecase-factory';
import { USECASE_MAP } from '../usecase-map';
import { AutoSortingRuleModule } from '../types';

/**
 * 分拣规则Presenter状态类型
 */
interface SortingRuleState {
  ruleList: any[]; // 规则列表
  loading: boolean; // 加载状态
  isModalOpen: boolean; // 编辑弹窗状态
  currentRule: any | null; // 当前编辑的规则
}

/**
 * 分拣规则Presenter
 * 抽离UI逻辑,调用UseCase处理业务
 */
export class SortingRulePresenter extends BasePresenter<SortingRuleState> {
  private useCase: any;

  constructor(private moduleData: AutoSortingRuleModule['data']) {
    // 初始化状态
    super({
      ruleList: [],
      loading: false,
      isModalOpen: false,
      currentRule: null,
    });
    // 根据模块配置加载对应UseCase
    this.useCase = usecaseFactory(moduleData.useCase, USECASE_MAP);
  }

  /**
   * 获取分拣规则列表
   */
  fetchRuleList = async () => {
    this.setState({ loading: true });
    try {
      const res = await this.useCase.getRuleList();
      this.setState({ ruleList: res.data });
    } catch (error) {
      console.error('获取分拣规则失败:', error);
      this.setState({ ruleList: [] });
    } finally {
      this.setState({ loading: false });
    }
  };

  /**
   * 打开编辑弹窗
   * @param rule 待编辑的规则
   */
  openEditModal = (rule: any = null) => {
    this.setState({
      isModalOpen: true,
      currentRule: rule,
    });
  };

  /**
   * 关闭编辑弹窗
   */
  closeEditModal = () => {
    this.setState({
      isModalOpen: false,
      currentRule: null,
    });
  };

  /**
   * 保存分拣规则
   * @param rule 规则数据
   */
  saveRule = async (rule: any) => {
    this.setState({ loading: true });
    try {
      if (rule.id) {
        await this.useCase.updateRule(rule);
      } else {
        await this.useCase.createRule(rule);
      }
      this.closeEditModal();
      await this.fetchRuleList(); // 刷新列表
    } catch (error) {
      console.error('保存分拣规则失败:', error);
      throw error; // 向上抛出,让View层处理错误提示
    } finally {
      this.setState({ loading: false });
    }
  };
}
4. Vue 组件中使用 Presenter
typescript 复制代码
<!-- modules/auto-sorting-rule/index.vue -->
<template>
  <div class="auto-sorting-rule-module">
    <!-- 加载状态 -->
    <el-loading v-if="state.loading" fullscreen />
    
    <!-- 规则列表 -->
    <el-table :data="state.ruleList" border>
      <el-table-column 
        prop="ruleName" 
        :label="i18n.ruleName" 
        v-if="data.showServiceCode"
      />
      <el-table-column 
        prop="sortingType" 
        :label="i18n.sortingType" 
        v-if="data.showSortingType"
      />
      <el-table-column label="操作">
        <template #default="scope">
          <el-button 
            type="primary" 
            size="small" 
            @click="openEditModal(scope.row)"
            :disabled="!permission.edit"
          >
            {{ i18n.edit }}
          </el-button>
          <el-button 
            type="danger" 
            size="small" 
            @click="deleteRule(scope.row.id)"
            :disabled="!permission.delete"
          >
            {{ i18n.delete }}
          </el-button>
        </template>
      </el-table-column>
    </el-table>

    <!-- 编辑弹窗 -->
    <el-dialog 
      v-model="state.isModalOpen" 
      :title="state.currentRule ? i18n.editRule : i18n.addRule"
    >
      <el-form :model="state.currentRule || {}" label-width="100px">
        <el-form-item :label="i18n.ruleName" prop="ruleName">
          <el-input v-model="state.currentRule.ruleName" />
        </el-form-item>
      </el-form>
      <template #footer>
        <el-button @click="closeEditModal">{{ i18n.cancel }}</el-button>
        <el-button type="primary" @click="saveRule(state.currentRule)">{{ i18n.save }}</el-button>
      </template>
    </el-dialog>
  </div>
</template>

<script lang="ts">
import { defineComponent } from 'vue';
import { usePresenter } from '@clean-arch/vue-adapter';
import { SortingRulePresenter } from './presenter';
import { AutoSortingRuleModule } from './types';

export default defineComponent({
  name: 'AutoSortingRuleModule',
  props: {
    data: {
      type: Object as () => AutoSortingRuleModule['data'],
      required: true,
    },
    permission: {
      type: Object as () => AutoSortingRuleModule['permission'],
      required: true,
    },
    i18n: {
      type: Object as () => AutoSortingRuleModule['i18n'],
      required: true,
    },
  },
  setup(props) {
    // 初始化Presenter
    const { state, fetchRuleList, openEditModal, closeEditModal, saveRule } = usePresenter(
      SortingRulePresenter,
      props.data
    );

    // 组件挂载时加载列表
    fetchRuleList();

    return {
      state,
      fetchRuleList,
      openEditModal,
      closeEditModal,
      saveRule,
      ...props,
    };
  },
});
</script>
3.4.4 通用 Presenter 实践(列表场景)

通过抽象通用 Presenter,沉淀 80% 的列表场景逻辑,业务模块仅需继承扩展即可

1. 通用列表 Presenter 抽象类
typescript 复制代码
// @clean-arch/core/presenter/abs-table-presenter.ts
import { BasePresenter } from './base-presenter';
import { AbsTableUsecase } from '../usecase/abs-table-usecase';

/**
 * 通用列表状态类型
 */
export interface TableState<Row = any> {
  dataSource: Row[]; // 列表数据
  loading: boolean; // 加载状态
  pagination: { current: number; pageSize: number; total: number }; // 分页信息
  searchParams: Record<string, any>; // 搜索参数
  selectedRows: Row[]; // 选中行
}

/**
 * 通用列表Presenter抽象类
 * 沉淀列表分页、搜索、选中、导出等通用逻辑
 */
export abstract class AbsTablePresenter<Row = any> extends BasePresenter<TableState<Row>> {
  constructor(protected usecase: AbsTableUsecase<Row>) {
    super({
      dataSource: [],
      loading: false,
      pagination: { current: 1, pageSize: 10, total: 0 },
      searchParams: {},
      selectedRows: [],
    });
  }

  /**
   * 加载列表数据
   * @param searchParams 搜索参数
   */
  fetchTable = async (searchParams: Record<string, any> = {}) => {
    this.setState({ loading: true });
    try {
      const { current, pageSize } = this.state.pagination;
      const res = await this.usecase.fetchTable({
        current,
        pageSize,
        ...searchParams,
      });
      this.setState({
        dataSource: res.list,
        pagination: { ...this.state.pagination, total: res.total },
        searchParams,
      });
    } catch (error) {
      console.error('加载列表失败:', error);
      this.setState({ dataSource: [], pagination: { ...this.state.pagination, total: 0 } });
    } finally {
      this.setState({ loading: false });
    }
  };

  /**
   * 切换分页
   * @param pagination 新分页信息
   */
  onPageChange = async (pagination: Partial<TableState<Row>['pagination']>) => {
    this.setState({ pagination: { ...this.state.pagination, ...pagination } });
    await this.fetchTable(this.state.searchParams);
  };

  /**
   * 搜索列表
   * @param searchParams 搜索参数
   */
  onSearch = async (searchParams: Record<string, any>) => {
    // 搜索时重置页码
    this.setState({ pagination: { ...this.state.pagination, current: 1 } });
    await this.fetchTable(searchParams);
  };

  /**
   * 选择行
   * @param selectedRows 选中行
   */
  onSelect = (selectedRows: Row[]) => {
    this.setState({ selectedRows });
  };

  /**
   * 导出列表(通用逻辑,可重写)
   */
  exportTable = async () => {
    this.setState({ loading: true });
    try {
      await this.usecase.exportTable(this.state.searchParams);
    } catch (error) {
      console.error('导出列表失败:', error);
    } finally {
      this.setState({ loading: false });
    }
  };

  /**
   * 扩展点:业务自定义处理列表数据
   * @param data 原始列表数据
   * @returns 处理后的数据
   */
  protected processData?(data: Row[]): Row[];
}
2. 业务列表 Presenter 继承扩展
typescript 复制代码
// modules/auto-sorting-rule/presenter/table-presenter.ts
import { AbsTablePresenter } from '@clean-arch/core';
import { SortingRuleUsecase } from '../usecase/default';

/**
 * 分拣规则列表Presenter
 * 继承通用列表Presenter,补充业务特有逻辑
 */
export class SortingRuleTablePresenter extends AbsTablePresenter {
  constructor(usecase: SortingRuleUsecase) {
    super(usecase);
  }

  /**
   * 重写扩展点:处理列表数据(隐藏TWH仓的敏感字段)
   * @param data 原始数据
   * @returns 处理后的数据
   */
  protected processData = (data: any[]): any[] => {
    const currentWh = getCurrentWh();
    if (currentWh === 'TWH') {
      return data.map(item => {
        delete item.sensitiveField;
        return item;
      });
    }
    return data;
  };

  /**
   * 业务特有逻辑:批量启用规则
   */
  batchEnable = async () => {
    if (this.state.selectedRows.length === 0) return;
    this.setState({ loading: true });
    try {
      const ids = this.state.selectedRows.map(item => item.id);
      await this.usecase.batchEnable(ids);
      await this.fetchTable(this.state.searchParams); // 刷新列表
    } catch (error) {
      console.error('批量启用规则失败:', error);
    } finally {
      this.setState({ loading: false });
    }
  };
}
3.4.5 跨组件通信(Presenter Context)

对于复杂模块(如包含多个子组件),通过 PresenterProvider 实现跨组件共享 Presenter

1. React Context 实现
typescript 复制代码
// @clean-arch/react-adapter/presenter-context.ts
import { createContext, useContext, ReactNode } from 'react';
import { BasePresenter } from '@clean-arch/core';

// 创建Presenter Context
const PresenterContext = createContext<BasePresenter | null>(null);

/**
 * Presenter Provider组件
 * @param props 子组件、Presenter实例
 * @returns Provider组件
 */
export function PresenterProvider({
  presenter,
  children,
}: {
  presenter: BasePresenter;
  children: ReactNode;
}) {
  return (
    <PresenterContext.Provider value={presenter}>
      {children}
    </PresenterContext.Provider>
  );
}

/**
 * 获取Presenter Context
 * @returns Presenter实例
 */
export function usePresenterContext<T extends BasePresenter>(): T {
  const presenter = useContext(PresenterContext);
  if (!presenter) {
    throw new Error('usePresenterContext must be used within a PresenterProvider');
  }
  return presenter as T;
}
2. 跨组件使用示例
typescript 复制代码
// 父组件
import { SortingRulePresenter } from './presenter';
import { PresenterProvider } from '@clean-arch/react-adapter';
import RuleTable from './components/RuleTable';
import RuleForm from './components/RuleForm';

export const AutoSortingRuleModule = (props) => {
  const presenter = new SortingRulePresenter(props.data);
  return (
    <PresenterProvider presenter={presenter}>
      <RuleTable />
      <RuleForm />
    </PresenterProvider>
  );
};

// 子组件:RuleTable
import { usePresenterContext } from '@clean-arch/react-adapter';
import { SortingRulePresenter } from '../presenter';

export const RuleTable = () => {
  const presenter = usePresenterContext<SortingRulePresenter>();
  const state = presenter.getState();
  
  return (
    <table>
      <tbody>
        {state.ruleList.map(rule => (
          <tr key={rule.id}>
            <td>{rule.ruleName}</td>
            <td>
              <button onClick={() => presenter.openEditModal(rule)}>编辑</button>
            </td>
          </tr>
        ))}
      </tbody>
    </table>
  );
};

// 子组件:RuleForm
import { usePresenterContext } from '@clean-arch/react-adapter';
import { SortingRulePresenter } from '../presenter';

export const RuleForm = () => {
  const presenter = usePresenterContext<SortingRulePresenter>();
  const state = presenter.getState();
  
  const handleSave = async (formData) => {
    await presenter.saveRule(formData);
  };
  
  return (
    <form onSubmit={(e) => {
      e.preventDefault();
      handleSave(state.currentRule);
    }}>
      <input 
        type="text" 
        value={state.currentRule?.ruleName || ''} 
        onChange={(e) => presenter.setState({
          currentRule: { ...state.currentRule, ruleName: e.target.value }
        })}
      />
      <button type="submit">保存</button>
    </form>
  );
};

四、使用方法(完整落地步骤)

4.1 环境准备

1. 安装核心 SDK
bash 复制代码
npm install @clean-arch/core @clean-arch/vue-adapter @clean-arch/react-adapter --save
2. 安装辅助工具
bash 复制代码
npm install json-schema-to-typescript ajv lodash --save-dev
3. 配置 git hook(可选,用于配置校验)
bash 复制代码
npm install husky lint-staged --save-dev
npx husky install
npx husky add .husky/pre-commit "npm run validate:config"

4.2 第一步:注册渲染器

在框架入口文件(main.ts / main.js)中注册通用渲染器

Vue 注册示例
typescript 复制代码
// src/main.ts (Vue)
import { createApp } from 'vue';
import App from './App.vue';
import Renderer from '@clean-arch/vue-adapter/vue-moduler/Renderer';

const app = createApp(App);
// 注册全局渲染器组件
app.component('Renderer', Renderer);
app.mount('#app');
React 注册示例
typescript 复制代码
// src/index.tsx (React)
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App.tsx';
import { Renderer } from '@clean-arch/react-adapter/react-moduler/Renderer';

// 注册全局渲染器组件(或直接在页面中引入)
ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

4.3 第二步:开发标准化模块

以 "订单拦截池" 模块为例,完整开发流程如下

步骤 1:创建模块目录
bash 复制代码
mkdir -p modules/order-interception/{presenter,usecase,repository}
touch modules/order-interception/{index.vue,schema.json,types.ts,fields.ts,usecase-map.ts}
touch modules/order-interception/presenter/index.ts
touch modules/order-interception/usecase/{default.ts,for-twh.ts}
touch modules/order-interception/repository/{default.ts,for-twh.ts}
步骤 2:编写 Schema 文件
json 复制代码
// modules/order-interception/schema.json
{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "OrderInterceptionModule",
  "description": "订单拦截池模块",
  "type": "object",
  "properties": {
    "data": {
      "type": "object",
      "properties": {
        "showCrossBorder": {
          "type": "boolean",
          "description": "是否显示跨境字段",
          "default": false
        },
        "useCase": {
          "type": "string",
          "enum": ["default", "for-twh"],
          "default": "default"
        }
      },
      "required": ["useCase"]
    },
    "permission": {
      "type": "object",
      "default": {}
    },
    "i18n": {
      "type": "object",
      "required": ["title"]
    }
  },
  "required": ["data", "i18n"]
}
步骤 3:生成 TypeScript 类型
bash 复制代码
npm run generate:types
步骤 4:编写 Presenter
typescript 复制代码
// modules/order-interception/presenter/index.ts
import { AbsTablePresenter } from '@clean-arch/core';
import { OrderInterceptionUsecase } from '../usecase/default';

export class OrderInterceptionPresenter extends AbsTablePresenter {
  constructor(usecase: OrderInterceptionUsecase) {
    super(usecase);
  }

  // 业务特有逻辑:拦截订单
  interceptOrder = async (orderId: string, reason: string) => {
    this.setState({ loading: true });
    try {
      await this.usecase.interceptOrder(orderId, reason);
      await this.fetchTable(this.state.searchParams);
    } finally {
      this.setState({ loading: false });
    }
  };
}
步骤 5:编写 UseCase 和 Repository
typescript 复制代码
// modules/order-interception/usecase/default.ts
import { AbsTableUsecase } from '@clean-arch/core';
import { OrderInterceptionRepository } from '../repository/default';

export class OrderInterceptionUsecase extends AbsTableUsecase {
  private repository: OrderInterceptionRepository;

  constructor() {
    super();
    this.repository = new OrderInterceptionRepository();
  }

  async fetchTable(params) {
    return this.repository.getOrderList(params);
  }

  async interceptOrder(orderId: string, reason: string) {
    return this.repository.interceptOrder(orderId, reason);
  }
}

// modules/order-interception/repository/default.ts
import { httpAdapter } from '@clean-arch/core/repository/http-adapter';

export class OrderInterceptionRepository {
  async getOrderList(params) {
    return httpAdapter.get('/api/order/interception/list', { params });
  }

  async interceptOrder(orderId: string, reason: string) {
    return httpAdapter.post('/api/order/interception', { orderId, reason });
  }
}
步骤 6:编写模块入口文件
typescript 复制代码
<!-- modules/order-interception/index.vue -->
<template>
  <div class="order-interception-module">
    <!-- 加载状态 -->
    <el-loading v-if="state.loading" fullscreen text="加载中..." />
    
    <!-- 搜索区域 -->
    <el-form :model="state.searchParams" inline @submit.prevent="onSearch(state.searchParams)">
      <el-form-item :label="i18n.orderNo">
        <el-input v-model="state.searchParams.orderNo" placeholder="请输入订单号" />
      </el-form-item>
      <el-form-item :label="i18n.status">
        <el-select v-model="state.searchParams.status" placeholder="请选择状态">
          <el-option label="未拦截" value="0" />
          <el-option label="已拦截" value="1" />
        </el-select>
      </el-form-item>
      <el-form-item>
        <el-button type="primary" @click="onSearch(state.searchParams)">{{ i18n.search }}</el-button>
        <el-button @click="onSearch({})">{{ i18n.reset }}</el-button>
        <el-button type="success" @click="exportTable()" :disabled="!permission.export">{{ i18n.export }}</el-button>
      </el-form-item>
    </el-form>

    <!-- 列表区域 -->
    <el-table 
      :data="state.dataSource" 
      border 
      @selection-change="onSelect"
      :loading="state.loading"
    >
      <el-table-column type="selection" width="55" />
      <el-table-column prop="orderNo" :label="i18n.orderNo" width="180" />
      <el-table-column prop="createTime" :label="i18n.createTime" width="200" />
      <el-table-column prop="status" :label="i18n.status" width="120">
        <template #default="scope">
          <el-tag :type="scope.row.status === '1' ? 'success' : 'warning'">
            {{ scope.row.status === '1' ? i18n.intercepted : i18n.unIntercepted }}
          </el-tag>
        </template>
      </el-table-column>
      <el-table-column 
        prop="crossBorder" 
        :label="i18n.crossBorder" 
        v-if="data.showCrossBorder" 
        width="120"
      >
        <template #default="scope">
          <el-tag type="info">{{ scope.row.crossBorder ? i18n.yes : i18n.no }}</el-tag>
        </template>
      </el-table-column>
      <el-table-column label="操作" width="180">
        <template #default="scope">
          <el-button 
            type="primary" 
            size="small" 
            @click="interceptOrder(scope.row.orderNo, '手动拦截')"
            :disabled="!permission.intercept || scope.row.status === '1'"
          >
            {{ i18n.intercept }}
          </el-button>
          <el-button 
            type="text" 
            size="small" 
            @click="onSearch({ orderNo: scope.row.orderNo })"
          >
            {{ i18n.viewDetail }}
          </el-button>
        </template>
      </el-table-column>
    </el-table>

    <!-- 分页区域 -->
    <el-pagination
      @size-change="handleSizeChange"
      @current-change="handleCurrentChange"
      :current-page="state.pagination.current"
      :page-sizes="[10, 20, 50, 100]"
      :page-size="state.pagination.pageSize"
      :total="state.pagination.total"
      layout="total, sizes, prev, pager, next, jumper"
      style="margin-top: 16px; text-align: right;"
    />
  </div>
</template>

<script lang="ts">
import { defineComponent } from 'vue';
import { usePresenter } from '@clean-arch/vue-adapter';
import { OrderInterceptionPresenter } from './presenter';
import { OrderInterceptionModule } from './types';
import { OrderInterceptionUsecase } from './usecase/default';
import { OrderInterceptionUsecaseForTWH } from './usecase/for-twh';
import { USECASE_MAP } from './usecase-map';

export default defineComponent({
  name: 'OrderInterceptionModule',
  props: {
    data: {
      type: Object as () => OrderInterceptionModule['data'],
      required: true,
    },
    permission: {
      type: Object as () => OrderInterceptionModule['permission'],
      required: true,
    },
    i18n: {
      type: Object as () => OrderInterceptionModule['i18n'],
      required: true,
    },
  },
  setup(props) {
    // 根据模块配置的useCase,初始化对应的UseCase实例
    let useCase;
    if (props.data.useCase === 'for-twh') {
      useCase = new OrderInterceptionUsecaseForTWH();
    } else {
      useCase = new OrderInterceptionUsecase();
    }

    // 初始化Presenter
    const { 
      state, 
      fetchTable, 
      onSearch, 
      onSelect, 
      exportTable, 
      interceptOrder 
    } = usePresenter(OrderInterceptionPresenter, useCase);

    // 分页大小变更
    const handleSizeChange = (pageSize: number) => {
      state.pagination.pageSize = pageSize;
      fetchTable(state.searchParams);
    };

    // 当前页变更
    const handleCurrentChange = (current: number) => {
      state.pagination.current = current;
      fetchTable(state.searchParams);
    };

    // 组件挂载时加载初始数据
    fetchTable();

    return {
      state,
      fetchTable,
      onSearch,
      onSelect,
      exportTable,
      interceptOrder,
      handleSizeChange,
      handleCurrentChange,
      ...props,
    };
  },
});
</script>

<style scoped lang="less">
.order-interception-module {
  padding: 16px;
  .el-form {
    margin-bottom: 16px;
  }
}
</style>
步骤 7:编写 UseCase 映射表
typescript 复制代码
// modules/order-interception/usecase-map.ts
import { OrderInterceptionUsecase } from './usecase/default';
import { OrderInterceptionUsecaseForTWH } from './usecase/for-twh';

/**
 * UseCase映射表
 * 关联用例名称与具体实现
 */
export const USECASE_MAP = {
  default: OrderInterceptionUsecase,
  'for-twh': OrderInterceptionUsecaseForTWH,
};

4.4 第三步:编写页面(协议化配置 + 入口)

步骤 1:创建页面目录
bash 复制代码
mkdir -p views/order-interception
touch views/order-interception/{index.vue,index.config.ts,constants.ts,module-map.ts}
步骤 2:编写页面常量与模块映射
typescript 复制代码
// views/order-interception/constants.ts
/**
 * 模块ID枚举(页面内唯一)
 */
export enum ModuleId {
  ORDER_INTERCEPTION = 'order-interception',
}
typescript 复制代码
// views/order-interception/module-map.ts
import { ModuleId } from './constants';

/**
 * 模块映射表
 * 关联模块ID与模块基础信息
 */
export const MODULE_MAP = {
  [ModuleId.ORDER_INTERCEPTION]: {
    name: 'OrderInterception',
    component: () => import('../../modules/order-interception/index.vue'),
  },
};
步骤 3:编写页面协议配置
typescript 复制代码
// views/order-interception/index.config.ts
import { ModuleId } from './constants';
import { MODULE_MAP } from './module-map';
import { Api } from '@/api';

/**
 * 页面协议配置
 * 差异化场景:TWH仓显示跨境字段,TWDC仓启用批量拦截权限
 */
const pageConfig = {
  default: {
    pageInfo: {
      title: '订单拦截池',
      breadcrumb: ['订单管理', '订单拦截池'],
      keepAlive: true,
    },
    modules: [
      {
        id: ModuleId.ORDER_INTERCEPTION,
        ...MODULE_MAP[ModuleId.ORDER_INTERCEPTION],
        visible: true,
        order: 1,
      },
    ],
    data: {
      [ModuleId.ORDER_INTERCEPTION]: {
        data: {
          showCrossBorder: false,
          useCase: 'default',
          exportURL: Api.ORDER_INTERCEPTION_EXPORT,
        },
        permission: {
          intercept: 'order.interception.intercept',
          export: 'order.interception.export',
          batchIntercept: false, // 默认禁用批量拦截
        },
        i18n: {
          title: 'order.interception.title',
          orderNo: 'order.interception.orderNo',
          createTime: 'order.interception.createTime',
          status: 'order.interception.status',
          crossBorder: 'order.interception.crossBorder',
          intercepted: 'order.interception.intercepted',
          unIntercepted: 'order.interception.unIntercepted',
          yes: 'common.yes',
          no: 'common.no',
          search: 'common.search',
          reset: 'common.reset',
          export: 'common.export',
          intercept: 'order.interception.intercept',
          viewDetail: 'common.viewDetail',
        },
      },
    },
  },
  // TWH仓配置(显示跨境字段,使用专属UseCase)
  'TWH': {
    data: {
      [ModuleId.ORDER_INTERCEPTION]: {
        data: {
          showCrossBorder: true,
          useCase: 'for-twh',
        },
      },
    },
  },
  // TWDC仓配置(启用批量拦截权限)
  'TWDC': {
    data: {
      [ModuleId.ORDER_INTERCEPTION]: {
        permission: {
          batchIntercept: 'order.interception.batchIntercept',
        },
      },
    },
  },
};

export default pageConfig;
export { ModuleId };
步骤 4:编写页面入口文件
typescript 复制代码
<!-- views/order-interception/index.vue -->
<template>
  <div class="order-interception-page">
    <!-- 模块渲染器:根据配置动态渲染模块 -->
    <Renderer :modules="modules" />
  </div>
</template>

<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import { Module } from '@clean-arch/vue-adapter';
import config, { ModuleId } from './index.config';
import { getCurrentWh } from '@/utils/warehouse';

/**
 * 订单拦截池页面
 * 基于协议化配置动态渲染模块
 */
@Module({
  config,
  currentKey: getCurrentWh(), // 当前场景关键字(仓编码)
})
@Component({
  name: 'OrderInterceptionPage',
})
export default class OrderInterceptionPage extends Vue {
  // 暴露模块ID供模板使用(可选)
  ModuleId = ModuleId;

  // 模块渲染器所需的模块列表(由@Module装饰器注入)
  modules = this.$options.modules || [];
}
</script>

<style scoped lang="less">
.order-interception-page {
  height: 100%;
  overflow: auto;
}
</style>

4.5 第四步:路由配置与访问

步骤 1:配置 Vue 路由
typescript 复制代码
// src/router/index.ts
import Vue from 'vue';
import Router from 'vue-router';
import OrderInterceptionPage from '@/views/order-interception/index.vue';

Vue.use(Router);

export default new Router({
  routes: [
    {
      path: '/order/interception',
      name: 'OrderInterception',
      component: OrderInterceptionPage,
      meta: { title: '订单拦截池' },
    },
  ],
});
步骤 2:访问验证

启动项目后,访问 /order/interception

  • 访问 TWH 仓:显示跨境字段,使用 for-twh UseCase
  • 访问 TWDC 仓:启用批量拦截按钮
  • 访问其他仓:隐藏跨境字段,使用默认 UseCase

五、Q&A

Q1:配置文件中多个场景冲突时,优先级如何确定?

优先级遵循 "精准匹配> 多场景合并 > 默认配置"

  1. 精准匹配 :如同时配置了 TWHTWH,TWHN,访问 TWH 仓时优先使用 TWH 配置
  2. 多场景合并 :如配置 TWH,TWHN,访问 TWH 仓时合并两个场景的配置,后出现的场景覆盖先出现的
  3. 兜底 :未匹配到任何场景时,使用 default 配置

Q2:如何处理模块间的数据通信?

支持两种通信方式

  1. 全局状态管理 :通过 SDKcore/common/store 封装全局状态(框架无关),Vue / React 均通过适配层调用

  2. 模块事件总线SDK 提供 EventBus 工具,模块间通过 "发布 - 订阅" 模式通信,示例:

    typescript 复制代码
    // 模块A发布事件
    import { eventBus } from '@clean-arch/core';
    eventBus.emit('order:intercepted', { orderNo: 'OD123456' });
    
    // 模块B订阅事件
    eventBus.on('order:intercepted', (data) => {
      console.log('订单拦截通知:', data.orderNo);
    });

Q3:UseCase 切换失败可能的原因有哪些?

常见原因及解决方案

  • UseCase 名称与 usecase-map.ts 映射不一致 :检查配置中的 useCase 字段与映射表 key 完全匹配
  • 模块未导入对应的 UseCase :确保 usecase-map.ts 中导入了所有场景的 UseCase 实现
  • UseCase 未继承 AbsTableUsecase :通用 Presenter 依赖抽象类的 fetchTable 方法,需确保继承关系正确

Q4:如何新增一个新场景(如 TWHX 仓)?

无需修改模块代码,仅需 3 步

  1. 在页面配置文件中新增 TWHX 场景配置
  2. 配置差异化字段(如 showCrossBorder: true
  3. 部署后,访问 TWHX 仓自动加载新配置。

Q5:双框架下如何保证 UI 风格一致?

通过两层保障

  1. 设计规范统一 :制定全局设计规范(颜色、间距、字体),Vue / React 模块均遵循
  2. 组件适配层SDKvue-adapterreact-adapter 分别封装框架专属组件(如 Vueelement-uiReactantd),但对外暴露一致的 Props 和样式类名

六、总结

6.1 方案核心特点

本方案通过 "页面协议化 + 模块标准化 + 分层架构" 的组合,解决了多场景、多框架下的前端开发痛点

  1. 跨框架复用SDK 适配层实现一套核心逻辑支持 Vue / React,避免重复开发
  2. 协议化配置:多场景差异化通过配置文件实现,零代码适配,新增场景成本极低
  3. 模块标准化 :模块目录、Schema、分层逻辑统一,降低团队协作成本
  4. 分层解耦"View → Presenter → UseCase → Repository" 职责清晰,代码可维护性大幅提升
  5. 增量落地:支持局部模块改造,无需全局重构,存量系统迁移成本低

6.2 核心价值(业务视角)

  1. 开发效率提升 :新场景开发从 "重复编码" 变为 "配置调整",提升研发效率
  2. 维护成本降低通用逻辑集中维护,缩短线上故障修复时间,运维成本降低
  3. 技术栈迁移平滑VueReact 迁移时,核心业务逻辑无需修改,仅需开发适配层,降低迁移成本
  4. 团队协作高效 :标准化规范减少 "个性化代码",缩短新成员上手时间
  5. 业务扩展灵活:支持多仓、多角色、多业务线的差异化扩展,支撑业务快速迭代

6.3 适用场景(精准匹配)

  1. 多场景业务系统:如多仓、多区域、多租户的管理系统(如仓储管理、订单管理)
  2. 技术栈迁移过渡期Vue / React 双框架并行,需复用通用能力的项目
  3. 模块化开发需求:需沉淀通用组件 / 逻辑,支持跨项目复用的团队
  4. 多角色权限系统 :不同角色需差异化展示 UI / 功能的系统(如管理员 / 操作员 / 查看员)
  5. 存量系统改造 :代码臃肿、维护困难,需增量优化的存量 Vue / React 项目

6.4 注意事项(落地避坑)

  1. 配置规范执行:严格遵循协议配置的字段命名、结构规范,避免配置冲突
  2. Schema 强制校验 :模块必须定义 Schema,且通过构建脚本校验,避免类型错误
  3. UseCase 边界控制UseCase 仅处理业务逻辑,禁止包含 UI 相关代码,确保框架无关
  4. 适配层最小实现:适配层仅适配框架调用方式,不修改核心逻辑,保持核心层纯净
  5. 配置版本管理:差异化配置需纳入版本控制,避免多人协作时冲突

6.5 后续优化建议( roadmap )

  1. 工具链自动化 :开发脚手架(如 create-clean-module),一键生成模块目录、SchemaTypeScript 类型
  2. 可视化配置平台 :开发 Web 界面管理页面 / 模块配置,非技术人员也可配置场景差异化
  3. 性能优化
    • 模块懒加载优化:支持按场景动态加载模块,减少首屏加载时间
    • 状态缓存优化Presenter 状态支持持久化,页面刷新后恢复状态
  4. 生态扩展
    • 支持更多框架 :如 Vue 3、React 18+、Svelte
    • 集成低代码平台:模块支持低代码拖拽配置,进一步降低开发成本
  5. 监控体系完善:新增配置变更监控、模块性能监控,提升系统稳定性

6.6 总结

本方案通过 "页面协议化 + 模块标准化 + 分层架构" 的设计,从根本上解决了多场景、多框架下前端开发的 "差异化与标准化" 矛盾,实现了 "通用能力沉淀复用、差异化场景零代码适配、技术栈迁移平滑过渡" 的核心目标。

方案的核心不是 "过度设计" ,而是 "工程化解决业务痛点" ------ 通过标准化规范降低协作成本,通过配置化适配业务差异,通过分层解耦提升代码质量。落地后,不仅能提升研发效率、降低维护成本,还能支撑业务快速迭代,为技术栈迁移、业务扩展提供灵活的架构支撑。

对于面临类似痛点的团队,建议采用 "增量落地" 策略:先选择 1-2 个核心模块试点改造,验证方案价值后再全局推广,既能降低风险,又能快速看到业务收益。

相关推荐
崔庆才丨静觅7 小时前
稳定好用的 ADSL 拨号代理,就这家了!
前端
江湖有缘7 小时前
Docker部署music-tag-web音乐标签编辑器
前端·docker·编辑器
代码游侠7 小时前
复习——Linux设备驱动开发笔记
linux·arm开发·驱动开发·笔记·嵌入式硬件·架构
恋猫de小郭8 小时前
Flutter Zero 是什么?它的出现有什么意义?为什么你需要了解下?
android·前端·flutter
崔庆才丨静觅15 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby606116 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了16 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅16 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅16 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端