如何实现一个DSL的设计与实现

什么是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 == 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 进行删除。
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 解析文件就配置好了,我们就可以通过添加配置文件来产出多个项目。

相关推荐
VillanelleS几秒前
前端工程化之自动化部署
运维·前端·自动化
moyu849 分钟前
高效开发必备:手把手整合IconFont、 Vant与Element Plus
前端·javascript
trust Tomorrow10 分钟前
JS案例-基于Proxy的响应式数据
前端·javascript·css·html
BillKu12 分钟前
Vue3 + TypeScript,关于item[key]的报错处理方法
前端·javascript·vue.js·typescript
妄念鹿14 分钟前
关于tailwindcssV4版本官方插件没有提示
前端
七月丶15 分钟前
💬 打造丝滑交互体验:用 prompts 优化你的 CLI 工具(gix 实战)
前端·后端·github
愤怒的糖葫芦19 分钟前
异步编程进阶:Generator 与 Async/Await
前端·javascript
不想说话的麋鹿24 分钟前
「项目实战」从0搭建NestJS后端服务(八):静态资源访问以及文件上传
前端·node.js·全栈
liuxb24 分钟前
前端多标签主从管理方案分享
前端
小桥风满袖25 分钟前
Three.js-硬要自学系列3 (平行光与环境光、动画渲染循环、stats状态查看器)
前端·css