里程碑三:基于 Vue3 领域模型架构建设

🐮🐴前景提要

在前端开发的日常工作中,可能会出现不同模块,功能却大致相同的需求。在没有针对这种场景做相应的对策,会导致大部分时间去重复 CRUD (增加(Create)、读取(Read)、更新(Update)、删除(Delete))操作。

尽管这些功能高度统一,但却分布在不同的业务模块下面。比如:电商管理系统中:店铺的数据表操作商品的数据表操作....等等诸多模块,开发这些模块虽然不难,但重复做这些动作,会浪费掉很多不必要的时间。

因此正对于这种场景,应该需要作出什么样的对策呢?🤔

答案是:"我们可以设计一个可配置模型,基于这个模型来渲染生成我们需要的各种各样的页面,这些页面里携带有一些需要的功能,并且支持自定义扩展"。🤩

这个模型我们取了个名字,成它为领域模型 "DSL"。

🌺领域模型 DSL 设计

json-schema

领域模型DSL的具体设计可以这样思考:

我们可以通过一份配置文件,这个配置文件描述了当前需要的一个页面内容,里面有哪些模块。假设当前的需求是这样的:"需要建一个培训人员管理模块,这个模块的功能是,入职人员录入,培训附件的录入,新增人员、修改、删除、是否完成培训这些按钮"。

那么我们可以通过一份配置描述当前页面有哪些模块。

当然我们不仅希望能通过配置能生成一个页面,也能够生成一个站点项目(包括菜单,系统登录、退出)等之诸多此类。

那么如何定义描述这份配置呢,这份配置有要遵循什么规范呢?

我们可以考虑到:json-schema。借助 json-schema 能帮助我们实现标准化和定义 JSON 数据的期望。

比如:

go 复制代码
schema: {
  type: 'object',
  properties: {
    user_id: {
      type: '', // 字段类型
      label: '',
      tableOption: {}, // 这个 user_id 字段在表格中的配置描述
      searchOption: {}, // user_id 在 搜索项中的配置描述
      components: {}, // user_id 在组件中的配置描述
      ... // 其他 Option 配置
    }
  }
}

有了一份这种详细的配置,我们就可以通过某种方式将其转换解析成页面,比如字段这样对应:

properties 中定义的 tableOption、searchOption、components 都是为了方便以及清晰的表达,当前某个字段在页面中 某个组件 的配置。tableOption 配置了 user_id 在 table 组件中的呈现、searchOption 则表示在 搜索栏中的呈现。

当然 Option 中则描述了这个组件具体配置信息,比如 tableOption :

ruby 复制代码
tableOption: {
    ...elTableColumnConfig, // 标准的 el-table-column 配置
    toFixed: 0, // 保留几位小数
    visible: true, // 默认为 true( false,表示不在表单中显示 )
},

这段配置,不仅能描述 el-ui 组件中的配置,也能描述自定义配置 比如处理小数的 toFixed。

上面 json-schema 不仅能配置描述一个页面内容,也能配置描述一个项目站点。

详细配置:

csharp 复制代码
{
    name: '', // 项目名称
    desc: '', // 项目描述
    homePage: '', // 项目主页
    menu: [{
        key: '', // 菜单的 key
        moduleType: '', // 模块类型
        iframeConfig: {},
        siderConfig: {},
        ... // 其他 Config 
    }]
}

通过上面的配置,我们就可以描述一个项目包含哪些东西,菜单 以及 其对应的子页面。子模块有能描述这个子模块的详细信息:

go 复制代码
schemaConfig: {
    api: '', // 数据接口 Api
    schema: { // schema-view 的描述
        type: '', // 类型
        properties: { // 必要的参数
            key: { // 某业务 key
                type: '', // 类型
                label: '', // 中文名
                tableOption: {}, // 表格配置
                searchOption: {} // 搜索项配置
            }
        }
    },
    // 表格 中 按钮的描述
    tableConfig: {
        headerButtons: [],
        rowButtons: []
    }
}

上面那个配置描述了 schema-view 的的详细配置。

有了这份 json-schema 配置之外,我们还需要额外处理配置中的一种情况:假设我建立了两个项目站点:"A办公平台"、"B办公平台"。然后里面都有 "培训人员管理" 子模块。

mode 模块继承和扩展

如果在这种情况下,我们写两份 json-schema 数据配置,显然是不合理的。我们做这件事的核心是 "整合 80%" 重复性的工作。那么我们是否可以这样考虑,假设提供一个公共的 json-schema 数据,在这份配置的基础上扩展出其他的配置,比如 提供一个 "培训人员管理" 基础配置,然后 让它继承到 "A办公平台" 和 "B办公平台" 上,这样就会更合理一些。

提供一个基础类配置,让它继承到 "A扩展类"、"B扩展类"。然后在创建对应的系统。这样相同模块就能重复利用。

当然我们还需要细致入微的再深入考虑一下继承的问题,比如说:"B办公平台存在采购模块,办公平台基础类也存在采购模块呢?或者 B办公平台 采购模块中的内容比办公平台基础类多一个配置呢?"。

这种特殊情况也是需要考虑的,具体的做法可以通过对比两者之间是否存在差异,有差异则修改,没有则新增的判断方式处理。

ini 复制代码
const _ = require('lodash');
const glob = require("glob");
const path = require("path");
const { sep } = path;

/**
 * 解析 model 配置,并返回组织且继承后的数据结构
 * [{
 *  modelKey: ${model},
 *  project: {
 *     proj1key: ${proj1},
 *     proj2key: ${proj2}
 *  }
 * }]
 */
module.exports = (app) => { 
    const modelList = [];

    // 遍历当前文件夹,构建模型数据结构,挂载到 modelList 上
    const modelPath = path.resolve(app.baseDir, `.${sep}model`);
    const fileList = glob.sync(path.resolve(modelPath, `.${sep}**${sep}**.js`));
    fileList.forEach(file => {
        if (file.indexOf('index.js') > -1) return;

        // 区分配置类型(model 或 project)
        const type = file.indexOf(`${sep}project${sep}`) > -1 ? 'project' : 'model';

        if (type === 'project') {
            const modelKey = file.match(//model/(.*?)/project/)?.[1];
            const projKey = file.match(//project/(.*?).js/)?.[1];
            let modelItem = modelList.find(item => item.model?.key === modelKey);
            
            // 初始化 model 数据结构
            if (!modelItem) {
                modelItem = {};
                modelList.push(modelItem);
            }
            // 初始化 project 数据结构
            if (!modelItem.project) {
                modelItem.project = {};
            }

            modelItem.project[projKey] = require(path.resolve(file));
            modelItem.project[projKey].key = projKey;
            modelItem.project[projKey].modelKey = modelKey;
        };

        if (type === 'model') {
            const modelKey = file.match(//model/(.*?)/model.js/)?.[1];
            let modelItem = modelList.find(item => item.model?.key === modelKey);
            if (!modelItem) {
                modelItem = {};
                modelList.push(modelItem);
            }
            modelItem.model = require(path.resolve(file));
            modelItem.model.key = modelKey;
        };
    });

    // 数据进一步整理:project => 继承 model
    modelList.forEach(item => {
        const { model, project } = item;
        for(const key in project) {
            project[key] = projectExtenModel(model, project[key]);
        }
    })

    return modelList;
}
javascript 复制代码
/**
 * 项目继承 model 配置
 * @param {*} model 模型配置
 * @param {*} project 项目配置
 * @returns 合并后的配置
 */
const projectExtenModel = (model, project) => {
    return _.mergeWith({}, model, project, (modelValue, projValue) => {
        // 处理数组何必的特殊性情况
        if (Array.isArray(modelValue) && Array.isArray(projValue)) {
            let result = [];

            // 因为 project 继承 model,所以需要处理修改和新增内容的情况
            // project 有的键值,model 也有 => 修改
            // project 有的键值,model 没有 => 新增
            // model 有的键值,project 没有 => 保持不变

            // 处理修改
            for (let i = 0; i < modelValue.length; i++) {
                let modelItem = modelValue[i];
                const projItem = projValue.find(projItem => projItem.key === modelItem.key);
                // project 有的键值,model 也有,则递归调用 projectExtendModel 方法覆盖修改
                result.push(projItem ? projectExtenModel(modelItem, projItem) : modelItem);
            }

            // 处理新增
            for (let i = 0; i < projValue.length; i++) {
                const projItem = projValue[i];
                const modelItem = modelValue.find(modelItem => modelItem.key === projItem.key);
                if (!modelItem) {
                    result.push(projItem);
                }
            }

            return result;
        }
    });
}

模版组件设计

有了数据之后,但还需要一个能够更具配置来渲染页面的模版,那么这个模版如何设计呢?

dahboard

我们建立一个模版入口页面比如叫它 "dahboard",但也可以是其他:"dahboard2"、"dahboard3"等等诸多此类。

关于组件的设计,我们可以遵循 "高内聚低耦合" 的宗旨来设计组件。

dahboard 页面,我们可以放一个 header-view 组件,在 header-view 我们放公共部分,像 "头部菜单" 组件,"右侧菜单" 组件,"content 内容" 插槽。

然后 将子模块内容渲染到 "content 内容" 插槽 中,比如:"schema-view"、"iframe-view" 插入到 "content 内容" 里面显示。

业务契合度不是很高的页面封装成公共组件、将业务深度关联的页面封装成业务组件。这样做的好处是能够降低维护成本、以及随着业务不断叠加,减缓熵增(孤立系统中无序程度不断增加的趋势)的情况。

schema-view & iframe-view

在 业务组件里面也遵命 "高内聚低耦合" 的宗旨来设计:

schema-view 中 放 search-panle 和 table-panle 。在这两个 panle 中实现 search 组件和 table 组件。

总结:

DSL的设计核心:可以根据 json-schema 配置,通过解析引擎,来渲染出对应的站点和页面。这份 json-schema配置也可以扩展 和 继承。根据配置的 json-schema 扩展自定义需求、自定义按钮无限扩展其他的页面,从而实现沉淀 80% 的重复性功能重新开发的根本痛点,提高开发效率。

相关推荐
玉米Yvmi2 小时前
大文件上传的基石:切片上传原理与实现详解
前端·javascript·面试
用户4099322502122 小时前
Composable的命名规矩和参数约定,别再瞎写了
前端·javascript·后端
用户游民2 小时前
Flutter Provider原理以及用法
前端·flutter
Rust研习社2 小时前
告别环境混乱!使用 mise 管理你的开发环境
前端·后端·rust
小小荧2 小时前
Vue Native多分支迭代,Vue跨端原生生态迎来革新
前端·javascript·vue.js
EntyIU2 小时前
uv工程化项目指南
前端·python·uv
WebGirl3 小时前
如何在VS code中添加SKill
前端
marsh02063 小时前
49 openclaw故障排查:系统异常时的诊断方法
服务器·前端·青少年编程·ai·php·技术美术
Maimai108083 小时前
前端如何落地 SSE:从实时评论到可复用的实时数据 Hook
前端·javascript·react.js·前端框架·web3·状态模式·webassembly