全栈里程碑三:领域模型架构建设

文档知识来源:抖音 "哲玄前端",《大前端全栈实践课》

重点分享 DSL 的设计 ,以及 DSL 如何分版块映射到整个站点的。

DSL设计的由来/现状

随着业务发展,我们面临以下问题:

  1. 重复开发严重:多个系统 80% 的代码都是相似的(CRUD、用户管理、权限控制等)
  2. 维护成本高:相同逻辑在不同系统中重复实现,修改时需要多处同步
  3. 开发效率低:开发人员花大量时间在重复的 CV 操作上
  4. 代码质量不一:不同开发者实现相同功能时风格、质量参差不齐
  5. 新人上手慢:每个系统都有不同的代码结构和约定

为了解决这些问题,我们设计了一套 DSL(Domain Specific Language)模板配置系统,让开发人员通过声明式配置自动生成代码,避免重复的 CV 操作。

DSL 设计

DSL(Domain Specific Language)模板引擎通常指的是一种用于生成动态内容的工具,它允许开发人员通过一种特定于领域的语言来描述模板,而模板引擎会在服务器端根据数据生成最终的HTML或其他输出格式。

在服务端调用的上下文中,DSL模板引擎是指服务器端利用模板引擎渲染动态内容,并返回给客户端。它不是直接在客户端运行的脚本,而是服务端通过传递数据到模板引擎,利用模板文件生成最终结果。

例如:

  1. 服务器端渲染:在服务端使用DSL模板引擎(如Jinja2、Thymeleaf等)处理数据,将数据填充到模板中,然后生成最终的HTML内容,并将其发送给客户端浏览器。
  2. 客户端渲染:虽然一些模板引擎(如Handlebars、Mustache等)也可以在客户端运行,但它们本质上是为了在服务端准备好模板和数据后进行渲染,因此DSL模板引擎通常还是在服务端调用为主。

总结来说,DSL模板引擎的典型用法是服务端调用,它帮助处理数据并将渲染结果返回给客户端。

根据DSL模版配置 + 配置解析 + 预留好的各种各样的组件 ----> 生成各种各样的系统

(1) DSL模版配置

js 复制代码
export default {
    mode: 'dashboard', //模版类型,不同模板类型对应不一样的模版数据结构
    name: '',// 名称
    desc: '', // 描述
    icon: '', // 首页(项目配置)
    // 头部菜单 header-container
    menu: [
        {
            key: '', //菜单唯一描述
            name: '', // 菜单名称
            /**
             * header-container 里面可能会出现 菜单1 菜单2 菜单组件1 菜单组件2 
             * meuType设置为两种类型: group 、 module(对应一个板块)
             */
            menuType: '', // 枚举值, group / module

            // 当 meuType == group 时,可填
            subMenu: [{
                // 可递归 menuItem 这里递归出来的就是header-container的每一个头部菜单
                // ... 表示可以出现多个{}这里面的内容
            },
            // 更多菜单项...
            ],

            /**
             * 点击header-container 中的某一个菜单,可能出现 sider-container/schema-view/iframe-view/custom-view
             *  schema-view 80%的内容,都已schema沉淀,最终根据配置渲染出页面;
             *  iframe-view 第三方组件;
             *  custom-view 自定义页面;
             *  sider-container 左侧菜单;
             *      --- 左侧菜单 :里面可能会出现 菜单1 菜单2 菜单组件1 菜单组件2  
             */

            // 当 meuType == module 时,可填
            moduleType: '', // 枚举值: sider/iframe/custom/schema

            // 当 moduleType == sider 时,(侧边栏)
            siderConfig: {
                menu: [{
                    //可递归 menuItem (除了 moduleType == sider),这里递归出来的就是sider-container的每一个侧边菜单
                },
                // 更多菜单项...
            ]
            },
            // 当 moduleType == iframe 时,
            iframeConfig: {
                path: '', // iframe的路径
            },
            // 当 moduleType == custom 时,
            customConfig: {
                path: '', // 自定义路由路径
            },
            // 当 moduleType == schema 时,
            schemaConfig: {
                /**
                 * DSL(dashboader模版配置)---> schema config ---> 解析器 ---> schema-view
                 * schema-view 里面有 1.schema-search-bar , select 或者 date-range 或者 其他组件 
                 *                   2.其他组件
                 *                   3.schema-table
                 *                   4.schema-table-row , <button>[点击按钮,弹出 schema-form [有 input , select, radio, 其他组件]],其他组件[ 有 其他组件 等]   
                 *  往往重复性的内容,来源于同一份数据,(;可以是同一个api接口返回的数据,也可以是同一份数据库的数据等)
                 *  
                 */
                api: '/api/user', // 数据源api (遵循 RESTFUL 规范)
                schema: { // 板块数据接口
                    type: 'object',
                    properties: {
                        key: {
                            ...schema, // 标准 schema 配置
                            type: '', // 字段类型
                            label: '', // 字段的中文名
                            // 字段在 table 中的相关配置
                            tableOption: {
                                ...elTableColumnConfig, // 标准 el-tabel-colum 配置
                                toFixed:0,// 保留小数点后几位
                                visiable: true, // 是否在表单中展示,默认为true(false,表示不在表单中显示)
                            },
                            // 字段在 search-bar 中的相关配置 (搜索框)
                            searchOption:{
                                ...eleComponentConfjg,// 标准 el-component-colum 配置
                                comType:'', //配置组件类型  input/select/....
                                default:'',// 默认值

                                // comType === 'select'
                                enumList:[], //下拉框可选项

                                // comType === 'dynamicSelect'
                                api:''
                            },
                            // 字段在不同动态 component 中的相关配置,前缀对应 componentConfig 中的键值
                            // 如: componentConfig.creatForm, 这里对应 createFormOption
                            // 字段在 createFrom 中相关配置
                            createFormOption:{
                                ...eleComponentConfjg,// 标准 el-component-colum 配置
                                comType:'',// 控件类型,input/select/input-number
                                visiable:true,// 是否展示(true/false),默认为 true
                                disable:false, //是否禁用(true/false),默认为 false
                                default:'', //默认值

                                // comType === 'select'
                                enumList:[], //下拉框可选项

                            }
                        
                            // 还可以配置其他的 option
                            // formOption:{}
                        },
                       // 更多菜单项...
                }
                },
                // tabel 配置,表单描述
                tableConfig: {
                    headerButtons:[
                        {
                            label:'', // 按钮中文名
                            eventKey:'', // 按钮事件名
                            eventOption:{
                                // 当 eventKey === 'showComponrnt'
                                comName:'', // 组件名称
                            }, // 按钮事件具体配置 (点击按钮事件的配置)
                            ...elButtonConfig, //标准 el-button 配置
                        },
                        // 更多选项...
                    ],  //某一行,某一列会有按钮
                    rowButtons:[
                        {
                            label:'', //按钮中文名
                            eventKey:'', //按钮事件名
                            eventOption: {
                                // 当 eventKey === 'showComponrnt'
                                comName:'', // 组件名称
                                // 当 eventKey === ' remove '
                                params:{
                                    // paramKey = 参数的键值
                                    // rowValue = 参数值 (格式为 schema::tableKey,到 table 中找相应的字段)
                                    paramKey: rowValueKey
                                }
                            }, // 按钮事件具体配置
                            ...elButtonConfig, // 标准 el-button 配置
                        },
                         // 更多选项...
                    ],  //每一行,某一列会有按钮

                }, 
                // search-bar 相关配置
                searchConfig: {}, 
                // 动态组件
                componentConfig: {
                    // 有多种不同的配置
                    // comA:{},
                    // comB:{},
                    // crate-form 表单相关配置
                    createForm:{
                        title:'', // 表单标题
                        saveBtnText:'', // 保存按钮文案
                    }
                    // ...支持用户动态扩展
                }, 
            },
        }
        // 更多菜单项...
    ]
}

(2) 实现 解析引擎

app/pages/dashboard/complex-view/schema-view/hook/schema.js 解析模版信息

js 复制代码
import { ref, watch, onMounted,nextTick} from 'vue';
import { useRoute } from 'vue-router';
import { useMenuStore } from '$elipsStore/menu.js';

export const useSchema = function(){
    const route = useRoute();
    const menuStore = useMenuStore();

    const api = ref('');
    const tableSchema = ref({});
    const tableConfig = ref(); 
    const searchSchema = ref({});
    const searchConfig = ref();
    const components = ref({});

    // 构造 schemaConfig 相关配置,输送给 schemaView 解释;
    const buildData = function(){
        const {key, sider_key: siderKey} = route.query;

        const mItem = menuStore.findMenuItem({
            key:'key',
            value: siderKey ?? key
        });

        if(mItem && mItem.schemaConfig){
            const { schemaConfig:sConfig } = mItem;

            // 这里复制出一份拿到的sConfig.schema,目的是为了不污染原来拿到的这个数据源,因为所拿到的,可能会进行改变
            const configSchema = JSON.parse(JSON.stringify(sConfig.schema)); 

            api.value = sConfig.api ?? '';
            tableSchema.value = {};
            tableConfig.value = undefined;
            searchSchema.value = {};
            searchConfig.value = undefined;
            components.value = {};
            nextTick(() =>{
                /**
                 * tableSchema.value = configSchema;
                 * 为何 封装一个buildDtoSchema方法:
                 * 因为拿到的sConfig,可能会有很多种option的配置选项,那么如果都拿出来的话,会有许多没有用处,导致浪费;
                 * 最终的结果就是,每一个代码 都是单一的
                 */
                // 处理构造 tableSchema 和 tableConfig
                tableSchema.value = buildDtoSchema(configSchema,'table');
                tableConfig.value = sConfig.tableConfig;

                // 处理构造 searchSchema 和 searchConfig
                const dtoSearchSchema = buildDtoSchema(configSchema,'search');
                for(const key in dtoSearchSchema.properties){
                    // 如果是从其他页面跳转过来的,某些搜索项 需要有参数
                    if(route.query[key] !== undefined){
                        dtoSearchSchema.properties[key].option.default = route.query[key];
                    }
                }
                searchSchema.value = dtoSearchSchema;
                searchConfig.value = sConfig.searchConfig;

                 //构造 components = { conKey: {schema, config} }
                 const  { componentConfig } = sConfig;
                 if(componentConfig && Object.keys(componentConfig).length>0){
                    const dtoComponents = {};
                    for(const comName in componentConfig){
                        dtoComponents[comName] = {
                            schema:buildDtoSchema(configSchema,comName),
                            config:componentConfig[comName]
                        }
                    }
                    components.value = dtoComponents;                 
                } 
            });
        }
    }

    // 通用构建 schema 方法
    const buildDtoSchema = ((_schema, comName) =>{
        if(!_schema?.properties){ return };

        const dtoShema = {
            type: 'object',
            properties: {}
        }

        // 提取有效 schema 字段信息
        for(const key in _schema.properties){
            const props = _schema.properties[key];

            // tableOption schemaBarOption formOption
            if(props[`${comName}Option`]){
                let dtoPros = {};
                // 提取 props 中非 Option 的部分,存放到 dtoProps 中
                for(const pKey in props){
                    if(pKey.indexOf('Option') <0){
                        dtoPros[pKey] = props[pKey];
                    }
                }
                // 处理 comName Option
                dtoPros = Object.assign({}, dtoPros, { option: props[`${comName}Option`]});
                

                // 处理 required 字段
                const { required } = _schema;
                if(required && required.find(pk => pk === key)){
                    dtoPros.option.required = true;
                }
                 
               dtoShema.properties[key] = dtoPros;
            }
        }
        return dtoShema;
    })

    watch([
        // 监听 用户点击菜单, key / sider_key 意味着用户点击了不同的菜单; menuList 刷新页面
        () => route.query.key, 
        () => route.query.sider_key,
        () => menuStore.menuList
    ],()=>{
        buildData();
    },{ deep: true})

    onMounted(()=>{
        buildData();
    })
    
    return {
        api,
        tableSchema,
        tableConfig,
        searchSchema,
        searchConfig,
        components
    };
}

(3)预留好的各种各样的组件

这一部分指,在不同的系统中,放置不同的定制组件(20%);

每个不同的系统都会出现不一样的需求内容,那么我们需要在有80%重复内容的基础上,也支持20%的定制化内容;

代码实现

model.js 配置的是公共能力;

pdd.js , tb.js 配置的是自己定制化的能力;

如果在自己的系统和公共能力冲突时,会以本身系统的能力为主;

最终呈现效果

看下面两张图片:

一张登录的系统是pdd,一张登录的系统是tb, 两个不同的系统,展示了一样的订单管理; pdd也展示了定制化的数据分析和信息查询; 还展示了在公共的基础上,可以有对自己系统的不同内容,商品管理(拼多多) tb也展示了定制化的运营活动;

这也就诠释了,DSL设计,既有共同的内容,也可以满足每个系统不一样的内容

相关推荐
清秋3 小时前
全网最全 ECMAScript 攻略( 更新至 ES2026)
前端·javascript·编程语言
Cao_Ron3 小时前
用 SVG image symbol 画 ECharts graph 圆形节点
前端
hxy06013 小时前
React-Call让你的React组件像异步函数一样调用
前端·react.js
Hilaku3 小时前
为什么 AI 写前端时,总是优先选择 React,而不是 Vue?
前端·javascript·程序员
京东云开发者3 小时前
GPT-5.6、ChatGPT Work 与 Codex 更新食用指南
前端
Cao_Ron3 小时前
用 ECharts graph 画出卡片式矩形节点
前端
skiyee3 小时前
换个底座的 Wot Starter 居然让 AI 更懂项目!
前端·ai编程
anyup3 小时前
这一套绝了!AI 大模型写故事,星云 SDK 驱动 3D 数字人实时演绎
前端·人工智能·aigc
耀耀切克闹灬4 小时前
短链跳转
前端
孟陬4 小时前
高质量全方位的 AI 工具 sse-stuntman — Mock SSE 接口
前端·node.js·openai