最近由于项目需要,需要非开发人员根据业务数据的手动录入表单并且可以同步输出对应的JSON数据.记录下整个业务分析和最终实现的过程.
前提
基于以上的业务流程,最初在没有对JSON Schema有足够的了解下,采用了动态表单和ng-content
来实现,将JSON Schema但是最终出来的效果使整个表单业务过于复杂,无法清晰的分清表单的层次,不易操作;
之后在整体梳理了业务和对JSON Schema有了一定的了解后,调整了整个逻辑,最初的想法是将JSON Schema的通用逻辑完全实现一遍,这样在后续维护中,只需要替换新的JSON Schema并维护Schema规则即可.
在这个分析过程中一个重要的难点就是如何确定每一层表单的边界?
针对这个问题确实很难确定,要结合业务来做明确的边界,这里也是一直以来苦恼的地方,如何确定一个即抽象又符合业务的边界影响着日后的维护工作的难度.由于业务的需要,所以在分析Schema的过程中如果一旦遇到以下情况时就是每一层业务表单的边界.
type
类型为object
,其中allOf
、anyOf
、oneOf
等只是对当前层级下的规则补充
type
类型为array
并且存在items
以下情况可以视为同层的表单属性
type
类型为number
时,Form类型为input-number
type
类型为string
时,需要区分可输入和不可输入,如果为固定值const
,From类型为label
,否则就是input
上面这些分析看似离成功很近,但是忽略了一个重要的因素,JSON Schema的规则定义是一个十分宽泛且抽象的,业务中一旦到了分界点,可能存在的情况又回到了Schema本身,这无疑将规则无限放大,如果希望达到最初的要求,就要和业务JSON Schema共同制定一套符合业务需要的规则,这无疑将一件简单的事情变得异常复杂,所以在最终在权衡后大致将整个业务流程梳理为:
技术细节
- JSON输出
- 树
- 动态表单控件
1.JSON输出
这一部分不需要过多赘述,最终将树递归生成业务数据展示即可
2.树
由于业务中需要展示表单的层次结构,所以在选择要使用哪种树时,最终决定普通的数组树即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| interface TreeNode { /** * 结点ID */ id: number; /** * 父结点ID */ parentId: number; data: { /** * 表单控件 */ form: DynamicFormBase; /** * 分组索引,满足复合(数组表单)表单业务逻辑 * 关联兄弟结点 */ groupIndex: number | null; }; children: Array<TreeNode>; }
|
3. 动态表单控件
动态表单的类型定义
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89
| class DynamicFormBase<T = AsAny.AsAny> { /** * 表单值 */ value: T | undefined; /** * key */ key: string; /** * 标签 */ label: string; /** * 提示 */ placeholder: string; /** * 错误提示 */ errorTip: string; /** * 必填 */ required: boolean; /** * 是否可见 */ isVisiable: boolean; /** * 排序 */ order: number; /** * 表单控件类型 */ controlType: DynamicFormControlType; /** * input-number */ min: number; max: number; step: number; /** * select */ options: Global.Selects<AsAny.AsAny>;
/** * 参考JSON Schema * 用于控制同级表单项之前的逻辑关系 */ allOf: Array<FormSchema<T>>;
constructor( options: { value?: T; key?: string; label?: string; placeholder?: string; errorTip?: string; required?: boolean; isVisiable?: boolean; order?: number; min?: number; max?: number; step?: number; controlType?: DynamicFormControlType; options?: Global.Selects<AsAny.AsAny>; allOf?: Array<FormSchema<T>>; } = {} ) { this.value = options.value; this.key = options.key || ''; this.label = options.label || ''; this.placeholder = options.placeholder || ''; this.errorTip = options.errorTip || ''; this.required = !!options.required; this.isVisiable = !!options.isVisiable; this.order = options.order === undefined ? 1 : options.order; this.min = options.min === undefined ? 1 : options.min; this.max = options.max === undefined ? 10 : options.max; this.step = options.step === undefined ? 1 : options.step; this.controlType = options.controlType || DynamicFormControlType.title; this.options = options.options || []; this.allOf = options.allOf || []; } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| enum DynamicFormControlType { /** * input输入框 */ input = 'input', /** * input_number输入框 */ inputNumber = 'inputNumber', /** * 仅显示 */ title = 'title', /** * 标签输入 */ tagsSelect = 'tagsSelect', /** * 选择器 */ select = 'select', /** * 嵌套表单(单个) */ singleForm = 'singleForm', /** * 嵌套表单(多个) */ combinedForm = 'combinedForm' }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| /** * 表单控件Schema(灵感来源于JSON Schema) * if:根据当前表单值来确定是否执行then内的逻辑 * then:if验证成功后决定执行的逻辑 * -required 必填 * -visiable 显示 * -hidden 隐藏 */ interface FormSchema<T> { if: (value: T | undefined) => boolean; then: () => FormValidation; }
interface FormValidation { /** * 表单控件唯一key * if验证成功后将对应key的表单修改为必填 */ required?: Array<string>; /** * 表单控件唯一key * if验证成功后将对应key的表单修改为可见 */ visiable?: Array<string>; /** * 表单控件唯一key * if验证成功后将对应key的表单修改为不可见 */ hidden?: Array<string>; }
|
最终效果:https://dev.criterion-editor.vosbyte.com/json-editor