什么是DSL?
大多数前端做重复的项目都是需要做到的时候重新去另外一个项目复制过来,然后修修改改,比如我需要做二个商城类的PC端管理,功能都有商品配置,新增,下架,商品规格之类的一些设置,这时候就需要一份DSL配置文件,冗余一些重复的功能点(花更少的时间做一些重复的事情),增加工作效率。
设计思路
首先我们需要一份配置文件,文件需要有一个模型(做两个商城类,需要一个商城模型,配置两个不同的项目文件继承于这个模型),然后再根据具体的项目文件产出具体的项目。

具体实现
首先在我们的文件中需要有这几个来配置基础的领域模型
csharp
{
mode: 'dsahboard' // 模板类型, 不同模板类型对应不同的模板
name: '' //名称
desc: '' //描述
icon: ''// icon
hpmePage: '' // 首页(项目配置)
}
DSL具体的配置项
go
menu: [{
key: '', // 菜单唯一描述
name: '', // 菜单名称,
menuType: '',// 枚举值:group / module
// 当menuType == group时,可填
subMenu: [{
// 可递归 menuItem
}, ...],
// 当menuType == module时,可填
moduleType: '', // sider/iframe/custom/schema
// 当 moduleType == sider 时
siderConfig: {
menu: [{
// 可递归munu下的item
}, ...]
},
// 当 moduleType === iframe 时
iframeConfig: {
path: ''
},
// 当 moduleType === custom
customConfig: {
path: '' // 自定义路径
},
// 当 moduleType === schema
schemaConfig: {
api: '', // 数据源 api (遵循 RESTFUL 规范 (GET,PUL,DELETE,POST) ) /api/user
schema: {
type: 'object',
properties: {
key: {
...schema, // 标准的scema 配置
type: '', // 字段类型
label: '', //字段中文名
// 字段在 table 中的相关配置
tableOption: {
...elTableColumnConfig, // 标准 el-tabel-column 配置
toFixed: 0,
visiable: true // 默认为 true (false 或者不配置,表示隐藏该字段)
},
searchOption: {
...eleComponentConfig, // 标准 el-comment-colum 配置
comType: '', // 配置组件类型 input/select/...
default: '' // 默认值
}
},
},
...
},
// table 配置
tableConfig: {
headerButtons: [{
label: '', // 按钮中文名
eventKey: '', // 按钮事件名
eventOption: '',// 按钮事件配置
...elButtonConfig // 标准 el-button 配置
}, ...],
rowButtons: [{
label: '', // 按钮中文名
eventKey: '', // 按钮事件名
eventOption: {
// 当 eventKey === 'remove' 时
params: {
// paramKey = 参数的键值
// rowValueKey = 参数值(当格式为 schema::tableKey 时 ,去table中找tableKey的value)
paramKey: rowValueKey
},
},
// 按钮事件配置
...elButtonConfig, // 标准 el-button 配置
}, ...]
},
searachConfig: {}, // 过滤条件配置
components: {} // 模块组件
},
}
}, ...]
menuType:这个配置为项目顶部栏/左边栏的配置项,分为group / module两个配置,当设置为group时,表示这个还有子菜单这时候需要配置subMenu字段来显示子菜单,当设置为module时,表示这个菜单项需要直接响应页面
当menuType == module时,可填 moduleType: '', // sider/iframe/custom/schema
- sider:表示当前项需要展示一个侧标栏,则需要配置 siderConfig 字段来配置具体侧边栏的菜单项。
- iframe:表示当前项需要展示一个iframe标签,则需要配置 iframeConfig 里面的 path 来显示iframe需要展示的路径。
- custom:表示当前项需要展示一个自定义的页面,则需要配置 customConfig 里面的 path 来显示具体需要中展示的自定义组件
- schema:表示这个菜单需要展示我们冗余的页面。(DSL重要部分)
schemaConfig的具体配置
- api:需要遵循 RESTFUL 规范 (GET,PUL,DELETE,POST)具体用于对于当前页面的具体操作(比如查询、删除、过滤)
- schema:需要遵循json schema 具体官方文档 json-schema.org/learn/getti...
css
// 例如
schema:{
type:Object,
properties:{
product_name:{
type:'string',
label:'商品名称',
tableOption:{ // 表示具体table product_name 这一列的配置
width: 300,
'show-overflow-tooltip': true // 支持当前使用组件的配置 列如 element-plus
},
searchOption:{ // 表示当前页面有一个 product_name 这个字段的查询条件
comType:'', // 表示当前过滤是什么组件(input,select,date,...)
api:'', // 当comtype为 select 时,需要后端请求下拉选项配置的api
enumList:[{ // 当comtype为 select 时下拉选项为自定义时(label,value具体组件具体配置)
label:'商品名1',
value:1
},{
label:'商品名2',
value:2
}],
format:'', // 当comtype为 date 时,可以设置具体返回的格式("YYYY-MM-DD HH:mm:ss")
// 当然还可以无限拓展 比如:input框只能输入number,可以配置inputType:'number',当是需要使用到自定义组件时可以配置,customCom:'',等等等等
}
}
}
}
- tableConfig:表示 table 上面的操作,比如一些新增按钮
csharp
// 表示头部的新增操作
headerButtons:[{
label:'', // 按钮名称
evenKey:'', // 按钮事件
...elementPlusConfig // 可以具体配置一些element-plus 的 button 配置
}]
// 表示table列的操作
rowButtons:[{
label:'', // 按钮名称
evenKey:'', // 按钮事件
eventOption:{
parmas:{
product_id:'schema::product_id' // 表示删除的 product 列表的需要的的参数可以这里获取
}
}, // 按钮的额外一些参数
...elementPlusConfig // 可以具体配置一些element-plus 的 button 配置(具体框架具体配置)
}]
最后通过配置项解析获取到数据
这里 homePage 的 schema 表示跳转到 schema 的页面,proj_key 表示这个我这个项目是 pdd 通过模板产出的一个 project,key则表示进入这个系统后默认选中一个 tab 页
根据数据产出具体项目
xml
<template>
<el-card class="table-panel">
<el-row v-if="tableConfig?.headerButtons?.length > 0" justify="end" class="operation-panel">
<el-button v-for="(btn, index) in tableConfig.headerButtons" :key="index" v-bind="btn"
@click="operationHandler({ btnConfig: btn })">{{ btn.label }}</el-button>
</el-row>
<schema-table ref="schemaTableRef" :apiParams="apiParams" :schema="tableSchema" :api="api" :buttons="tableConfig?.rowButtons ?? []"
@operate="operationHandler" />
</el-card>
</template>
<script setup>
const EventHandlerMap = {
remove: removeData
}
const operationHandler = ({ btnConfig , rowData }) => {
const { eventKey } = btnConfig;
if (EventHandlerMap[eventKey]) {
EventHandlerMap[eventKey]({ btnConfig , rowData})
} else {
// 具体操作
}
}
const removeData ({ btnConfig , rowData})=>{
// 具体操作
}
</script>
- 这里的 tableConfig.headerButtons 就是配置文件里面的 schema.tableConfig.headerButtons v-bind 的目的就是为了把 element-plus 的配置项绑定上(具体什么框架什么配置),EventHandlerMap 就是通过 schema.tableConfig.headerButtons 里面的 eventKey 来具体响应什么事件 operationHandler 接受两个参数,第一个是当前配置项的参数,需要拿到 eventKey 来具体响应事件。
schema-table
ruby
<template>
<div class="schema-table">
<el-table v-if="schema && schema.properties" v-loading="loading" :data="tableData" class="table">
<template v-for="(scheamItem, key) in schema.properties">
<el-table-column :key="key" v-if="scheamItem.option.visiable !== false" :prop="key"
:label="scheamItem.label" v-bind="scheamItem.option">
</el-table-column>
</template>
<el-table-column v-if="buttons && buttons.length > 0" label="操作" :width="operationWidth" fiexd="right">
<template #default="scope">
<el-button v-for="(btn, index) in buttons" :key="index" link v-bind="btn"
@click="operationHandler({ btnConfig: btn, rowData: scope.row })">{{ btn.label }}</el-button>
</template>
</el-table-column>
</el-table>
<el-row justify="end" class="pagination">
<el-pagination size="small" background :current-page="currentPage" :page-size="pageSize"
:page-sizes="[10, 20, 50, 100, 200]" layout="total, sizes,prev, pager, next, jumper" :total="total"
@size-change="onPageSizeChange" @current-change="onCurrentSizeChange" />
</el-row>
</div>
</template>
<script setup>
const operationHandler = ({ btnConfig, rowData }) => {
emit('operate', { btnConfig, rowData })
}
</script>
- 这个组件页面就是通过配置的 schema.properties 来渲染 table 具体列展示什么字段以及通过 tableOption 来配置 table 列的具体参数(这里通过数据处理把 tableOption 修改成了 option ),最后通过 tableConfig.rowButtons 来显示出操作列的具体 operationHandler 操作,这里传了两个参数,第二个为当前列的数据,所以最外层会接受到两个参数(比如删除 table 第一列的时候,我们需要拿到 table 第一列的 product_id(这个参数就是配置文件里面的 eventOption.params里面的具体key值了 ),最后根据这个配置的 product_id 在 table )中拿到对应的 value 进行删除。
schema-search-bar
xml
<template>
<el-form v-if="schema && schema.properties" :inline="true" class="schema-search-bar">
<!-- 动态组件 -->
<el-form-item v-for="(schemaItem, key) in schema.properties" :key="key" :label="schemaItem.label">
<!-- 展示子组件 -->
<component :ref="handelSearchComList" :schema="schemaItem"
:is="SearchConfig[schemaItem.option?.comType]?.component" :schemaKey="key" @loaded="handelChildLoaded">
</component>
</el-form-item>
<!-- 操作组件 -->
<el-form-item>
<el-button type="primary" plain class="search-btn" @click="search">搜索</el-button>
<el-button plain class="reset-btn" @click="reset">重置</el-button>
</el-form-item>
</el-form>
</template>
<script setup>
let childComLoadedCount = 0;
const handelChildLoaded = () => {
childComLoadedCount++;
if (childComLoadedCount >= Object.keys(schema?.value?.properties).length) {
emit('load', getvalue());
}
}
</script>
- 这里的 schema 做过数据处理,把多余的数据(tableOption)过滤掉,把 searchOption 的key 改为 option 具体展示如下
go
* schema: {
type: 'object',
properties: {
key: {
...schema, // 标准的scema 配置
type: '', // 字段类型
label: '', //字段中文名
// 字段在 table 中的相关配置
option: {
...eleComponentConfig, // 标准 el-comment-colum 配置
comType: '', // 配置组件类型 input/select/...
default: '' // 默认值
}
},
},
- 最后我们就可以通过配置的 comType 来具体展示什么组件 :schema="schemaItem" 传给子组件主要是为了绑定默认值和一些 element-plus 的配置,这里需要注意一个 @loaded="handelChildLoaded" 每个子组件需要传一个 loaded 方法来通知父组件加载完成(防止子组件没加载完成父组件以及加载完成导致默认值赋不上)
总结
这也一个简单的 DSL 解析文件就配置好了,我们就可以通过添加配置文件来产出多个项目。