DSL是什么
DSL:Domain-Specific Language,即领域特定语言。 领域模型:是对业务领域中核心概念、数据、规则和流程的抽象化、结构化的知识表示。它不是一个具体的框架,而是一个概念模型。
领域模型设计
设计领域模型基类,继承派生出具体的子类,使用配置来表示一个项目,根据这一份配置可以抽象出页面中的表格信息,搜索信息,以及数据库表信息。
0、首先设计出一套基本的领域模型模版,后续在开发过程中逐步添加需要的配置。
由于考虑要校验字段的合法性,定义时,参考标准schema模型设计,配置每个字段,每个字段中有相对应的option选项,来配置search或者table等地方该字段的表现形式。
1、设计一个项目配置项的解析器,针对不同模型及其下的项目进行解析,得到配置信息。
const glob = require('glob');
const path = require('path');
const { sep } = path;
/**
* project 继承 model
* @param {object} model
* @param {object} project
*/
const projectExtendModule = (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也有,则递归调用 projectExtendModule 方法覆盖修改
result.push(
projItem ? projectExtendModule(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;
}
});
};
/**
* 解析 model 配置,并返回组织且继承后的数据结构
[{
model: ${model},
project: {
proj1Key: ${proj1},
proj2Key: ${proj2}
},
}, ...]
* @param {object} app
*/
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];
const modelItem = modelList.find((item) => item.model?.key === modelKey);
if (!modelItem) {
// 初始化 model 数据结构
modelItem = {};
modelList.push(modelItem);
}
if (!modelItem.project) {
modelItem.project = {};
}
modelItem.project[projKey] = require(file);
modelItem.project[projKey].key = projKey; // 注入 projectKey
modelItem.project[projKey].modelKey = 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; // 注入 modelKey
}
});
// 数据进一步整理,project要继承model
modelList.forEach((item) => {
const { model, project } = item;
for (const key in project) {
project[key] = projectExtendModule(model, project[key]);
}
});
return modelList;
};
2、开发dashboard页面入口,该入口打包后的产物由koa服务渲染。前端使用vue-router进行路由管理,在开发过程中,发现项目id这个字段在项目页面调接口时是一直使用的,考虑服务端应该要能直接拿到该参数,前端路由hash模式不适合,所以应该使用路由的history模式,此时对代码进行轻量重构。(在开发过程中,发现有方案改动,及时进行重构,以免后续重构困难。)
3、页面中实现的功能需要抽象出来的组件模块。根据DSL定义的模型,该模型中有菜单,有不同类型页面,要实现菜单模块及不同类型页面。
实现一个hook,提取数据
定义一个hook,实现方法来从全量数据中提取需要的数据,消除噪音。在不同模块中使用时按需获取。
schema类型页面,是一个可根据配置项生成的表格页面。包括表格区域,头部搜索区域,除了这些配置项,还预留可自行扩展区域,供开发者定制扩展自己的组件。
组件封装时需要考虑什么
在封装组件的时候考虑三要素:接收什么参数(defineProps、provide/inject、eventBus...);暴露什么事件(defineEmoits);提供什么方法(defineExpose)。 如下例组件:
<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"
>
<!-- 展示子组件 动态组件-->
<!-- vue动态组件 -->
<component
:ref="searchComList"
:is="SearchItemConfig[schemaItem.option?.comType]?.component"
:schemaKey="key"
:schema="schemaItem"
@loaded="handleChildLoaded"
></component>
</el-form-item>
<el-form-item>
<!-- 操作区域 -->
<el-button type="primary" plain class="search-btn" @click="search">
搜索
</el-button>
<el-button type="primary" plain class="reset-btn" @click="reset">
重置
</el-button>
</el-form-item>
</el-form>
</template>
<script setup>
import { ref, toRefs } from 'vue';
import SearchItemConfig from './search-item-config.js';
const props = defineProps({
/**
* schema 配置,结构如下:
{
type: 'object',
properties: {
key: {
...schema, // 标准schema配置
type: '', // 字段类型
label: '', // 字段的中文名
// 字段在 search-bar 中的相关配置
option: {
...eleComponentConfig, // 标准 el-component 组件的配置
comType: '', // 配置组件类型 input / select 或其他
default: '', // 默认值
},
},
...
},
};
*/
schema: Object,
});
const { schema } = toRefs(props);
const emit = defineEmits(['load', 'search', 'reset']);
const searchComList = ref([]); // 所有的动态组件
const getValue = () => {
let dtoObj = {};
// 遍历列表,可以获取所有的动态组件
searchComList.value.forEach((component) => {
dtoObj = {
...dtoObj,
...component?.getValue(),
};
});
return dtoObj;
};
let childComLoadedCount = 0;
const handleChildLoaded = () => {
childComLoadedCount++;
if (childComLoadedCount >= Object.keys(schema?.value?.properties).length) {
emit('load', getValue());
}
};
const search = () => {
emit('search', getValue());
};
const reset = () => {
searchComList.value.forEach((component) => {
component.reset();
});
emit('reset');
};
defineExpose({
reset,
getValue,
});
</script>
<style lang="less">
.schema-search-bar {
min-width: 500px;
.input,
.select,
.dynamic-select {
width: 280px;
}
.search-btn {
width: 100px;
}
.reset-btn {
width: 100px;
}
}
</style>
动态组件使用
<component
ref="searchComList"
//is参数是接受具体的组件
:is="SearchItemConfig[schemaItem.option?.comType]?.component"
// 动态组件的接收参数 schemaKey及schema
:schemaKey="key"
:schema="schemaItem"
// 动态组件emit出的事件
@loaded="handleChildLoaded"
></component>
总结:在设计DSL时,一套模版里是包含了这个项目中的多个配置,页面展示及数据库表设计都可以从这个模版中提取出来,注意模型的设计。