前言
在上一篇动态表单设计文章详细讲解了动态表单的设计、具体的数据轮流转。接下来讲解一下我的问卷可视化编辑器的schema是如何设计的。
Schema 拆分设计
首先来看看动态表单设计的定义schema
属于大包大揽的设计,这种设计随着
编辑器功能变多,schema会变得非常臃肿庞大会导致:
- 整个项目加载变慢
- 修改维护变得异常困难
- 组件之间schema变动相互影响
针对如上问题,我针对编辑器的schema设计进行了优化,让schema像乐高积木一样模块化、可复用、按需求加载
1. 按组件拆分schema
components/SO(单选)
├── config.js # 组件schema定义
├── config.vue # 配置区域视图组件
├── index.js # 物料列表schema定义 (图片、组件名称、分类等)
└── index.vue # 编辑器区域视图组件
SO的config.js定义单选按钮组件属性、MSS的config.js定义矩阵多选子题组件属性。其他更多组件以此类推,实现了每个组件之间的模块化、可扩展性
2. 公共部分抽离
把重复的公用的属性定义到PublicConfigClass中。
在其他schema中只需要通过extend继承这个PublicConfigClass基类既可以。我们还可以在当前组件覆盖掉基类属性值,例如
version=1.1.0表示使用基类默认的version=1.0.0
实现
复用性以及可扩展性,,假如想额外的定义样式的公共属性只需要在声明PulicStylesConfig即可。
3. 物料列表schema与编辑器schema分离
在动态表单设计会把物料列表的schema耦合到编辑器schema中,例如物料的图片地址、类别等是不需要在渲染引擎体现的,因此进行了拆分
4. 动态加载组件以及配置信息(按需加载)
// 双击物料组件将组件添加到编辑区
const dblclickHandle = async (item) => {
// 动态注册组件
componentInstall(item.viewKey, fetchViewComponent(item));
componentInstall(item.conKey, fetchConfigComponent(item));
// 创建新组建
const newComponent = await createComponent(item);
...
};
动态的加载schema文件
// $ 配置信息缓存,提升组件加载速度
const componentCache = new Map();
/** 获取config.js文件 */
const loadConfig = (packageName, categoryName, keyName) => {
const key = packageName + categoryName + keyName
if (!componentCache.has(key)) {
componentCache.set(key, import(`./components/${packageName}/${categoryName}/${keyName}/config.js`))
}
return componentCache.get(key)
}
/** 获取目标的配置信息 */
export const createComponent = async (targetData) => {
const { category, key } = targetData
const config = await loadConfig(targetData.package, category, key)
return new config.default()
}
动态加载物料的组件(视图/配置)
// 遍历获取所有题库的组件
const configModules = import.meta.glob('./components/**/config.vue', { eager: true });
const indexModules = import.meta.glob('./components/**/index.vue', { eager: true });
/**
* 获取组件
* @param {string} name
* @param {boolean} isView
*/
const fetchComponent = (name, isView) => {
// 判断是加载的是config还是index组件
const module = isView ? indexModules : configModules
for (const key in module) {
const urlSplit = key.split('/')
const componentName = urlSplit[urlSplit.length - 2]
// 找到对应组件的文件
if (componentName === name) {
return module[key]
}
}
}
/** 获取展示组件 */
export const fetchViewComponent = (item) => {
const { key } = item
return fetchComponent(key, true).default
}
/** 获取配置组件 */
export const fetchConfigComponent = (item) => {
const { key } = item
return fetchComponent(key, false).default
}
- 总结
经过如上四个步骤优化,使得编辑器具备
- 加载更快: 只加载当前需要的Schema
- 更好维护性: 修改
单选组件定义不会影响到其他组件定义 - 更易扩展: 新增组件只需要添加一组
config.js、config.vue、index.js、index.vue即可 - class定义schema优点: 使得schema支持继承和组合、new实例不影响模版schema数据等
schema体积优化
在针对复杂的组卷时,其会包含很多题目组件会导致整张试卷的schema过于庞大,而Schema拆分设计来提升了可维护性和加载效率以及可扩展性,但没有真正的减少schema的体积,下面是我对schema的一些体积优化考量
1. 极致简化属性名
// 优化前(32字节)
{
"fontSize": "16px",
"fontWeight": "bold"
}
// 优化后(18字节,减少43%)
{
"fs": "16px", // fontSize → fs
"fw": "bold" // fontWeight → fw
}
2. 枚举值数字化
// 优化前(48字节)
{
"alignment": ["left", "center", "right"],
"size": ["small", "medium", "large"]
}
// 优化后(26字节,减少45%)
{
"align": [0, 1, 2], // 0=left, 1=center, 2=right
"sz": [1, 2, 3] // 1=small, 2=medium, 3=large
}
3. 结构扁平化
// 优化前(多层嵌套)
{
"style": {
"text": {
"color": "#333",
"size": "14px"
}
}
}
// 优化后(扁平结构)
{
"textColor": "#333", // style.text.color → textColor
"textSize": "14px" // style.text.size → textSize
}
4. 使用数组代替对象
// 优化前(每个对象有键名)
{
"items": [
{ "id": 1, "type": "text" },
{ "id": 2, "type": "image" }
]
}
// 优化后(固定顺序的数组)
{
"items": [
[1, "text"], // [id, type]
[2, "image"]
]
}
5. schema字典化
// 建立字典(服务端与客户端共享)
const DICT = [
'fontSize', 'fontWeight', 'color',
'width', 'height', 'text'
];
// 传输时用索引代替键名
{
"0": "16px", // 0=fontSize
"1": "bold" // 1=fontWeight
}
这些方式都是根据失去可读性来实现缩减schema体积大小的方式。(如果你有更好的方案也可以分享给我哦,谢谢)
schema中的version
在单个组件的schema定义中会存在version版本属性
该属性主要用于确定加载
当前组件的具体版本,因为我们的组件会被迭代升级,下一次schema的改动可能会不兼容当前的schema,因此为了不影响此前已经构建好的组件数据,引入了version的理念动态加载远程不同版本的物料组件,实现多版本共存
总结
通过上面的步骤,我们对schema拆分设计、如何优化schema体积以及引入schema中version理念有了认知,此次设计到的代码源码,后期计划会进行编辑器出码和编辑器的可插拔设计,希望这篇文章对大家有所帮助也祝大家万事如意。