# vite-formily-project **Repository Path**: bill_law6/vite-formily-project ## Basic Information - **Project Name**: vite-formily-project - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2024-06-10 - **Last Updated**: 2024-06-10 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # Formily 学习项目 ## [Formily/reactive](https://reactive.formilyjs.org/zh-CN/guide/concept) @formily/reactive 的核心思想是参考 Mobx 的,那为什么要重新造轮子呢? 主要有 4 点原因: - mobx 不支持 action 内部进行依赖收集 - mobx 的 observable 函数不支持过滤 react node,moment,immutable 之类的特殊对象 - mobx 的 observable 函数会自动将函数变成 action - mobx-react-lite 的 observer 不支持 React 并发渲染 基于以上原因,formily 不得不重新造轮子,不过该轮子是强依赖 Proxy 的,也就是不支持 IE 浏览器,当然,重新造轮子也有它的好处: - 把控性更强,可以为 formily 场景做更深的优化定制 - 不用考虑 Mobx 的历史包袱,代码可以更干净 - 如果 Mobx 版本 Break Change 或者存在安全漏洞,对 Formily 无影响 ### 核心概念 #### Observable observable 是响应式编程模型中最重要的一块,它的核心概念就是: 一个 observable 对象,字面意思是可订阅对象,我们通过创建一个可订阅对象,在每次操作该对象的属性数据的过程中,会自动通知订阅者,@formily/reactive 创建 observable 对象主要是通过 ES Proxy 来创建的,它可以做到完美劫持数据操作 我们在@formily/reactive 中主要用以下几个 API 来创建 observable 对象: - observable 函数创建深度 observable 对象 - observable.deep 函数创建深劫持 observable 对象 - observable.shallow 函数创建浅劫持 observable 对象 - observable.computed 函数创建缓存计算器 - observable.box 函数创建带 get/set 方法的 observable 对象 - observable.ref 函数创建引用级 observable 对象 - define 函数定义 observable 领域模型,可以组合 observable 函数与其静态属性(比如 observable.computed)函数完成领域模型的定义 - model 函数定义自动 observable 领域模型,它会将 getter setter 属性包装为 computed 计算属性,将函数包装为 action,将其他数据属性用 observable 包装(注意这里是深劫持) #### Reaction reaction 在响应式编程模型中,它就相当于是可订阅对象的订阅者,它接收一个 tracker 函数,这个函数在执行的时候,如果函数内部有对 observable 对象中的某个属性进行读操作(依赖收集),那当前 reaction 就会与该属性进行一个绑定(依赖追踪),直到该属性在其他地方发生了写操作,就会触发 tracker 函数重复执行,用一张图表示: 可以看到从订阅到派发订阅,其实是一个封闭的循环状态机,每次 tracker 函数执行的时候都会重新收集依赖,依赖变化时又会重新触发 tracker 执行。所以,如果一旦我们不想再订阅 reaction 了,一定要手动 dispose,否则会内存泄漏。 在@formily/reactive 中的我们主要是使用以下几个 API 来创建 reaction: autorun 创建一个自动执行的响应器 reaction 创建一个可以实现脏检查的响应器 Tracker 创建一个依赖追踪器,需要用户手动执行追踪 ##### Computed computed 在响应式编程模型中也是属于一个比较重要的概念,一句话表达的话,computed 是一个可以缓存计算结果的 Reaction 它的缓存策略是:只要 computed 函数内部所依赖的 observable 数据发生变化,函数才会重新执行计算,否则永远读取缓存结果 这里要求的就是 computed 函数必须是纯函数,内部依赖的数据要么是 observable 数据,要么是外部常量数据,如果是外部变量数据(非 observable),那如果外部变量数据发生变化,computed 是不会重新执行计算的。 ##### Batch 前面有讲到@formily/reactive 是基于 Proxy 劫持来实现的响应式编程模型,所以任何一个原子操作都会触发 Reaction 执行,这样明显是浪费了计算资源的,比如我们有一个函数内部是对多个 observable 属性进行操作的: ```javascript import { observable, autorun } from "@formily/reactive"; const obs = observable({}); const handler = () => { obs.aa = 123; obs.bb = 321; }; autorun(() => { console.log(obs.aa, obs.bb); }); handler(); ``` 这样就会执行 3 次打印,autorun 默认执行一次,加上 obs.aa 赋值执行一次,obs.bb 赋值执行一次,如果原子操作更多一些,那执行次数会更多,所以,我们推荐使用 batch 模式,将更新进行合并: ```javascript import { observable, autorun, batch } from "@formily/reactive"; const obs = observable({}); const handler = () => { obs.aa = 123; obs.bb = 321; }; autorun(() => { console.log(obs.aa, obs.bb); }); batch(() => { handler(); }); ``` 当然,我们也可以使用 action 进行高阶包装: ```javascript import { observable, autorun, action } from "@formily/reactive"; const obs = observable({}); const handler = action.bound(() => { obs.aa = 123; obs.bb = 321; }); autorun(() => { console.log(obs.aa, obs.bb); }); handler(); ``` 最终执行次数就是 2 次了,即便 handler 内部的操作再多也还是 2 次 #### 最佳实践 在使用@formily/reactive 的时候,我们只需要注意以下几点即可: 尽量少用 observable/observable.deep 进行深度包装,不是非不得已就多用 observable.ref/observable.shallow,这样性能会更好 领域模型中多用 computed 计算属性,它可以智能缓存计算结果 虽然批量操作不是必须的,但是尽量多用 batch 模式,这样可以减少 Reaction 执行次数 使用 autorun/reaction 的时候,一定记得调用 dispose 释放函数(也就是调用函数所返回的二阶函数),否则会内存泄漏 --- ## [Formily/core](https://core.formilyjs.org/zh-CN/guide/architecture`) ### 核心架构 #### 领域模型 Formily 内核架构非常复杂,因为要解决一个领域级的问题,而不是单点具体的问题,先上架构图: ![架构图](./public/formily_core.svg) ##### 说明 从上图中我们可以看到 Formily 内核其实是一个 @formily/reactive 领域模型。 实际消费领域模型则主要是依赖 @formily/reactive 的 响应器 机制做依赖追踪来消费。 我们可以在响应器(Reactions)中消费 Form/Field/ArrayField/ObjectField/VoidField 模型中的任意属性,依赖的属性发生变化,响应器就会重复执行。 从而实现了表单层面的 Reactive 编程模型。 #### MVVM OOP 架构 MVVM(Model–view–viewmodel)是一种 OOP 软件架构模式,它的核心是将我们的应用程序的逻辑与视图做分离,提升代码可维护性与应用健壮性。我们可以用一张图来描述: ![架构图](./public/formily_core.svg) 解释一下就是,View(视图层)负责维护 UI 结构与样式,同时负责与 ViewModel(视图模型)做数据绑定,这里的数据绑定关系是双向的,也就是,ViewModel(视图模型)的数据发生变化,会触发 View(视图层)的更新,同时视图层的数据变化又会触发 ViewModel(视图模型)的变化。Model 则更偏实际业务数据处理模型。ViewModel 和 Model 都是充血模型,两者都注入了不同领域的业务逻辑,比如 ViewModel 的业务逻辑更偏视图交互层的领域逻辑,而 Model 的业务逻辑则更偏业务数据的处理逻辑。 那么,Formily 解决方案在 MVVM 中应该是什么样的定位呢? 很明显,Formily 它提供了 View 和 ViewModel 两层能力,View 则是@formily/react @formily/vue,专门用来与@formily/core 做桥接通讯的,所以,@formily/core 的定位就是 ViewModel 层, 那 Model 层在哪里呢? 当然就是我们的实际业务代码层了,这一层 formily 就不会管了,所以这一层,用户到底是用 OOP 模式维护了一个 Model 还是用 FP 模式维护了一系列的业务逻辑函数集,formily 都不关心。 所以,这也使得 formily 对业务的入侵性很低,因为 formily 的目标是减少用户设计 ViewModel 的成本,让用户更加专注于业务逻辑的实现。 FP 架构 还记得之前 React 团队用了一个最简单的表达式 UI = fn(State) 来表达整个 React 体系吗?这样的函数式表达 UI,非常简单清晰,那会不会和 MVVM 模式产生冲突呢? 并不会冲突,因为在 MVVM 的模式中,View 和 ViewModel 的关系其实就约等于 UI = fn(State) ,因为 ViewModel 是一个注入逻辑的充血模型,它与 fn(State) 都能达到相同的目标,只是它是更 OOP 的表达,只是 fn(State) 是一种更加函数式的表达,将状态作为贫血模型而存在,通过一个又一个的函数,对贫血模型做 Immutable 式的更新,最终反应到 UI 上。 所以,从逻辑和数据分离的角度上来看,函数式表达更加清晰,只是函数式表达要求所有数据都是 Immutable 的。所以在性能要求高的场景上,采用函数式模型收益并不会太大,当然只是在 js 语言下是这样的。相反,MVVM 这种模式对数据的要求更多的是 Reactive 数据,也就是可以通过引用式操作数据的响应式数据模型,这样可以做到精确监控数据变化,最终反应到 UI 上。 所以,在表单场景上,MVVM 模式性能优势会更好一些,最重要的是,目前大多数存活了几十年的 GUI 产品,几乎都是不约而同的使用 MVVM,这么看来,在前端领域,函数式体系会更偏学术化一些,从实际对业务的收益来看的话,MVVM 还是首选。 ### 表单模型 前面讲到了 Formily 内核的整体架构,同时也讲了 MVVM,你应该也能大致能理解什么是 Formily 的表单模型了,下面我们再深一步来讲表单模型的具体领域逻辑,主要是偏思路性的总结性内容,如果第一遍看不懂,可以先直接去看 API 文档,看完之后再回来看,就能加深对 formily 的理解了。 #### 梳理 整个表单模型很大很复杂,分解下来其实核心是以下几个子模型: - 字段管理模型 - 字段模型 - 数据模型 - 联动模型 - 路径系统 下面具体来讲一下表单模型是如何管理的。 字段管理模型 字段管理模型,主要包含: - 字段添加 - 字段查询 - 导入字段集 - 导出字段集 - 清空字段集 ##### 字段添加 主要通过 createField/createArrayField/createObjectField/createVoidField 方法来创建字段,如果字段已经存在,则不会重复创建 ##### 字段查询 主要通过 query 方法来查询字段,query 方法可以传入字段的路径或者正则表达式来匹配字段。 因为字段路径的详细规则还是比较复杂的,在后面的路径系统篇中会详细讲解。 然后调用 query 方法会返回一个 Query 对象,Query 对象中可以有批量遍历所有字段的 forEach/map/reduce 方法,也可以有只取查询到的第一个字段的 take 方法,同时还有直接读取字段属性的 get 方法,还有可以深层读取字段属性的 getIn 方法,两个方法的差别就是前者可以有智能提示,后者没有提示,所以推荐用户都用 get 方法。 ##### 导入字段集 主要通过 setFormGraph 来导入字段集,入参格式是一个扁平对象格式,key 是字段的绝对路径,value 是字段的状态,使用该 API 主要在一些需要做时间旅行的场景,将 Immutable 字段状态导入至表单模型中。 ##### 导出字段集 主要通过 getFormGraph 来导出字段集,导出格式是一个扁平对象格式,key 是字段的绝对路径,value 是字段的状态,与导入字段集入参一致,因为返回的数据是一个 Immutable 的数据,所以是可以完全做持久化存储的,方便时间旅行。 ##### 清空字段集 主要通过 clearFormGraph 来清空字段集。 #### 字段模型 字段模型主要包含了: - Field 模型,主要负责管理非自增型字段状态,比如 Input/Select/NumberPicker/DatePicker 这些组件 - ArrayField 模型,主要负责管理自增列表字段状态,可以对列表项进行增删移动的。 - ObjectField 模型,主要负责管理自增对象字段状态,可以对对象的 key 做增删操作。 - VoidField 模型,主要负责管理虚字段状态,虚字段是一种不会污染表单数据的节点存在,但是它可以控制它的子节点显示隐藏,交互模式。 因为字段模型非常复杂,所以会在后面的字段模型篇中详细讲解。 #### 数据模型 表单数据模型,formily 之前的版本或多或少都会存在一些边界问题,在 2.x 中重新梳理了一版,才真正把之前的遗留问题突破掉了。 数据模型主要包含: - 表单值(values)管理 - 表单默认值(initialValues)管理 - 字段值(value)管理 - 字段默认值(initialValue)管理 - 值与默认值的选择合并策略 表单值管理,其实就是一个对象结构的 values 属性,只是它是一个 @formily/reactive observable 属性,同时借助了 @formily/reactive 的深度 observer 能力,监听了它任意属性变化,如果发生变化,便会触发 onFormValuesChange 的生命周期钩子。 同理,默认值管理其实也是一个对象结构的 initialValues 属性,同样会深度监听属性变化,触发 onFormInitialValues 的生命周期钩子。 字段值管理,是在每个数据型字段的 value 属性上体现的,formily 会给每个字段维护一个叫 path 的数据路径属性,然后 value 的读写,都是对顶层表单的 values 进行读写,这样保证了字段的值与表单的值是绝对幂等的,同理字段默认值也一样。 总结一下,**值的管理,都是在顶层表单上管理的,字段的值与表单的值是通过 path 来实现的绝对幂等**。 值与默认值的差别其实就在于表单重置的时候,字段是否会重置为默认值状态 值与默认值的选择合并策略 平时我们在业务开发的过程中,总会有数据回显的需求,这份数据一般都是作为异步默认值,作为详情页面的话,都还好,但是作为编辑页面的话,就会存在一些问题了: **存在冲突** 比如表单值为{xx:123},表单默认值为{xx:321},这里的策略是: - 如果 xx 没有相应的字段模型,代表仅仅只是冗余数据,用户无法修改 如果表单值是先赋值,默认值是后赋值的,那么默认值直接覆盖表单值,这种场景适用于异步数据回显场景,不同业务状态,回显的默认数据不一样,最终提交数据{xx:321} 如果默认值先赋值,表单值是后赋值的,那么表单值直接覆盖默认值,这种场景适用于同步默认值,最终提交数据{xx:123} - 如果 xx 有字段模型 如果表单值先赋值,默认值是后赋值的 如果当前字段被用户修改过(modified 为 true),那么默认值不能覆盖表单值,最终提交数据{xx:123} 如果当前字段没有被用户修改过(modified 为 false),那么默认值会直接覆盖字段值,这种场景适用于异步数据回显场景,不同业务状态,回显的默认数据不一样,最终提交数据{xx:321} 如果默认值先赋值,表单值是后赋值的,那么表单值直接覆盖默认值,这种场景适用于同步默认值,最终提交数据{xx:123} **不存在冲突** 比如表单值为{xx:123},表单默认值为{yy:321},这里的策略是直接合并。 总结一下,值与默认值的选择合并策略,**核心是看该字段是否被用户修改过,一切以用户为准,如果没被用户修改过就以赋值顺序为准** 这里提到的默认值,是可以重复赋值的,说的也是在重复赋值的过程中,要不要舍弃值的问题。 #### 校验模型 表单校验模型核心是对数据的合法性校验,然后将校验结果管理起来,所以校验模型主要包含了: - 校验规则管理 - 校验结果管理 因为校验模型隶属于字段模型,所以会在后面的字段模型篇中详细讲解 #### 联动模型 联动模型在 formily1.x 中核心是走的主动式联动模型,大致用一句表达式来表达就是: setFieldState(Subscribe(FormLifeCycle, Selector(Path)), TargetState) 解释下就是,任意一次联动,都是基于表单的某个生命周期钩子去触发指定路径下字段的状态,这样的模型能解决很多问题,但是它也有个很明显的问题,就是在多对一联动的场景下,需要同时监听多个字段变化去控制某个字段的状态,这样对用户而言,实现成本还是比较高的,特别是实现一些计算器联动需求,代码量剧增。当然,对于一对多场景,反而这种模型又是最高效的。 所以,在 formily2.x 中,在主动联动模型上新增了被动联动模型,同样是一句表达式表达: subscribe(Dependencies, Reactions) 简化了很多,核心就是针对依赖数据变化做响应,依赖的数据可以是表单模型属性,也可以是任意字段模型的属性,响应的动作可以是改任意字段模型的属性,也可以是做其他异步动作。这样的模型同样是一个完备的联动模型,只是在一对多场景下,比起主动模型而言,实现成本会比较高。 所以,两种联动模型,需要用户根据自身需求来选择。 #### 路径系统 路径系统,非常重要,几乎整个表单模型处处都有用到路径系统,它的主要给表单模型提供了以下几个能力: 它可以用来从字段集中查找任意一个字段,同时支持按照规则批量查找 它可以用来表达字段间关系的模型,借助路径系统,我们可以实现查找某个字段父亲,能查找父亲,也就能实现树级别的数据继承能力,同样,我们也能查找某个字段的相邻节点 它可以用来实现字段数据的读写,带解构的数据读写 整个路径系统,其实是基于@formily/path 的路径 DSL 来实现的,想要了解更多路径系统的内容,可以详细看看 FormPath API 篇 ### 字段模型 Formily 的字段模型核心包含了两类字段模型: - 数据型字段 - 虚数据型字段 数据型字段(Field),核心是负责维护表单数据(表单提交时候的值)。 虚数据型字段(VoidField),你可以理解为它就是一个阉割了数据维护能力的 Field,所以它更多的是作为容器维护一批字段的 UI 形式。 下面我们具体分析这两种类型字段。 #### 数据型字段 在 字段模型 中有 3 种数据型字段: - Field - ArrayField - ObjectField ArrayField 和 ObjectField 都是继承自 Field,Field 的定位就是维护非自增型数据字段,对比 ArrayField/Object,并不是说 Field 就不能存数组类型或者对象类型的数据,Field 其实可以存任意数据类型的数据,只是,如果用户期望实现数组的添加删除移动这样的交互,则需要使用 ArrayField,对象属性的添加删除交互,则需要使用 ObjectField,如果没有这样的需求,所有数据类型统一用 Field 即可。 然后咱们再看具体 Field 领域规则: - 路径规则 - 显隐规则 - 数据读写规则 - 数据源规则 - 字段组件规则 - 字段装饰器规则 - 校验规则 #### 路径规则 因为我们实际业务的表单结构本身就是一个树结构,所以在 Formily 中,每个字段在表单模型中都会有一个绝对路径,这个绝对路径大致描述了字段在表单数据中的位置(为什么用大致,后面会讲),通过绝对路径可以找到任意一个字段,同时还能表达字段间的父子关系,所以字段模型中,我们定义了 address 属性来表达字段的绝对路径,主要用点语法来描述,比如 a.b.c 这样的路径代表了字段 c 的父亲是字段 b,字段 b 的父亲是 a。 当然,事情并没有这么简单,因为我们还有 VoidField,VoidField 作为虚数据字段,它同样也有自己的绝对路径,因为它可以作为数据字段的父亲,如果我们只有绝对路径, 就无法让一个数据字段正确的往表单数据里写入字段数据。读取数据也会读错位置。 所以,我们其实还需要一个数据路径作为专门用于数据字段写入数据和读取数据的,这里我们用 path 来描述字段的数据路径,大概的规则可以看看这张图: ![路径模型](./public/path_model.png) 总结下来就是,Address 永远是代表节点的绝对路径,Path 是会跳过 VoidField 的节点路径,但是如果是 VoidField 的 Path,是会保留它自身的路径位置。 所以,不管是 Field 还是 VoidField,都会有它的 Address 和 Path,所以我们在用 query 方法查询字段的时候,既可以用 Address 规则查询,也可以用 Path 规则查询,比如 query("b.c")可以查询到 c 字段,同样用 query("a.b.c")也能查询到 c 字段。 #### 显隐规则 字段的显示隐藏,主要用 display 属性来表达: - display 为 none 代表字段 UI 隐藏,同时不保留字段数据 - display 为 hidden 代表字段 UI 隐藏,保留字段数据 - display 为 visible 代表字段 UI 显示,同时恢复字段数据 在 display 属性之上,我们还提供了两个便捷属性 1. visible,如果为 true 代表 display 等于 visible,如果为 false 代表 display 等于 none 2. hidden,如果为 true 代表 display 等于 hidden,如果为 false 代表 display 等于 visible 上面讲的是显隐属性的写规则,读取规则就会更复杂一些,这里有一个默认继承逻辑: 如果父节点主动设置了 display 属性,子节点没有主动设置 display 属性,那么子节点会继承父节点的 display 那什么才是主动设置 display 呢?主要包括: - 给字段配置了初始化属性 display/visible/hidden - 如果初始化时没有配置,但是在后期又给字段设置了 display/visible/hidden 那如果希望从不继承变为继承怎么办?把 display 设置为 null 即可。 #### 数据读写规则 因为 Field 是数据型字段,它负责维护表单数据的某个节点的数据,这里的读取,其实是直接读取的表单数据,通过 path 属性来寻址,这样也保证了表单数据与字段数据的绝对幂等,读取的方式直接读取 value/initialValue 即可。 数据写入规则与读取规则一致,Field 不会独立维护一份数据,它操作的直接就是具体表单的数据,通过 path 属性来寻址,写入的方式主要有: - 直接修改 value/initialValue 属性 - 调用 onInput 会写入数据,同时设置字段的 inputValue 为入参数据,inputValues 为多参数据,然后设置 modified 属性为 true,代表该字段被手动修改过,最后触发 triggerType 为 onInput 的校验规则 - 调用 setValue 方法 #### 数据源规则 考虑到字段的值来源不是只有通过 Input 输入框输入的,还有会从一个数据源中选取的,比如下拉框之类的,所以字段模型加了一个数据源的属性 dataSource,专门用于读取数据源。只是在组件消费端需要做一层映射。写入数据源的方式可以直接修改 dataSource 属性,也可以调用 setDataSource 方法 #### 组件规则 字段模型,如果没有代理 UI 组件信息,那就没法实现更加精细化的联动控制了,比如 A 字段的值变化要控制 B 字段的 placeholder,那就必须将字段的属性给代理起来,所以 formily 提供了 component 属性,专门用于代理 UI 组件信息,component 是一个数组[Component,ComponentProps],第一个元素代表是哪个组件,第二个代表组件的属性有哪些,为什么用数组,主要原因是这样方便类型提示,同时写法也比较简单。 读取组件信息的方式直接读取 component 属性即可。 写入组件信息的方式主要有: - 直接修改 component 属性,传入数组 - 调用 setComponent 方法,第一个参数是组件,第二个是组件属性 - 调用 setComponentProps 方法,直接会设置组件属性 #### 装饰器规则 与字段组件规则相似,字段装饰器主要用来维护字段的包裹容器,比如 FormItem,更偏 UI 布局的控制,这里我们用 decorator 属性来描述字段装饰器。 读取装饰器信息的方式直接读取 decorator 属性即可。 写入装饰器信息的方式主要有: - 直接修改 decorator 属性,传入数组 - 调用 setDecorator 方法,第一个参数是组件,第二个是组件属性 - 调用 setDecoratorProps 方法,直接会设置组件属性 #### 校验规则 校验规则主要包含: - 校验器 - 校验时机 - 校验策略 - 校验结果 ##### 校验器 在字段模型中的校验器主要用 validator 属性描述,在字段初始化的时候可以给字段传入 validator,初始化之后也可以再次修改 validator 一个 validator 主要有以下几种形态: - 纯字符串格式校验,比如"phone" | validator = "url" | validator= "email" ,这样的格式校验是正则规则的简写形式,formily 内部提供了一些标准的正则规则,当然用户也能通过 registerValidateFormats 来手动创建规则,方便复用 - 自定义函数校验,有 3 种返回值模式: - (value)=>"message",返回字符串代表有错误,不返回字符串代表无错误 - (value)=>({type:"error",message:"message"}),返回对象形式,可以指定 type 是 error 或 warning 或 success - {validator:()=>false,message:"message"},返回布尔形式,错误消息会复用对象结构的 message 字段 - 对象结构校验,是一种更完备的表达,比如: - {format:"url"} 这样可以指定正则格式 - {required:true}这样可以指定必填 - 还有更多的规则属性可以参考 API 文档,同时我们还能通过 registerValidateRules 来注册类似的校验规则 - 对象数组结构校验,是前面三种的组合表达,其实前 3 种,都会转换成对象数组结构,比如: - ["url",{required:true},(value)=>"message"]其实相当于 [{format:"url"},{required:true},{validator:(value)=>"message"}] #### 校验时机 有些时候,我们希望某些校验规则只在聚焦或者失焦的时候触发,我们可以在每个校验规则对象中加一个 triggerType,比如{validator:(value)=>"message",triggerType:"onBlur"} 这样就可以精确的控制某个校验规则只在某个事件中执行校验,这里的 triggerType 主要有"onInput" | "onBlur" | "onFocus" ,如果调用 form.validate,是会一次性校验所有 triggerType 的规则,如果手动调用 field.validate,则可以在入参中指定 triggerType,不传参就会校验所有。 ##### 校验策略 有些时候,我们希望某个字段的校验策略是,执行所有校验规则的时候,如果某个校验规则校验失败则立即返回结果,我们只需要在 field 初始化的时候传入参数 validateFirst 为 true 即可,默认是 false,也就是校验失败也会继续校验,拿到的校验结果是一个数组。 ##### 校验结果读取 对于校验结果,在字段模型中主要是存放在 feedbacks 属性中的,feedbacks 是由 Feedback 对象组成的数组,每个 Feedback 的结构是: ```javascript interface Feedback { path: string //字段数据路径 address: string //字段绝对路径 type: 'error' | 'success' | 'warning' //校验结果类型 code: //校验结果编码 | 'ValidateError' | 'ValidateSuccess' | 'ValidateWarning' | 'EffectError' | 'EffectSuccess' | 'EffectWarning' messages: string[] //校验消息 } ``` 读取方式主要有 4 种: - 直接读取 feedbacks 属性 - 读取 errors 属性,相当于是从 feedbacks 中过滤出 type 为 error 的所有校验结果 - 读取 warnings 属性,相当于是从 feedbacks 中过滤出 type 为 warning 的所有校验结果 - 读取 successes 属性,相当于是从 feedbacks 中过滤出 type 为 success 的所有校验结果 ##### 校验结果写入 写入方式有 3 种: - 调用 validate 方法,触发字段校验器执行校验动作,校验结果的 Code 统一是 Validate\*` - 调用 onInput 会触发 validate - 调用 onFocus 会触发 validate - 调用 onBlur 会触发 validate - 调用 reset,并指定 validate 为 true 会触发 validate - 直接修改 feedbacks 属性 - 直接修改 errors 属性,会转换成 feedbacks 对象数组,同时 Feedback 的 code 会被强制覆盖为 EffectError - 直接修改 warnings 属性,会转换成 feedbacks 对象数组,同时 Feedback 的 code 会被强制覆盖为 EffectWarning - 直接修改 successes 属性,会转换成 feedbacks 对象数组,同时 Feedback 的 code 会被强制覆盖为 EffectSuccess 这样的写入逻辑主要是为了防止用户修改校验结果污染本身校验器的校验结果,做严格分离,容易恢复现场。 ##### 校验结果查询 校验结果的查询主要通过 queryFeedbacks 来查询,查询的入参与 Feedback 对象一致,可以按照 type 或者 code,也可以按照路径进行过滤。 #### ArrayField ArrayField 相比于 Field,仅仅只是在继承 Field 的基础上扩展了数组相关的方法,比如 push/pop/insert/move 这些,为什么要提供这些方法,它的能力不只是对字段的数据做处理,它内部还提供了对 ArrayField 子节点的状态转置处理主要为了保证字段的顺序与数据的顺序是一致。可以举个例子: ![状态转置](./public/field_array.png) 这是一个 move 调用的过程,数组元素的值会发生移动,同时对应字段的状态也会发生移动。 #### ObjectField 因为 object 类型是无序的,也就不存在状态转置,所以 ObjectField 就提供了 addProperty/removeProperty/existProperty 3 个 API 给用户使用。 #### VoidField VoidField 相比于 Field,主要是阉割了数据读写规则、数据源规则和校验规则,用户使用的时候,主要还是使用显隐规则和组件,装饰器规则。 `前面讲的一系列字段领域规则,并没有提到详细的 API 使用细节,更多的是从思路上帮助用户梳理 formily,如果对 API 不熟悉的,最好先看 API 文档。` #### Formily Core API ```javascript interface createForm { (props: IFormProps): Form; } ``` 函数没有 key,所以 createForm 直接就是一个函数,传入 IFormProps 类型的 props,返回一个 Form 类型的结果。 ##### Form effect hooks ```javascript import React, { useMemo, useState } from "react"; import { createForm, onFormInit } from "@formily/core"; import { ActionResponse } from "./ActionResponse"; export default () => { const [response, setResponse] = useState(""); useMemo( () => createForm({ effects() { onFormInit(() => { setResponse("表单已初始化"); }); }, }), [] ); return ; }; ``` #### Field effect hooks ```javascript import React, { useMemo, useState } from "react"; import { createForm, onFieldInit } from "@formily/core"; import { ActionResponse } from "./ActionResponse"; export default () => { const [response, setResponse] = useState(""); const form = useMemo( () => createForm({ effects() { onFieldInit("target", () => { setResponse("target已初始化"); }); }, }), [] ); return ( ); }; ``` #### Form hooks API ```javascript interface createEffectHook { ( type: string, callback?: ( payload: any, form: Form, ...ctx: any[] //用户注入的上下文 ) => (...args: any[]) => void //高阶回调用于处理监听器的封装,帮助用户实现参数定制能力 ) } interface createEffectContext { (defaultValue: T): { provide(value: T): void consume(): T } } interface useEffectForm { (): Form } ``` #### Form Checkers - isForm - isField #### FormPath ```javascript class FormPath { constructor(pattern: FormPathPattern, base?: FormPathPattern) } type FormPathPattern = string | number | Array | RegExp ``` 使用样例 ```javascript import { FormPath } from "@formily/core"; const target = {}; FormPath.setIn(target, "a.b.c", "value"); console.log(FormPath.getIn(target, "a.b.c")); //'value' console.log(target); //{a:{b:{c:'value'}}} ``` ```javascript import { FormPath } from "@formily/core"; const target = { array: [], }; FormPath.setIn(target, "array.0.aa", "000"); console.log(FormPath.getIn(target, "array.0.aa")); //000 console.log(target); //{array:[{aa:'000'}]} FormPath.setIn(target, "array[1].aa", "111"); console.log(FormPath.getIn(target, "array.1.aa")); //111 console.log(target); //{array:[{aa:'000'},{aa:'111'}]} ``` #### Form Validator Registry - setValidateLanguage - registerValidateFormats - registerValidateLocale - registerValidateMessageTemplateEngine - registerValidateRules - getValidateLocaleIOSCode ### Models 实例属性 - Form - Field - ArrayField - ObjectField - VoidField - Query --- ## 加入 React 支持 除了安装 react, @types/react, react-dom, @types/react-dom, @vitejs/plugin-react 几个包外,修改 tsconfig.json 内容。 ```json { "compilerOptions": { ... "jsx": "react-jsx" // 或者 "preserve" ... } } ``` 以及 vite.config.ts 文件中配置 vitejs 插件。**一定注意要将 main.ts 改成 main.tsx 以支持 jsx 语法** --- ## Formily React ### 介绍 @formily/react 的核心定位是将 ViewModel(@formily/core)与组件实现一个状态绑定关系,它不负责管理表单数据,表单校验,它仅仅是一个渲染胶水层,但是这样一层胶水,并不脏,它会把很多脏逻辑优雅的解耦,变得可维护。 ### 核心架构 @formily/react 的架构相比于@formily/core 并不复杂,先看架构图: ![Formily React核心框架](./public/formily_react_architecture.svg) 从这张架构图中我们可以看到,@formily/react 支持了两类用户,一类就是纯源码开发用户,他们只需要使用 Field/ArrayField/ObjectField/VoidField 组件。另一类就是基于 JSON-Schema 做动态开发的用户,他们依赖的主要是 SchemaField 组件,但是,这两类用户都需要使用一个 FormProvider 的组件来统一下发上下文。然后是 SchemaField 组件,它内部其实是依赖的 Field/ArrayField/ObjectField/VoidField 组件。 ### 核心概念 @formily/react 本身架构不复杂,因为它只是提供了一系列的组件和 Hooks 给用户使用,但是我们还是需要理解以下几个概念: - 表单上下文 - 字段上下文 - 协议上下文 - 模型绑定 - 协议驱动 - 三种开发模式 #### 表单上下文 从架构图中我们可以看到 FormProvider 是作为表单统一上下文而存在,它的地位非常重要,主要用于将@formily/core 创建出来的 Form 实例下发到所有子组件中,不管是在内置组件还是用户扩展的组件,都能通过 useForm 读取到 Form 实例 #### 字段上下文 从架构图中我们可以看到不管是 Field/ArrayField/ObjectField/VoidField,会给子树下发一个 FieldContext,我们可以在自定义组件中读取到当前字段模型,主要是使用 useField 来读取,这样非常方便于做模型映射 #### 协议上下文 从架构图中我们可以看到 RecursionField 会给子树下发一个 FieldSchemaContext,我们可以在自定义组件中读取到当前字段的 Schema 描述,主要是使用 useFieldSchema 来读取。注意,该 Hook 只能用在 SchemaField 和 RecursionField 子树中使用 #### 模型绑定 想要理解模型绑定,需要先理解什么是 MVVM,理解了之后我们再看看这张图: ![Formily React核心框架](./public/formily_react_mvvm.svg) 在 Formily 中,@formily/core 就是 ViewModel,Component 和 Decorator 就是 View,@formily/react 就是将 ViewModel 和 View 绑定起来的胶水层,**ViewModel 和 View 的绑定就叫做模型绑定,实现模型绑定的手段主要有 useField,也能使用 connect 和 mapProps,需要注意的是,Component 只需要支持 value/onChange 属性即可自动实现数据层的双向绑定。** #### 协议驱动 协议驱动渲染算是@formily/react 中学习成本最高的部分了,但是学会了之后,它给业务带来的收益也是很高,总共需要理解 4 个核心概念: - Schema - 递归渲染 - 协议绑定 - 三种开发模式 ##### Schema formily 的协议驱动主要是基于标准 JSON Schema 来进行驱动渲染的,同时我们在标准之上又扩展了一些 x-\*属性来表达 UI,使得整个协议可以具备完整描述一个复杂表单的能力,具体 Schema 协议,参考 Schema API 文档 ##### 递归渲染 何为递归渲染?递归渲染就是组件 A 在某些条件下会继续用组件 A 来渲染内容,看看以下伪代码: ```json { <---- RecursionField(条件:object;渲染权:RecursionField) "type":"object", "properties":{ "username":{ <---- RecursionField(条件:string;渲染权:RecursionField) "type":"string", "x-component":"Input" }, "phone":{ <---- RecursionField(条件:string;渲染权:RecursionField) "type":"string", "x-component":"Input", "x-validator":"phone" }, "email":{ <---- RecursionField(条件:string;渲染权:RecursionField) "type":"string", "x-component":"Input", "x-validator":"email" }, "contacts":{ <---- RecursionField(条件:array;渲染权:RecursionField) "type":"array", "x-component":"ArrayTable", "items":{ <---- RecursionField(条件:object;渲染权:ArrayTable 组件) "type":"object", "properties":{ "username":{ <---- RecursionField(条件:string;渲染权:RecursionField) "type":"string", "x-component":"Input" }, "phone":{ <---- RecursionField(条件:string;渲染权:RecursionField) "type":"string", "x-component":"Input", "x-validator":"phone" }, "email":{ <---- RecursionField(条件:string;渲染权:RecursionField) "type":"string", "x-component":"Input", "x-validator":"email" }, } } } } } ``` @formily/react 递归渲染的入口是 SchemaField,但它内部实际是使用 RecursionField 来渲染的,因为 JSON-Schema 就是一个递归型结构,所以 RecursionField 在渲染的时候会从顶层 Schema 节点解析,如果是非 object 和 array 类型则直接渲染具体组件,如果是 object,则会遍历 properties 继续用 RecursionField 渲染子级 Schema 节点。 这里有点特殊的情况是 array 类型的自增列表渲染,需要用户在自定义组件内使用 RecursionField 进行递归渲染,因为自增列表的 UI 个性化定制程度很高,所以就把递归渲染权交给用户来渲染了,这样设计也能让协议驱动渲染变得更加灵活。 那 SchemaField 和 RecursionField 有啥差别呢?主要有两点: - SchemaField 是支持 Markup 语法的,它会提前解析 Markup 语法生成 JSON Schema 移交给 RecursionField 渲染,所以 RecursionField 只能基于 JSON Schema 渲染 - SchemaField 渲染的是整体的 Schema 协议,而 RecursionField 渲染的是局部 Schema 协议 #### 协议绑定 前面讲了模型绑定,而协议绑定则是将 Schema 协议转换成模型绑定的过程,因为 JSON-Schema 协议是 JSON 字符串,可离线存储的,而模型绑定则是内存间的绑定关系,是 Runtime 层的,比如 x-component 在 Schema 中是组件的字符串标识,但是在模型中的 component 则是需要组件引用,所以 JSON 字符串与 Runtime 层是需要转换的。然后我们就可以继续完善一下以上模型绑定的图: ![模型绑定图](./public/formily_react_jsonschema_bind.svg) 总结下来,在@formily/react 中,主要有 2 层绑定关系,Schema 绑定模型,模型绑定组件,实现绑定的胶水层就是@formily/react,需要注意的是,Schema 绑定字段模型之后,字段模型中是感知不到 Schema 的,比如要修改 enum,就是修改字段模型中的 dataSource 属性了,总之,想要更新字段模型,参考 Field,想要理解 Schema 与字段模型的映射关系可以参考 Schema 文档 三种开发模式 从架构图中我们其实已经看到整个@formily/react 是有三种开发模式的,对应不同用户: - JSX 开发模式 - JSON Schema 开发模式 - Markup Schema 开发模式 我们可以看看具体例子 - JSX 开发模式 该模式主要是使用 Field/ArrayField/ObjectField/VoidField 组件 ```tsx import React from "react"; import { createForm } from "@formily/core"; import { FormProvider, Field } from "@formily/react"; import { Input } from "antd"; const form = createForm(); export default () => ( ); ``` - JSON Schema 开发模式 该模式是给 SchemaField 的 schema 属性传递 JSON Schema 即可 ```tsx import React from "react"; import { createForm } from "@formily/core"; import { FormProvider, createSchemaField } from "@formily/react"; import { Input } from "antd"; const form = createForm(); const SchemaField = createSchemaField({ components: { Input, }, }); export default () => ( ); ``` - Markup Schema 开发模式 该模式算是一个对源码开发比较友好的 Schema 开发模式,同样是使用 SchemaField 组件。 因为用 JSON Schema 在 JSX 环境下很难得到最好的智能提示体验,而且也不方便维护,用标签的形式可维护性会更好,智能提示也很强。 Markup Schema 模式主要有以下几个特点: - 主要依赖 SchemaField.String/SchemaField.Array/SchemaField.Object...这类描述标签来表达 Schema - 每个描述标签都代表一个 Schema 节点,与 JSON-Schema 等价 - SchemaField 子节点不能随意插 UI 元素,因为 SchemaField 只会解析子节点的所有 Schema 描述标签,然后转换成 JSON Schema,最终交给 RecursionField 渲染,如果想要插入 UI 元素,可以在 VoidField 上传 x-content 属性来插入 UI 元素 ```tsx import React from "react"; import { createForm } from "@formily/core"; import { FormProvider, createSchemaField } from "@formily/react"; import { Input } from "antd"; const form = createForm(); const SchemaField = createSchemaField({ components: { Input, }, }); export default () => (
我不会被渲染
我会被渲染} />
); ``` ### Formily React API - Components - Field - ArrayField - ObjectField - VoidField - SchemaField - RecursionField - FormProvider - FormConsumer - ExpressionScope - RecordScope - RecordsScope - Hooks - useExpressionScope - useField - useFieldSchema - useForm - useFormEffects - useParentForm - Shared - connect - context - mapProps - mapReadPretty - observer - Schema ** Schema** 描述 @formily/react 协议驱动最核心的部分,Schema 在其中是一个通用 Class,用户可以自行使用,同时在 SchemaField 和 RecursionField 中都有依赖它,它主要有几个核心能力: - 解析 json-schema 的能力 - 将 json-schema 转换成 Field Model 的能力 - 编译 json-schema 表达式的能力 从@formily/react 中可以导出 Schema 这个 Class,但是如果你不希望使用@formily/react,你可以单独依赖@formily/json-schema 这个包