Elips:领域模型与 DSL 设计实践:从配置到站点的优雅映射

本文基于一个企业级后台管理框架 elips 的实际开发经验,分享领域模型驱动开发(Domain Model Driven Development)的设计思路,重点讲解如何通过 DSL(Domain Specific Language)实现配置化驱动的多租户站点生成。

一、为什么需要领域模型?

1.1 企业级后台的痛点

在开发企业级后台管理系统时,我们经常面临以下问题:

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

1.2 领域模型的解决思路

领域模型的核心思想是:用声明式的数据结构来描述业务领域,让系统根据这些描述自动生成功能

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

二、DSL 结构设计

2.1 DSL 的定义

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

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

javascript 复制代码
module.exports = {
  mode: "dashboard", // 模板类型:不同模板对应不同的渲染规则
  name: "", // 站点名称
  desc: "", // 站点描述
  icon: "", // 站点图标
  homePage: "", // 首页路径

  // 核心:菜单结构(驱动整个站点的版块划分)
  menu: [
    {
      key: "", // 菜单唯一标识
      name: "", // 菜单名称
      menuType: "", // 枚举:group(分组)/ module(模块)
      // ... 详细配置
    },
  ],
};

2.2 菜单类型系统

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

vbnet 复制代码
┌─────────────────────────────────────────────────────────────┐
│                      menuType 菜单类型                       │
├─────────────┬───────────────────────────────────────────────┤
│   group     │ 分组菜单,包含 subMenu 子菜单(支持递归嵌套)   │
├─────────────┼───────────────────────────────────────────────┤
│   module    │ 功能模块,根据 moduleType 决定渲染方式          │
└─────────────┴───────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│                     moduleType 模块类型                      │
├─────────────┬───────────────────────────────────────────────┤
│   sider     │ 带侧边栏的复合布局,内部可嵌套其他模块           │
├─────────────┼───────────────────────────────────────────────┤
│   iframe    │ 嵌入外部页面(适合集成第三方系统)               │
├─────────────┼───────────────────────────────────────────────┤
│   custom    │ 自定义路由页面(开发者手写组件)                 │
├─────────────┼───────────────────────────────────────────────┤
│   schema    │ Schema 驱动的 CRUD 页面(零代码)               │
└─────────────┴───────────────────────────────────────────────┘

2.3 模块配置详解

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

javascript 复制代码
{
    // 侧边栏配置:支持嵌套菜单结构
    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 继承体系

3.1 双层架构设计

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

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

3.2 实际示例

基础模型定义 - bussiness/model.js

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

项目扩展配置 - bussiness/project/pdd.js

javascript 复制代码
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.3 继承合并算法

模型继承的核心逻辑位于 model/index.js

javascript 复制代码
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 特有功能

四、DSL 到视图的映射机制

4.1 整体数据流

bash 复制代码
┌─────────────────────────────────────────────────────────────────────────┐
│                           数据流向                                       │
└─────────────────────────────────────────────────────────────────────────┘

  model/*.js     model/index.js      API            前端 Store      视图
     │               │                │                 │            │
     │ 加载 ────────▶│                │                 │            │
     │               │ 解析/继承      │                 │            │
     │               │───────────────▶│ /api/project   │            │
     │               │                │────────────────▶│ menuStore  │
     │               │                │                 │───────────▶│
     │               │                │                 │            │ 渲染菜单
     │               │                │                 │            │ 根据 moduleType
     │               │                │                 │            │ 路由到对应组件

4.2 后端:模型加载与 API 暴露

服务层 - service/project.js

javascript 复制代码
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;
  }
}

控制器层 - controller/project.js

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

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

入口路由配置 - entry.dashboard.js

javascript 复制代码
const routes = []

// moduleType === 'iframe' → iframe-view 组件
routes.push({
    path: '/iframe',
    component: () => import('./complex-view/iframe-view/iframe-view.vue')
})

// moduleType === 'schema' → schema-view 组件
routes.push({
    path: '/schema',
    component: () => import('./complex-view/schema-view/schema-view.vue')
})

// moduleType === 'sider' → sider-view 组件(支持嵌套路由)
routes.push({
    path: '/sider',
    component: () => import('./complex-view/sider-view/sider-view.vue'),
    children: [
        { path: 'iframe', component: /* iframe-view */ },
        { path: 'schema', component: /* schema-view */ },
        { path: 'todo', component: /* 自定义组件 */ }
    ]
})

// custom 类型:直接使用 customConfig.path
routes.push({
    path: '/todo',
    component: () => import('./todo/todo.vue')
})

菜单选择处理 - dashboard.vue

javascript 复制代码
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 },
  });
};

4.4 视图层级结构

sql 复制代码
┌─────────────────────────────────────────────────────────────────────────┐
│                           dashboard.vue                                  │
│  ┌───────────────────────────────────────────────────────────────────┐  │
│  │                    header-view(顶部导航栏)                        │  │
│  │  ┌─────────────────────────────────────────────────────────────┐  │  │
│  │  │ 菜单项 A (group) │ 菜单项 B (module) │ 菜单项 C (module) │ ... │  │  │
│  │  └─────────────────────────────────────────────────────────────┘  │  │
│  └───────────────────────────────────────────────────────────────────┘  │
│  ┌───────────────────────────────────────────────────────────────────┐  │
│  │                        <router-view>                               │  │
│  │                                                                    │  │
│  │   ┌─ iframe-view ──┐  ┌─ schema-view ─┐  ┌─── sider-view ────────┐│  │
│  │   │   <iframe>     │  │  动态表格/表单  │  │ 侧边栏 │ 内容区域    ││  │
│  │   └────────────────┘  └────────────────┘  │        │ <router-view>││  │
│  │                                           └────────┴──────────────┘│  │
│  └───────────────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────────────┘

五、设计亮点与最佳实践

5.1 高度声明式

开发者只需编写 DSL 配置,无需关心渲染细节:

javascript 复制代码
// 仅需 10 行配置,即可生成一个完整的 CRUD 模块
{
    key: 'user',
    name: '用户管理',
    menuType: 'module',
    moduleType: 'schema',
    schemaConfig: {
        api: '/api/users',
        schema: { /* JSON Schema */ }
    }
}

5.2 递归嵌套支持

菜单结构支持任意深度的嵌套,侧边栏内还可以有侧边栏:

javascript 复制代码
{
    key: 'level1',
    menuType: 'group',
    subMenu: [
        {
            key: 'level2',
            moduleType: 'sider',
            siderConfig: {
                menu: [
                    {
                        key: 'level3',
                        menuType: 'group',
                        subMenu: [/* ... */]
                    }
                ]
            }
        }
    ]
}

5.3 多租户一键切换

前端通过 proj_key 参数动态加载不同项目的配置:

bash 复制代码
http://localhost:8080/view/dashboard#/todo?proj_key=pdd
http://localhost:8080/view/dashboard#/todo?proj_key=jd

5.4 渐进式增强

从简单的自定义页面到完全配置化的 Schema CRUD,开发者可以根据需要选择合适的方案:

场景 推荐方案 开发成本
简单展示页 custom 需手写组件
集成第三方 iframe 零代码
标准 CRUD schema 配置驱动
复杂布局 sider + 嵌套 组合配置

六、总结

领域模型驱动开发的核心价值在于:

  1. 知识沉淀:将业务领域的理解固化为可复用的模型
  2. 开发提效:新项目只需继承和扩展,无需重复造轮子
  3. 维护便捷:配置集中管理,改一处生效到处
  4. 沟通统一:DSL 作为技术和业务的共同语言

elips 项目通过 Model-Project 的继承体系和多类型菜单模块的设计,实现了从一份配置自动生成完整后台站点的能力。这种设计模式特别适合:

  • 多租户 SaaS 应用
  • 企业内部多业务线管理系统
  • 低代码/配置化平台

希望本文对你理解领域模型设计有所帮助!


作者:小高 项目来源:elips -哲玄全栈

相关推荐
远瞻。1 天前
【博客】前端新手如何创建自己的个人网站相册
前端·docker·博客·反向代理
青莲8431 天前
Java并发编程基础与进阶(线程·锁·原子类·通信)
android·前端·面试
祎直向前1 天前
linuxshell测试题
前端·chrome
嫂子的姐夫1 天前
012-AES加解密:某勾网(参数data和响应密文)
javascript·爬虫·python·逆向·加密算法
Honmaple1 天前
SpringBoot + Seata + Nacos:分布式事务落地实战,订单-库存一致性全解析
spring boot·分布式·后端
irises1 天前
开源项目next-ai-draw-io核心能力拆解
前端·后端·llm
pas1361 天前
28-mini-vue customRender
前端·javascript·vue.js
耀耀_很无聊1 天前
16_大文件上传方案:分片上传、断点续传与秒传
java·spring boot·后端
只想提前退休1 天前
个人心得-搭建GitLab社区版服务器
后端