DSL 配置设计
前文介绍了 Mint DSL 的基础模型:DSL 分为领域模型和项目模型,两者的关系类似面向对象中的基类与子类。
领域模型负责沉淀某个垂直领域的通用配置,项目模型则继承领域模型,并根据具体项目的需求进行扩展或覆盖。
本章暂不展开继承与合并算法,而是先回答一个更基础的问题:
一份 DSL 配置,应该如何完整描述一个中后台站点?
从站点结构拆解配置层级
在设计配置之前,先观察一个常见的中后台系统由哪些部分组成。
页面整体通常可以分为两个区域:
- 顶部导航区域;
- 主体内容区域。
顶部导航区域一般包含:
- Logo;
- 站点名称;
- 一级菜单;
- 用户信息及操作入口。
主体内容区域则可能包含:
- 左侧菜单栏;
- 框架内置的标准管理页面;
- 业务自行开发的自定义页面;
- 通过 iframe 接入的第三方页面。
按照这个结构,一份站点 DSL 可以拆分成下面几个层级:
站点配置
├── 站点基本信息
│ ├── 名称
│ ├── Logo
│ └── 默认首页
└── 菜单配置
├── 菜单分组
│ └── 子菜单
└── 页面模块
├── 左侧菜单模块
├── 自定义页面
├── Schema 标准页面
└── iframe 页面
基于这个结构,可以先得到一份基础配置:
module.exports = {
// 中后台名称
name: "",
// 中后台 Logo
icon: "",
// 默认首页
homePage: "",
// 顶部菜单
menu: [
{
// 菜单名称
label: "",
// 菜单唯一标识
key: "",
// 菜单类型:module | group
menuType: "",
// menuType 为 group 时配置子菜单
subMenu: [],
/**
* 页面模块类型:
*
* sider 展示带有左侧菜单的业务模块
* custom 跳转到业务自行开发的页面
* schema 使用框架内置的标准管理页面
* iframe 嵌入外部页面
*/
moduleType: "",
// 左侧菜单模块配置
siderConfig: {
menu: [],
},
// 自定义页面配置
customConfig: {
path: "",
},
// Schema 标准页面配置
schemaConfig: {},
// iframe 页面配置
iframeConfig: {
path: "",
},
},
],
};
这份配置描述了站点的整体骨架,但还没有深入到具体页面。
其中,custom 和 iframe 的配置相对简单:
custom指向业务项目内部自行开发的路由页面;iframe指向需要嵌入的外部页面。
真正需要重点设计的是 schema 模块,因为它承载了 Mint 配置驱动页面的核心能力。
Schema 页面需要描述哪些内容
Schema 页面是框架内置的标准管理页面,主要用于承接中后台中大量重复出现的增删改查场景。
Mint 希望通过 Schema 页面覆盖大约 80% 的通用需求,同时为剩余 20% 的个性化场景提供扩展能力。
以商品管理页面为例,一个典型的管理页面通常包含:
- 搜索区域;
- 表格区域;
- 表格顶部操作区域;
- 表格行操作区域;
- 弹窗、抽屉等扩展组件。
因此,schemaConfig 可以先拆分为三个部分:
{
schemaConfig: {
// 搜索区域布局与按钮
searchConfig: {
extendButtons: [],
},
// 表格区域及操作按钮
tableConfig: {
headerButtons: [],
rowButtons: [],
},
// 页面扩展组件
componentConfig: {},
},
}
三者的职责分别是:
searchConfig:描述搜索区域的整体行为;tableConfig:描述表格及其顶部、行内操作;componentConfig:描述弹窗、抽屉等可被事件调度的组件。
不过,这份配置目前只描述了页面布局,还没有回答一个关键问题:
搜索项和表格列从哪里来?
要回答这个问题,需要从页面的数据结构出发继续抽象。
从页面布局抽象出统一数据模型
搜索和表格看起来是两个独立区域,但它们实际上都在围绕同一份业务数据工作。
例如,商品管理页面可能包含商品名称、商品状态、商品价格、商品图片和创建时间等字段。其中,“商品名称”可能同时出现在搜索区域和表格中;“商品状态”可能在搜索区域展示为下拉框,在表格中展示为状态文本。
因此,与其分别维护搜索字段和表格字段,不如先集中定义一份页面数据模型,再描述每个字段在不同场景中的展示方式。
Mint 使用 JSON Schema 描述页面的数据结构:
{
schemaConfig: {
// 页面数据请求接口
api: "",
// 页面数据模型
schema: {
type: "object",
properties: {
fieldKey: {
// 字段数据类型
type: "",
// 字段展示名称
label: "",
// 字段在搜索区域中的配置
searchOption: {},
// 字段在表格区域中的配置
tableOption: {},
// 其他应用场景的扩展配置
xxxOption: {},
},
},
// 必填字段
required: [],
},
},
}
这里需要区分两个概念:
schemaConfig是 Mint 中一个完整标准页面的配置;schemaConfig.schema是页面内部的数据模型,结构遵循 JSON Schema。
JSON Schema 不仅可以描述字段类型和必填规则,还可以配合 AJV 完成运行时数据校验。
在 JSON Schema 的基础上,Mint 为每个字段增加了不同场景的 Option 配置:
字段定义
├── type
├── label
├── searchOption
├── tableOption
└── xxxOption
同一个字段可以在不同区域采用不同的展示方式:
- 在搜索区域中展示为输入框或下拉框;
- 在表格中展示为文本、图片或者格式化后的数值;
- 在编辑弹窗中展示为地图、选择器或其他复杂组件;
- 在未来新增的业务场景中,通过新的
xxxOption继续扩展。
这样,字段的数据定义只需要维护一次,各个页面区域只负责描述如何使用该字段。
搜索字段配置
搜索区域本质上是一个动态表单。
框架负责实现表单容器、布局、提交和重置等通用能力,DSL 只需要描述每个字段应该使用什么控件,以及控件需要哪些参数。
searchOption 的基础结构如下:
searchOption: {
/**
* 透传给 UI 组件的属性
*
* 例如:
* placeholder、allowClear、disabled 等
*/
uiComponentConfig: {},
/**
* 搜索控件类型:
*
* input 普通输入框
* select 静态下拉框
* dynamicSelect 动态下拉框
* dateRangePicker 日期范围选择器
*/
comType: "",
/**
* 静态枚举数据
*
* comType 为 select 时使用
*/
enumList: [],
/**
* 是否在搜索区域显示
*
* 默认显示,只有显式配置为 false 时才隐藏
*/
visible: true,
// 搜索字段默认值
default: "",
/**
* 动态选项请求地址
*
* comType 为 dynamicSelect 时使用
*/
api: "",
}
例如,商品名称可以配置成输入框:
name: {
type: "string",
label: "商品名称",
searchOption: {
comType: "input",
visible: true,
default: "",
uiComponentConfig: {
placeholder: "请输入商品名称",
allowClear: true,
},
},
}
商品状态可以配置成静态下拉框:
status: {
type: "string",
label: "商品状态",
searchOption: {
comType: "select",
visible: true,
enumList: [
{
label: "上架",
value: "online",
},
{
label: "下架",
value: "offline",
},
],
uiComponentConfig: {
placeholder: "请选择商品状态",
allowClear: true,
},
},
}
如果下拉选项需要从服务端获取,则可以使用动态下拉框:
categoryId: {
type: "string",
label: "商品分类",
searchOption: {
comType: "dynamicSelect",
visible: true,
api: "/api/category/list",
uiComponentConfig: {
placeholder: "请选择商品分类",
allowClear: true,
},
},
}
为什么需要 visible
visible 不只是一个控制显示和隐藏的开关,它还与 DSL 的继承机制有关。
假设领域模型默认展示商品状态:
searchOption: {
visible: true,
}
某个项目继承领域模型后,可以通过下面的配置隐藏该字段:
searchOption: {
visible: false,
}
如果完全删除 searchOption,合并配置时很难判断项目模型是希望继承,还是希望移除该搜索项。
因此,Mint 约定:
- 未配置
visible时默认显示; - 只有显式配置为
false时才隐藏。
这使项目模型能够明确覆盖领域模型中的展示行为。
表格字段配置
同一个字段在表格中如何展示,由 tableOption 描述:
tableOption: {
/**
* 透传给 UI 表格列的属性
*
* 例如:
* width、fixed、align、ellipsis 等
*/
uiTableColumnConfig: {},
/**
* 是否在表格中显示
*
* 默认显示,只有显式配置为 false 时隐藏
*/
visible: true,
// 数值字段保留的小数位数
toFixed: 2,
/**
* 翻译字段
*
* 可以使用另一个字段的值作为展示内容
*/
translateField: "",
/**
* 字段展示方式
*
* image 表示按照图片渲染
* 未配置时直接展示原始数据
*/
showWay: "",
}
例如,商品价格可以配置小数位数:
price: {
type: "number",
label: "商品价格",
tableOption: {
visible: true,
toFixed: 2,
uiTableColumnConfig: {
width: 120,
align: "right",
},
},
}
商品图片可以指定为图片展示:
image: {
type: "string",
label: "商品图片",
tableOption: {
visible: true,
showWay: "image",
uiTableColumnConfig: {
width: 100,
},
},
}
如果接口返回的是状态码,同时还提供了状态名称,可以通过 translateField 指定实际展示字段:
status: {
type: "string",
label: "商品状态",
tableOption: {
visible: true,
translateField: "statusText",
},
}
这样,同一个字段的数据定义、搜索方式和表格展示方式便被组织到了一起。
页面级布局配置
字段的 searchOption 和 tableOption 描述“字段如何展示”,而 searchConfig 和 tableConfig 描述“页面区域如何组织”。
两者的职责并不相同:
schema.properties
└── 描述每个字段在不同场景中的表现
searchConfig / tableConfig
└── 描述搜索区和表格区的整体结构与操作
页面级配置如下:
{
searchConfig: {
// 搜索区域的扩展按钮
extendButtons: [],
},
tableConfig: {
// 表格顶部按钮
headerButtons: [],
// 表格行操作按钮
rowButtons: [],
},
}
例如,“新增商品”通常属于表格顶部操作,可以放在 headerButtons 中;“查看”“编辑”“删除”等针对单条数据的操作,则放在 rowButtons 中。
扩展组件配置
标准搜索和表格可以覆盖大部分管理页面,但一些业务操作仍然需要弹窗、抽屉或其他复杂组件。
因此,Mint 提供了 componentConfig,用于声明页面可以调度的扩展组件。
例如,定义一个商品编辑弹窗:
componentConfig: {
editFormModal: {
// 弹窗标题
title: "编辑商品",
// 是否展示底部操作区域
showFooter: true,
// 确认按钮文案
okText: "确认",
// 取消按钮文案
cancelText: "取消",
// 弹窗宽度
width: "600px",
/**
* 组件向外抛出的事件标识
*
* 模板解析器可以根据该标识
* 继续执行刷新列表等后续操作
*/
eventKey: "refreshList",
},
}
这里的 editFormModal 是组件在当前页面中的唯一名称。
DSL 只描述组件实例需要的配置,不关心组件具体如何实现。组件的注册、查找和渲染,将由后续的模板解析器与组件调度机制负责。
按钮与事件配置
组件声明完成后,还需要通过按钮触发相应行为。
Mint 不在 DSL 中直接编写 JavaScript 函数,而是使用 eventKey 描述按钮需要触发的事件,再通过 eventOption 提供事件参数。
例如,在表格行操作区配置“查看”按钮:
tableConfig: {
rowButtons: [
{
// 按钮文案
label: "查看",
// 触发框架内置的组件展示事件
eventKey: "showComponent",
// 事件参数
eventOption: {
// 对应 componentConfig 中的组件名称
comName: "editFormModal",
},
// 透传给 Button 组件的属性
uiButtonConfig: {},
},
],
}
当用户点击“查看”按钮时,模板解析器会按照下面的顺序处理:
用户点击“查看”
↓
读取按钮的 eventKey
↓
找到 showComponent 事件处理器
↓
读取 eventOption.comName
↓
在 componentConfig 中找到 editFormModal
↓
渲染并打开对应组件
对于删除等数据操作,可以在 eventOption 中声明请求参数:
tableConfig: {
rowButtons: [
{
label: "删除",
eventKey: "remove",
eventOption: {
params: {
id: "schema::id",
source: "product-management",
},
},
uiButtonConfig: {
danger: true,
},
},
],
}
其中,Mint 可以约定两类参数值:
"schema::id":从当前行数据中读取id字段;"product-management":直接使用配置中的固定值。
以当前行数据为例:
{
id: 1001,
name: "示例商品",
}
下面的参数配置:
params: {
id: "schema::id",
source: "product-management",
}
经过模板解析器处理后,会得到:
{
id: 1001,
source: "product-management",
}
通过这种引用规则,DSL 可以声明事件参数与当前行数据之间的映射关系,而不需要在配置中编写具体的取值函数。
完整 DSL 结构
经过前面的逐层拆解,一份完整的站点 DSL 可以整理为下面的结构:
module.exports = {
// 中后台基本信息
name: "",
icon: "",
homePage: "",
// 顶部菜单
menu: [
{
label: "",
key: "",
/**
* module 页面模块
* group 菜单分组
*/
menuType: "module",
// menuType 为 group 时使用
subMenu: [],
/**
* sider 左侧菜单模块
* custom 业务自定义页面
* schema 框架标准页面
* iframe 外部页面
*/
moduleType: "schema",
siderConfig: {
menu: [],
},
customConfig: {
path: "",
},
schemaConfig: {
// 页面数据接口
api: "",
// 页面数据模型
schema: {
type: "object",
properties: {
fieldKey: {
label: "",
type: "",
// 搜索区域中的字段配置
searchOption: {
uiComponentConfig: {},
comType: "",
enumList: [],
visible: true,
default: "",
api: "",
},
// 表格区域中的字段配置
tableOption: {
uiTableColumnConfig: {},
visible: true,
toFixed: 2,
translateField: "",
showWay: "",
},
// 其他业务场景的扩展配置
xxxOption: {},
},
},
required: [],
},
// 搜索区域整体配置
searchConfig: {
extendButtons: [],
},
// 表格区域整体配置
tableConfig: {
headerButtons: [],
rowButtons: [
{
label: "",
eventKey: "",
eventOption: {
comName: "",
params: {
fieldKey: "schema::fieldKey",
fixedValue: "",
},
},
uiButtonConfig: {},
},
],
},
// 页面扩展组件
componentConfig: {
componentName: {
title: "",
showFooter: true,
okText: "确认",
cancelText: "取消",
width: "600px",
eventKey: "",
},
},
},
iframeConfig: {
path: "",
},
},
],
};
本章小结
Mint DSL 的设计并不是简单地把页面代码转换成配置,而是从中后台页面的共性出发,逐层抽象出:
- 站点;
- 菜单;
- 页面模块;
- 数据模型;
- 字段展示方式;
- 页面操作;
- 事件行为;
- 扩展组件。
其中,JSON Schema 负责描述数据本身,searchOption 和 tableOption 负责描述字段在不同区域中的展示方式,searchConfig 和 tableConfig 负责描述页面级布局与操作,componentConfig 则负责承接标准页面之外的扩展能力。
领域模型可以沉淀这些通用配置,项目模型则通过继承和覆盖调整具体行为。
下一章节将继续介绍模板解析器如何读取这份 DSL,并将配置真正转换成可以交互的中后台页面。