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

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

重点分享 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设计,既有共同的内容,也可以满足每个系统不一样的内容

相关推荐
mCell6 小时前
如何零成本搭建个人站点
前端·程序员·github
mCell7 小时前
为什么 Memo Code 先做 CLI:以及终端输入框到底有多难搞
前端·设计模式·agent
恋猫de小郭7 小时前
AI 在提高你工作效率的同时,也一直在增加你的疲惫和焦虑
前端·人工智能·ai编程
少云清7 小时前
【安全测试】2_客户端脚本安全测试 _XSS和CSRF
前端·xss·csrf
银烛木8 小时前
黑马程序员前端h5+css3
前端·css·css3
m0_607076608 小时前
CSS3 转换,快手前端面试经验,隔壁都馋哭了
前端·面试·css3
听海边涛声8 小时前
CSS3 图片模糊处理
前端·css·css3
IT、木易8 小时前
css3 backdrop-filter 在移动端 Safari 上导致渲染性能急剧下降的优化方案有哪些?
前端·css3·safari
0思必得08 小时前
[Web自动化] Selenium无头模式
前端·爬虫·selenium·自动化·web自动化
anOnion8 小时前
构建无障碍组件之Dialog Pattern
前端·html·交互设计