表单场景的复杂度
首先我们需要思考:表单场景的复杂度在哪里
- 字段数量多,性能随字段数增加变差
- 字段关联时逻辑复杂
- 表单数据管理复杂
- 表单状态管理复杂
- 表达场景化复用性
核心思想
精确渲染
在react下实现一个表单,如果想要手机表单数据,实现一些联动需求,大多数都是通过setState实现字段收集,虽然实现简单,但是却引入了性能问题:每次输入都会导致全量渲染,虽然有diff算法,但是diff也有计成本,从时间复杂度看的话,初次渲染表单时O(n) ,字段输入时也是 O(n),这样是明显不合理的。 所以,真正想要实现精确渲染,必须要学习 mobx的依赖追踪机制和响应式模型的抽象能力(@formily/reactive)
领域模型(DomainModel)
domainModel是对 特定问题领域的概念类、现实世界中的实体类 以及 它们之间关系的可视化表示。“主要用于详细描述问题领域本身”。(就像:数据EQ图中的实体)
通过常见的范例,抽象出一个最简单的field模型
:
例如:
- 需求为根据某些字段的值,去隐藏或者禁用某些字段(需要 value、disabled、visible)
- 在一个表单树中,我们要索引一个字段,字段的路径一定要有(需要 path)
- 字段对应的UI组件,以及组件的配置化参数 要可以管理吧(需要 Component,ComponentProps)
- 还有很多
interface Field {
path:string[],
value:any,
visible:boolean,
disabled:boolean,
component:[Component,ComponentProps]
}
为了解决联动问题,fomrily需要抽象出字段模型,包含字段所有的状态,只要操作这些状态就可以引发联动; 经过formily的不断试错和纠正,设计出了真正优雅的表单模型; 有了这样的领域模型,就可以让表单的联动变得可枚举可预测;并为后面得协议描述打下坚实基础。
路径系统
想要设计更完备的话,其实不止需要字段模型,还要有一个表单模型
作为顶层模型
,顶层模型管理着所有字段模型,每个字段就可以有自己的路径,formily为了大家可以优雅的查找某个字段,独创了自己的路径系统:@formily/path (有学习成本,想用就用)
field.query('.aa').value() //直接读取同级别aa字段值
field.query('..aa').value() //读取父级aa字段值
field.query('..[+].aa') //读取跨级相邻aa字段值
生命周期
借助mobx和路径系统,已经打造了一个比较完备的表单方案了,但是这个方案就像一个黑盒,外界无法感知方案内部状态流转的过程,想在某些过程阶段内实现一些逻辑无法实现,因此!要引入另一个概念:生命周期
。
将表单的生命周期作为事件钩子
暴露给外界,就能做到既有抽象,但又灵活的表单方案。
协议驱动
如果要实现动态可配置表单
,那必然是需要将 表单结构 变得 可序列化
序列化方式可以是:以UI为思路的UI描述协议、也可以是以数据纬四路的数据描述协议。
因为表单本身就是为了维护一份数据,所以 数据描述协议是最合适不过的。而想要描述数据结构,最合适的就是 JSON-schema(一种描述JSON数据的声明性格式,可用来:简单描述数据的表面结构、根据它自动验证数据)。
但是光描述数据还不够,因为我们要输出的是实际业务可用的表单页面,所以formily的表单协议在 JSON-Schema上做了扩展,以 x-*
格式来表达扩展属性。
{
"type": "string",
"title": "字符串",
"description": "这是一个字符串",
"x-component": "Input",
"x-component-props": {
"placeholder": "请输入"
}
}
这样看来,虽然 UI协议和数据协议混合在一起,但是只要有统一的扩展约定,也还能保证两种协议职责单一。
- 然后,我们日常开始为了实现UI层面的要求,会有一些单纯的UI组件怎么描述呢?
formily定义了一个新的schema type:void
,在formily中,void代表一个虚拟数据节点,该节点不占用实际数据结构。
{
"type": "void",
"title": "卡片",
"description": "这是一个卡片",
"x-component": "Card",
"properties": {
"string": {
"type": "string",
"title": "字符串",
"description": "这是一个字符串",
"x-component": "Input",
"x-component-props": {
"placeholder": "请输入"
}
}
}
}
- 再然后,日常开发中有些表单联动的逻辑怎么用 JSON-shema 描述呢?
formily扩展了
x-reactions
,使用方式如下:
比如一个字段要控制另一个字段的显示隐藏
{
"type": "object",
"properties": {
"source": {
"type": "string",
"title": "Source",
"x-component": "Input",
"x-component-props": {
"placeholder": "请输入"
}
},
"target": {
"type": "string",
"title": "Target",
"x-component": "Input",
"x-component-props": {
"placeholder": "请输入"
},
"x-reactions": [
{
"dependencies": ["source"],
"when": "{{$deps[0] == '123'}}",
"fulfill": {
"state": {
"visible": true
}
},
"otherwise": {
"state": {
"visible": false
}
}
}
]
}
}
}
仔细观察,formily干净的描述了联动的几个关键点:
- 依赖方
dependencies
- 条件
when
- 满足条件-做什么
fulfill
- 不满足条件-做什么
otherwise
并且 x-reactions
是一个数组,可添加随意添加多条联动关系。
经过这些,我们的表单完全可以使用协议来描述了!(并且是配置化的描述感觉,很清晰)
分层架构
formily如何分层架构,才能更加自洽且优雅呢?
formily分为:
- 内核层(formily core)
- UI桥接层(React、Vue)
- 扩展组件层 (AntD,Element)
- 配置应用层(表单配置器)
- JSON-schmea独立存在
内核层与UI无关,保证了用户管理的逻辑和状态不耦合任何一个框架
JSON-schema 独立存在,给UI桥接层消费,保证了协议驱动在不同UI框架下的绝对一致性。
扩展组件层,保证用户开箱即用,无需使用者花时间做二次开发。
核心优势
总结下formily的核心优势:
- 高性能(proxy 响应式更新)
- 开箱即用
- 联动逻辑实现高效
- 跨端能力,跨框架能力,复用性强
- 动态渲染能力(根据条件、依赖、数据变化和用户输入等动态渲染表单,formik、antD Form等插件想实现动态渲染都是靠外部js逻辑处理的)。