领域模型架构建设(低代码平台)

9 阅读4分钟

** 领域模型架构建设(低代码平台)**

一、概述

基于 vue3,使用 DSL 驱动的低代码平台,通过配置化方式可以实现业务系统的快速搭建,减少crud工作量,聚焦于定制化页面的开发。

1.企业级后台痛点

  1. 重复开发:每个业务模块(商品管理、订单管理、用户管理)都需要从零搭建
  2. 维护困难:菜单、路由、表格列配置分散在各处代码中
  3. 扩展性差:新增一个项目需要复制大量代码
  4. 多租户困境:同一套业务逻辑,不同客户需要不同的定制

2.DSL领域模型解决思路

用声明式的数据结构来描述业务领域,系统根据这些描述自动生成功能。

传统方式:代码驱动 → 手写每个页面和功能 领域模型:配置驱动 → 定义模型 → 自动生成页面和功能

二、结构设计

1. DSL 的定义

在 elips 项目中,我们设计了一套 仪表盘领域特定语言(Dashboard DSL) ,用 JavaScript 对象来描述整个后台站点的结构。

核心 DSL 定义位于 dashboard-model.js:

module.exports = {
  mode: "dashboard", // 模板类型:不同模板对应不同的渲染规则
  name: "", // 站点名称
  desc: "", // 站点描述
  icon: "", // 站点图标
  homePage: "", // 首页路径
​
  // 核心:菜单结构(驱动整个站点的版块划分)
  menu: [
    {
      key: "", // 菜单唯一标识
      name: "", // 菜单名称
      menuType: "", // 枚举:group(分组)/ module(模块)
      // ... 详细配置
    },
  ],
};

2. 菜单类型系统

DSL 设计的关键在于 菜单类型(menuType)模块类型(moduleType) 的组合:

menuType菜单类型
group分组菜单,包含 subMenu 子菜单(支持递归嵌套
module功能模块,根据 moduleType 决定渲染方式
moduleType模块类型
sider带侧边栏的复合布局,内部可嵌套其他模块
iframe嵌入外部页面(适合集成第三方系统)
custom自定义路由页面(开发者手写组件)
schemaSchema 驱动的 CRUD 页面(零代码)

3. 模块配置详解

每种模块类型都有对应的配置块:

{
    // 侧边栏配置:支持嵌套菜单结构
    siderConfig: {
        menu: [/* 递归菜单结构 */]
    },
​
    // iframe 配置:嵌入外部页面
    iframeConfig: {
        path: 'https://example.com'
    },
​
    // 自定义路由配置
    customConfig: {
        path: '/todo'
    },
​
    // Schema 配置:数据驱动的 CRUD
    schemaConfig: {
        api: '/api/resource',  // RESTFUL API 地址
        schema: {
            type: 'object',
            properties: {
                fieldName: {
                    type: 'string',
                    label: '字段名称',
                    tableOptions: {
                        visible: true
                    }
                }
            }
        }
    }
}

三、Model-Project 继承体系

1. 双层架构设计

elips 采用了 Model(模型)→ Project(项目) 的两层继承架构:

                    ┌─────────────────┐
                    │   Model 模型     │
                    │  (领域模板)     │
                    └────────┬────────┘
                             │ 继承
          ┌──────────────────┼──────────────────┐
          ▼                  ▼                  ▼
  ┌───────────────┐  ┌───────────────┐  ┌───────────────┐
  │  Project A    │  │  Project B    │  │  Project C    │
  │  (京东电商)  │  │  (拼多多)    │  │  (淘宝)     │
  └───────────────┘  └───────────────┘  └───────────────┘

2. 实际示例

基础模型定义

module.exports = {
  model: "dashboard",
  name: "电商系统",
  menu: [
    {
      key: "product",
      name: "商品管理",
      menuType: "module",
      moduleType: "custom",
      customConfig: { path: "todo" },
    },
    {
      key: "order",
      name: "订单管理",
      // ...
    },
    {
      key: "client",
      name: "客户管理",
      // ...
    },
  ],
};

项目扩展配置

module.exports = {
  name: "拼多多",
  desc: "拼多多电商",
  homePage: "/todo?proj_key=pdd&key=product",
  menu: [
    // 重载:修改继承的菜单项
    {
      key: "product",
      name: "商品管理(PDD)", // 覆盖名称
    },
    // 重载:增强客户管理功能
    {
      key: "client",
      name: "客户管理(PDD)",
      moduleType: "schema", // 升级为 Schema 驱动
      schemaConfig: {
        api: "/api/pdd/client",
        schema: {},
      },
    },
    // 新增:项目特有的功能模块
    {
      key: "data",
      name: "数据分析",
      menuType: "module",
      moduleType: "sider",
      siderConfig: {
        menu: [
          /* 侧边栏菜单 */
        ],
      },
    },
  ],
};

3. 继承合并算法

模型继承的核心逻辑位于

const projectExtendModel = (model, project) => {
  return _.mergeWith({}, model, project, (modelValue, projValue) => {
    // 数组合并的特殊处理
    if (Array.isArray(modelValue) && Array.isArray(projValue)) {
      let res = [];
​
      // 规则 1:model 有的,project 也有 → 递归合并(重载)
      // 规则 2:model 有的,project 没有 → 保留(继承)
      for (let i = 0; i < modelValue.length; i++) {
        const modelItem = modelValue[i];
        const projItem = projValue.find((p) => p.key === modelItem.key);
        res.push(
          projItem ? projectExtendModel(modelItem, projItem) : modelItem
        );
      }
​
      // 规则 3:project 有的,model 没有 → 添加(扩展)
      for (let i = 0; i < projValue.length; i++) {
        const projItem = projValue[i];
        const modelItem = modelValue.find((m) => m.key === projItem.key);
        if (!modelItem) res.push(projItem);
      }
​
      return res;
    }
  });
};

继承规则总结:

场景行为说明
Model 有,Project 也有重载Project 的配置覆盖 Model
Model 有,Project 没有继承直接使用 Model 的配置
Model 没有,Project 有扩展新增 Project 特有功能

四、映射机制

1. 后端:模型加载与 API 暴露

服务层

const modelList = require("../../model/index.js")(app);
​
class ProjectService {
  // 根据项目 key 获取完整配置
  get(projKey) {
    let projConfig;
    modelList.forEach((modelItem) => {
      if (modelItem.project[projKey]) {
        projConfig = modelItem.project[projKey];
      }
    });
    return projConfig;
  }
}

控制器层

// GET /api/project?proj_key=pdd
get(ctx) {
    const { proj_key: projKey } = ctx.request.query
    const projConfig = projectService.get(projKey)
    this.success(ctx, projConfig)
}

2. 前端:动态路由与组件映射

入口路由配置

const routes = [];
​
const basePath = "/view/dashboard";
const pathOption = [{
  pathName: 'iframe',
  comPath: () => import('./complex-view/iframe-view/iframe-view.vue')
},
{
  pathName: 'schema',
  comPath: () => import('./complex-view/schema-view/schema-view.vue')
},
{
  pathName: 'todo',
  comPath: () => import('./todo/todo.vue')
}]
​
const routeList = []
const siderRouteList = []
pathOption.forEach(item => {
  const component = item.comPath
  const routeItem = {
    path: `${basePath}/${item.pathName}`,
    component,
  }
  const siderRouteItem = {
    path: item.pathName,
    component,
  }
  routeList.push(routeItem)
  siderRouteList.push(siderRouteItem)
})
routes.push(...routeList);
​
// 侧边栏菜单路由
routes.push({
  path: `${basePath}/sider`,
  component: () => import("./complex-view/sider-view/sider-view.vue"),
  children: siderRouteList,
});

菜单选择处理

const onMenuSelect = (menuItem) => {
  const { moduleType, key, customConfig } = menuItem;
​
  // moduleType 到路由的映射表
  const pathMap = {
    sider: "/sider",
    iframe: "/iframe",
    schema: "/schema",
    custom: customConfig?.path, // 自定义路径
  };
​
  router.push({
    path: pathMap[moduleType],
    query: { key, proj_key: route.query.proj_key },
  });
};