vue + formily
组件作为 Field 的props使用时,不需要调用
observer或useObservervue<VoidField title="SetupJSx" name="SetupJSx" :component="[SetupJSx]" :decorator="[FormItem]" />@formily/reactive-vue.observer+ 【选项式api setup jsx】 + useForm 读form数据jsxobserver(defineComponent({ setup() { const formRef = useForm() return () => { const formVal = JSON.stringify(formRef.value?.values ?? null, null, 2) return ( <pre>{ formVal }</pre>) } }, }))sfc + 组合式api
sfc + 选项式api
概览

依赖
- @formily/core
- @formily/reactive
- @formily/vue
依赖追踪机制和响应式模型
- @formily/reactive 延续了 Mobx 的核心思想
- 借助 @formily/reactive,@formily/core 天然获得了依赖追踪,高效更新,按需渲染的能力
JSON Schema 独立存在,给 UI 桥接层消费
import { useObserver } from '@formily/reactive-vue'
// vue sfc 组合式api + fomilyjsMVVM

- View(视图层)负责维护 UI 结构与样式,同时负责与 ViewModel(视图模型)做数据绑定
- 这里的数据绑定关系是双向的,也就是,ViewModel(视图模型)的数据发生变化,会触发 View(视图层)的更新,同时视图层的数据变化又会触发 ViewModel(视图模型)的变化。
- Model 则更偏实际业务数据处理模型。ViewModel 和 Model 都是充血模型,两者都注入了不同领域的业务逻辑,比如 ViewModel 的业务逻辑更偏视图交互层的领域逻辑,而 Model 的业务逻辑则更偏业务数据的处理逻辑
- Formily 它提供了 View 和 ViewModel 两层能力,View 则是@formily/react @formily/vue,专门用来与@formily/core 做桥接通讯的,
- 在 Formily 中,@formily/core 就是 ViewModel,Component 和 Decorator 就是 View,@formily/vue 就是将 ViewModel 和 View 绑定起来的胶水层,ViewModel 和 View 的绑定就叫做模型绑定
模型
如何描述一个字段
- 路径
- 状态属性(visible、disabled)
- 值
- UI组件
- UI 组件的属性
值的管理,都是在顶层表单上管理的,字段的值与表单的值是通过 path 来实现的绝对幂等。
表单值管理,其实就是一个对象结构的 values 属性,只是它是一个 @formily/reactive observable 属性,同时借助了 @formily/reactive 的深度 observer 能力,监听了它任意属性变化,如果发生变化,便会触发 onFormValuesChange 的生命周期钩子。
字段值管理,是在每个数据型字段的 value 属性上体现的,formily 会给每个字段维护一个叫 path 的数据路径属性,然后 value 的读写,都是对顶层表单的 values 进行读写,这样保证了字段的值与表单的值是绝对幂等的,同理字段默认值也一样。
字段模型
Formily 的字段模型核心包含了两类字段模型:
- 数据型字段 Field、ArrayField、ObjectField
- 虚数据型字段 VoidField
Field 的属性(也就是 Field.props)IFieldFactoryProps, @formily/vue.mapProps 的功能就是将 Field.props 映射成自定义组件的 props
mapProps(
{
title: 'label',
description: 'extra',
required: true,
validateStatus: true,
},
(props, field) => {
return {
...props,
help: field.selfErrors?.length ? field.selfErrors : undefined,
}
}
)ArrayField 和 ObjectField 都是继承自 Field,Field 的定位就是维护非自增型数据字段,对比 ArrayField/Object,并不是说 Field 就不能存数组类型或者对象类型的数据,Field 其实可以存任意数据类型的数据
只是,如果用户期望实现数组的添加删除移动这样的交互,则需要使用 ArrayField,对象属性的添加删除交互,则需要使用 ObjectField,如果没有这样的需求,所有数据类型统一用 Field 即可。
数据读写规则
因为 Field 是数据型字段,它负责维护表单数据的某个节点的数据,这里的读取,其实是直接读取的表单数据,通过 path 属性来寻址,这样也保证了表单数据与字段数据的绝对幂等,读取的方式直接读取 value/initialValue 即可。
数据写入规则与读取规则一致,Field 不会独立维护一份数据,它操作的直接就是具体表单的数据,通过 path 属性来寻址,写入的方式主要有:
- 直接修改 value/initialValue 属性
- 调用 onInput 会写入数据,同时设置字段的 inputValue 为入参数据,inputValues 为多参数据,然后设置 modified 属性为 true,代表该字段被手动修改过,最后触发 triggerType 为 onInput 的校验规则
- 调用 setValue 方法
组件规则
formily 提供了 component 属性,专门用于代理 UI 组件信息,component 是一个数组[Component,ComponentProps],第一个元素代表是哪个组件,第二个代表组件的属性有哪些
读取组件信息的方式直接读取 component 属性即可。
写入组件信息的方式主要有:
- 直接修改 component 属性,传入数组
- 调用 setComponent 方法,第一个参数是组件,第二个是组件属性
- 调用 setComponentProps 方法,直接会设置组件属性
响应式
import { action, observable } from '@formily/reactive'
import { observer } from '@formily/reactive-react'
action.bound((data) => {
field.dataSource = transform(data)
field.loading = false
})
// 可订阅对象
const obs = observable({
input: '',
})一个 observable 对象,字面意思是可订阅对象,我们通过创建一个可订阅对象,在每次操作该对象的属性数据的过程中,会自动通知订阅者,@formily/reactive 创建 observable 对象主要是通过 ES Proxy 来创建的,它可以做到完美劫持数据操作
观察者和被观察者
- 观察者 observer:监听者,监听其他数据变化
- 被观察者(可观察到的)observable,也就是【依赖】
- 当【被观察者】变化时,观察者更新变化,observable 是依赖
当form值变化时,组件跟着更新,需要用 observer 包裹组件
联动
在 Formily2.x 中描述逻辑的方式有
- 纯 JSX 模式下的 effects 或 reactions 属性
- Schema 模式下的 effects 或结构化 x-reactions 属性
- Schema 模式下的 effects 或函数态 x-reactions 属性
我们可以在响应器(Reactions)中消费 Form/Field/ArrayField/ObjectField/VoidField 模型中的任意属性,依赖的属性发生变化,响应器就会重复执行。
**(1):**reactions 是用在具体字段属性上的响应器,它会基于函数内依赖的数据变化而重复执行
/* eslint-disable */
<Field
name="A"
reactions={(field) => {
/**具体逻辑实现**/
}}
/>**(2):**effects 是用于实现副作用隔离逻辑管理模型,它最大的优点就是在字段数量超多的场景下,可以让视图代码变得更易维护,同时它还有一个能力,就是可以批量化的对字段做处理。
- 比如我们在 A,B,C 字段属性显示声明 x-reactions,如果这 3 个字段的 x-reactions 逻辑都是一模一样的,那我们在 effects 中只需这么写即可:
onFieldReact('*(A,B,C)', (field) => {
//...逻辑
})**(3):**如果字段数量很少,逻辑相对简单的,直接在字段属性上写 reactions 也是不错的,清晰明了。
{
"x-reactions": {
"dependencies": ["aa"],
"fulfill": {
"state": {
"visible": "{{$deps[0] == '123'}}"
}
}
}
}联动方式
- x-reactions
- effects + onFieldValueChange
- 主动模式:effects、schema.target
- 被动模式:
@formily/core.onFieldReact、schema.dependencies- onFieldReact 的第一个参数是【观察者】
- onFieldValueChange 的第一个参数是【被观察者】
const form = createForm({
effects() {
onFieldValueChange('select', (field) => {
form.setFieldState('*(input1,input2)', (state) => {
//对于初始联动,如果字段找不到,setFieldState会将更新推入更新队列,直到字段出现再执行操作
state.display = field.value
})
})
},
})
field.setComponentProps({
style: {
backgroundColor: field.value,
},
})
x-reactions={[
{
target: 'count',
effects: ['onFieldInputValueChange'],
dependencies: ['price'],
fulfill: {
state: {
value: '{{$deps[0] ? $self.value / $deps[0] : $target.value}}',
},
},
},
{
target: 'price',
effects: ['onFieldInputValueChange'],
dependencies: ['count'],
fulfill: {
state: {
value: '{{$deps[0] ? $self.value / $deps[0] : $target.value}}',
},
},
},
]}
x-reactions={{
target: 'color',
fulfill: {
state: {
'component[1].style.backgroundColor': '{{$self.value}}',
},
//以下用法也可以
// schema: {
// 'x-component-props.style.backgroundColor': '{{$self.value}}',
// },
},
}}异步数据源
在 effects 中修改 Field 的 dataSource,也可以在 reactions 中修改 dataSource 属性。
import { createForm, onFieldReact, FormPathPattern, Field } from '@formily/core'
import { observable, action } from '@formily/reactive'
const useAsyncDataSource = (
pattern: FormPathPattern,
service: (field: Field) => Promise<{ label: string; value: any }[]>
) => {
onFieldReact(pattern, (field) => {
field.loading = true
service(field).then(
action.bound((data) => {
field.dataSource = data
field.loading = false
})
)
})
}
const form = createForm({
effects: () => {
useAsyncDataSource('select', async (field) => {
const linkage = field.query('linkage').get('value')
if (!linkage) return []
return new Promise((resolve) => {
setTimeout(() => {
if (linkage === 1) {
resolve([
])
} else if (linkage === 2) {
resolve([])
}
}, 1500)
})
})
},
})表单受控
Formily 就不再支持受控模式了,推荐的是使用@formily/reactive 实现响应式受控,既能实现值受控,也能实现字段级受控
自定义组件
const phoneSchema = {
type: 'object',
properties: {
phone: {
type: 'string',
title: '手机号',
'x-component': 'Input',
'x-validator': [{ required: true }, 'email'],
},
verifyCode: {
type: 'string',
title: '验证码',
'x-component': 'VerifyCode',
'x-reactions': [
{
dependencies: ['.phone#value', '.phone#valid'],
fulfill: {
state: {
// 组件 props
'component[1].readyPost': '{{$deps[0] && $deps[1]}}',
'component[1].phoneNumber': '{{$deps[0]}}',
selfErrors: '{{$deps[0] && $self.value && $self.value !== $deps[0] ? "确认密码不匹配" : ""}}',
},
},
},
],
},
},
}
// VerifyCode 组件 自定义组件
interface IVerifyCodeProps {
value?: any
onChange?: (value: any) => void
readyPost?: boolean
phoneNumber?: number
style?: React.CSSProperties
}
export const VerifyCode: React.FC<React.PropsWithChildren<IVerifyCodeProps>> =
({ value, onChange, readyPost, phoneNumber, ...props }) => {
const [lastTime, setLastTime] = useState(0)
// ....
}路径
field.query('.[].name').value()
$self.query(".name").value()- 数据路径 path
- 绝对路径 address
Address 永远是代表节点的绝对路径,Path 是会跳过 VoidField 的节点路径,但是如果是 VoidField 的 Path,是会保留它自身的路径位置。
用 query 方法查询字段的时候,既可以用 Address 规则查询,也可以用 Path 规则查询
校验
Formily 的表单校验使用了极其强大且灵活的@formily/validator 校验引擎,校验主要分两种场景:
- Markup(JSON) Schema 场景协议校验属性校验,使用 JSON Schema 本身的校验属性与 x-validator 属性实现校验
- 纯 JSX 场景校验属性,使用 validator 属性实现校验
同时我们还能在 effects 或者 x-reactions/reactions 中实现联动校验
具体规则校验文档参考 FieldValidator
validator_style_1: {
title: '局部定义风格',
required: true,
'x-validator': `{{(value)=> {
if (!value) return ''
return value !== '123' ? '错误了❎' : ''
}}}`,
'x-component': 'Input',
'x-decorator': 'FormItem',
},
'x-validator': {
pattern: /123/,
message: '错误了❎',
},
'x-reactions': `{{(field) => {
field.selfErrors =
field.query('bb').value() >= field.value ? 'AA必须大于BB' : ''
}}}`,协议
实现动态可配置表单
JSON-Schema + 扩展,UI 协议与数据协议混合在一起
- void 类型描述 UI 容器,void 代表一个虚数据节点,表示该节点并不占用实际数据结构
{
"type": "void",
"title": "卡片",
"description": "这是一个卡片",
"x-component": "Card",
"properties": {
"string": {
"type": "string",
"title": "字符串",
"description": "这是一个字符串",
"x-component": "Input",
"x-component-props": {
"placeholder": "请输入"
}
}
}
}