本文基于一个企业级后台管理框架 elips 的实际开发经验,分享领域模型驱动开发(Domain Model Driven Development)的设计思路,重点讲解如何通过 DSL(Domain Specific Language)实现配置化驱动的多租户站点生成。
一、为什么需要领域模型?
1.1 企业级后台的痛点
在开发企业级后台管理系统时,我们经常面临以下问题:
- 重复开发:每个业务模块(商品管理、订单管理、用户管理)都需要从零搭建
- 维护困难:菜单、路由、表格列配置分散在各处代码中
- 扩展性差:新增一个项目需要复制大量代码
- 多租户困境:同一套业务逻辑,不同客户需要不同的定制
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 + 嵌套 | 组合配置 |
六、总结
领域模型驱动开发的核心价值在于:
- 知识沉淀:将业务领域的理解固化为可复用的模型
- 开发提效:新项目只需继承和扩展,无需重复造轮子
- 维护便捷:配置集中管理,改一处生效到处
- 沟通统一:DSL 作为技术和业务的共同语言
elips 项目通过 Model-Project 的继承体系和多类型菜单模块的设计,实现了从一份配置自动生成完整后台站点的能力。这种设计模式特别适合:
- 多租户 SaaS 应用
- 企业内部多业务线管理系统
- 低代码/配置化平台
希望本文对你理解领域模型设计有所帮助!
作者:小高 项目来源:elips -哲玄全栈