一、什么是 DSL 设计
DSL 架构是专为解决特定领域问题而设计的语言或配置规范,DSL 体现为一套可配置的菜单与页面描述协议------通过 JSON 配置描述菜单结构、页面类型、表格列定义、搜索表单、按钮行为等,前端根据配置自动渲染出完整的页面。
二、为什么要这样设计
2.1 解决的核心问题
| 问题 | 传统方式 | DSL 方式 |
|---|---|---|
| 新增 CRUD 页面 | 写 Vue/React 模板 + 表格 + 搜索 + 按钮 | 写一份 JSON 配置 |
| 多项目/多租户 | 每个项目一套代码 | 同一套代码,不同配置 |
| 页面结构统一性 | 依赖开发者规范 | 架构层面保证一致 |
2.2 设计目的
- 配置驱动渲染:将页面结构从代码中抽离为配置,实现「改配置即改页面」
- 多项目复用:同一套前端引擎,通过不同 model 配置支撑多个业务系统
- 降低开发成本:标准 CRUD 页面零重复代码,减少80%的重复性工作
- 统一交互规范:所有页面的表格、搜索、按钮行为由框架统一控制
三、设计思路
3.1 整体架构分层

js
┌─────────────────────────────────────────────────────┐
│ DSL 配置层 │
│ model/buiness/model.js model/course/model.js │
│ (描述菜单结构、页面类型、字段定义、按钮行为) │
└──────────────────────┬──────────────────────────────┘
│ API 返回
┌──────────────────────▼──────────────────────────────┐
│ 状态管理层 │
│ menuStore(菜单树) projectStore(项目列表) │
│ (全局状态存储 + 菜单查找方法) │
└──────────────────────┬──────────────────────────────┘
│ 驱动
┌──────────────────────▼──────────────────────────────┐
│ 路由引擎层 │
│ entry.dashboard.js │
│ (根据 moduleType 匹配路由 → 对应视图组件) │
└──────────────────────┬──────────────────────────────┘
│ 渲染
┌──────────────────────▼──────────────────────────────┐
│ 视图组件层 │
│ header-view sider-view schema-view │
│ iframe-view todo-view │
│ (通用视图组件,根据 DSL 配置动态渲染内容) │
└──────────────────────┬──────────────────────────────┘
│ 组合
┌──────────────────────▼──────────────────────────────┐
│ 通用组件层 │
│ schema-table schema-search-bar │
│ header-container sider-container │
│ (纯 UI 组件,接收 schema 配置渲染表格/搜索/布局) │
└─────────────────────────────────────────────────────┘
3.2 菜单模型设计
菜单是整个系统的骨架,采用树形结构 + 类型分发的设计:
js
MenuItem
├── key // 唯一标识
├── name // 显示名称
├── menuType // group(分组目录)/ module(功能模块)
│
├── [menuType=group]
│ └── subMenu: MenuItem[] // 递归子菜单
│
└── [menuType=module]
└── moduleType // 决定渲染哪种视图
├── schema → schemaConfig → 数据表格页
├── iframe → iframeConfig → 内嵌页面
├── sider → siderConfig → 侧边栏布局
└── custom → customConfig → 自定义组件
关键设计决策:
menuType区分「目录」和「模块」,目录只用于分组,模块才是实际页面moduleType决定渲染引擎,每种类型对应一个视图组件- 配置按需挂载:
schema类型才有schemaConfig,iframe类型才有iframeConfig
3.3 Schema 协议设计
Schema 是 DSL 中最复杂的部分,采用一份字段定义,多视图复用的设计:
js
properties: {
product_name: {
type: 'string', // 字段类型
label: '商品名称', // 字段标签(表格列头 + 搜索标签共用)
tableOptions: { ... }, // 表格列专属配置
searchOptions: { ... }, // 搜索表单专属配置
}
}
核心思路:
- 字段定义统一 :一个字段只定义一次,
label、type等基础属性被表格和搜索共用 - 视图配置隔离 :通过
tableOptions/searchOptions后缀区分不同视图的配置 - 按需提取 :
buildDtoSchema方法根据comName参数提取对应后缀的配置
js
原始 schema
↓ buildDtoSchema(schema, 'table')
tableSchema(只含 tableOptions 的字段)
↓ buildDtoSchema(schema, 'search')
searchSchema(只含 searchOptions 的字段)
3.4 路由分发设计
路由采用类型映射而非硬编码:
js
// dashboard.vue --- 顶部菜单点击
const pathMap = {
sider: '/sider',
iframe: '/iframe',
schema: '/schema',
custom: customConfig?.path,
}
router.push({
path: `/view/dashboard${pathMap[moduleType]}`,
query: { key, proj_key }
})
侧边栏嵌套路由:
js
/view/dashboard/sider → sider-view(侧边栏容器)
├── /sider/iframe → iframe-view(子路由)
├── /sider/schema → schema-view(子路由)
└── /sider/todo → todo(子路由)
侧边栏通过 sider_key query 参数区分左侧菜单选中项和右侧内容。
3.5 数据流向设计
js
model 配置
↓ GET /api/getProject?proj_key=xxx
menuStore.setMenuList(menu)
↓
header-view 渲染顶部菜单
↓ 用户点击菜单项
router.push → 路由变化
↓
对应视图组件(schema-view / iframe-view / sider-view)
↓
useSchema() hook 从 menuStore 查找当前菜单配置
↓ buildDtoSchema() 解析
provide('schemaViewData') 注入子组件
↓
schema-table / schema-search-bar 渲染
↓ 用户操作(搜索/翻页/按钮点击)
emit 事件 → 父组件处理或透传
- 通用事件(如
remove)在组件内部处理 - 业务事件通过
emit逐层上报,由最终使用方处理
3.6 路由参数设计(proj_key / sider_key)
在 DSL 流转过程中,路由 query 参数承担着上下文传递的关键职责:
proj_key --- 项目标识
作用: 确定当前访问的是哪个业务项目(如电商系统、课程系统),决定加载哪套菜单配置。
流转路径:
js
项目列表页(project-list.vue)
用户点击「进入」按钮
↓ 携带 proj_key 跳转
router.push({ path: '/view/dashboard/schema', query: { proj_key: 'pdd', key: 'product' } })
↓
dashboard.vue 初始化
↓ 读取 route.query.proj_key
GET /api/project/list?proj_key=pdd → projectStore(项目数据)
GET /api/getProject?proj_key=pdd → menuStore(该项目的菜单树)
↓
header-view 渲染顶部菜单
↓ 用户点击菜单项
router.push({ query: { key, proj_key: route.query.proj_key } }) ← proj_key 持续透传
↓
schema-view / iframe-view / sider-view
↓ 通过 route.query.proj_key 保持项目上下文
核心要点: proj_key 在整个应用生命周期中始终通过路由 query 透传,确保每次菜单切换都保持同一项目上下文。
sider_key --- 侧边栏菜单标识
作用: 在侧边栏布局(moduleType: 'sider')中,标识当前选中的左侧子菜单项。
流转路径:
js
用户点击顶部菜单(moduleType === 'sider')
↓ router.push
/view/dashboard/sider?key=data&proj_key=pdd
↓
sider-view.vue 加载
↓ 通过 route.query.key 查找菜单项
menuStore.findMenuItem({ key: 'key', value: 'data' })
↓ 获取 siderConfig.menu
渲染左侧菜单列表
↓ 首次加载自动选中第一个子菜单
router.push({ path: '/view/dashboard/sider/schema', query: { key: 'data', sider_key: 'analysis', proj_key: 'pdd' } })
↓
sider-view 内部 router-view(子路由)
↓ schema-view / iframe-view 通过 route.query.sider_key ?? route.query.key 查找菜单
渲染右侧内容
↓ 用户点击左侧菜单
router.push({ query: { key: 'data', sider_key: 'sider-search', proj_key: 'pdd' } })
核心要点: sider_key 只在侧边栏布局中存在,用于区分左侧菜单选中项。内容组件通过 sider_key ?? key 优先使用 sider_key 定位菜单。
两个 key 的协作关系
js
URL: /view/dashboard/sider/schema?key=data&sider_key=analysis&proj_key=pdd
↑ ↑ ↑
顶部菜单 侧边栏菜单 项目标识
路由层级:
/view/dashboard/sider → sider-view(通过 key 定位顶部菜单 + 获取侧边栏配置)
└── /sider/schema → schema-view(通过 sider_key 定位侧边栏菜单 + 获取 schema 配置)
| 参数 | 作用域 | 决定什么 | 谁消费 |
|---|---|---|---|
proj_key |
全局 | 加载哪个项目的菜单配置 | dashboard.vue、menuStore |
key |
顶部菜单 | 当前激活的顶部菜单项 | header-view、sider-view、schema-view、iframe-view |
sider_key |
侧边栏 | 当前激活的侧边栏子菜单项 | sider-view、子路由中的视图组件 |
查找优先级: sider_key ?? key --- 有侧边栏 key 时用侧边栏的,否则用顶部菜单的。
| 情况 | 有 sider_key? | 用哪个 | 原因 |
|---|---|---|---|
| 普通页面 | 无 | key |
只有顶部菜单,key 就是最终页面 |
| 侧边栏页面 | 有 | sider_key |
key 只是容器,sider_key 才是实际页面 |
四、设计优势
4.1 开发效率
- 零代码 CRUD:新增一个数据管理页面只需写 model 配置,无需写 Vue 代码
- 统一开发范式:所有开发者按同一套协议写配置,降低沟通成本
- 快速原型:业务人员可直接修改配置验证需求
4.2 可维护性
- 配置与逻辑分离:页面结构在 model 文件中,渲染逻辑在组件中,互不干扰
- 组件高度复用 :
schema-table、schema-search-bar等组件可在任意 schema 页面复用 - 易于统一升级:修改通用组件即可影响所有使用该组件的页面
4.3 可扩展性
- 新模块类型 :新增
moduleType枚举值 + 对应视图组件即可 - 新搜索组件 :在
search.item.config.js注册新组件即可 - 新按钮事件 :在
EventHandlerMap注册即可 - 多项目支撑 :不同
proj_key对应不同 model 配置,一套前端服务多个系统
4.4 架构清晰度
- 分层明确:DSL 层 → 状态层 → 路由层 → 视图层 → 组件层
- 数据流单向:配置 → Store → 组件 → 事件 → 组件
- 职责单一:每个组件只负责一种渲染逻辑
五、可扩展方向
5.1 Schema 组件类型扩展
当前搜索支持 input、select、dynamicSelect、dateRange等,可扩展:
| 组件类型 | 用途 |
|---|---|
cascader |
级联选择(如省市区) |
treeSelect |
树形选择 |
switch |
开关筛选 |
numberRange |
数值范围 |
upload |
文件上传搜索 |
5.2 表单 Schema(新增/编辑页面)
当前 Schema 只驱动「表格 + 搜索」,可扩展为驱动「新增/编辑表单」:
js
properties: {
product_name: {
type: 'string',
label: '商品名称',
formOptions: { // 表单配置
comType: 'input',
rules: [{ required: true, message: '请输入商品名称' }],
placeholder: '请输入商品名称',
},
}
}
配合 eventKey: 'showComponent' 弹出表单弹窗,实现完整的 CRUD 闭环。
5.3 详情页 Schema
js
detailOptions: {
comType: 'text', // 纯文本展示
// comType: 'image', // 图片展示
// comType: 'link', // 链接跳转
}
5.4 权限控制
在 DSL 中增加权限字段:
js
menu: [{
key: 'product',
name: '商品管理',
permission: 'product:view', // 菜单权限
schemaConfig: {
tableConfig: {
rowButtons: [{
label: '删除',
permission: 'product:delete', // 按钮权限
}]
}
}
}]
六、总结
Elpis 前端领域模型架构的核心思想是 「配置即页面」------通过一套 DSL 协议将菜单结构、页面类型、字段定义、交互行为等描述为可配置的 JSON 数据,前端引擎根据配置自动渲染出完整的业务页面。
架构核心价值
| 维度 | 价值 |
|---|---|
| 开发效率 | 标准 CRUD 页面零前端代码,新增页面只需编写 model 配置 |
| 多项目复用 | 同一套前端引擎,通过 proj_key 切换不同业务系统的配置 |
| 统一规范 | 所有页面的表格、搜索、按钮行为由框架统一控制,交互体验一致 |
| 易于维护 | 配置与逻辑分离,修改页面结构无需改动 Vue 代码 |
| 可扩展 | 新增模块类型、搜索组件、按钮事件只需注册即可,无需改动核心引擎 |
设计哲学
这套架构将前端从「页面开发者 」转变为「引擎开发者」,业务逻辑通过配置表达,而非通过代码表达。当业务需求变化时,只需调整配置而非重写代码,这是低代码思想在中后台系统中的典型实践。
适用场景
- 适合:中后台管理系统、多项目/多租户平台、标准化 CRUD 场景
- 不适合:高度定制化的 C 端页面、复杂交互流程页面、需要极致性能优化的场景
一句话总结
用 DSL 描述业务结构,用引擎驱动页面渲染,用配置替代代码实现业务需求。