react-formily踩坑实记
本文使用的是 formily 2.* 版本
理论
官方文档
formily 主站:https://formilyjs.org/zh-CN
formily 核心库 core:https://core.formilyjs.org/zh-CN
formily 响应式状态管理方案 reactive:https://reactive.formilyjs.org/zh-CN
formily 组件库 react:https://react.formilyjs.org/zh-CN
本文主要基于 react 进行 formily 相关实战。
formily 也有 vue 相关组件库,具体请看 https://vue.formilyjs.org/
工作相关的参考文章
在查找一些解决方案时发现了宝藏同事总结的使用方法,遂记录一下
其他文章
官方提供了非常详细的一些业务场景的例子,可以在官方的 codesandbox 例子中进行实验
关键组件
FormProvider:入口组件,用于下发表单上下文给字段组件,负责整个表单状态的通讯,它相当于是一个通讯枢纽。挂载 Form 实例的组件。
SchemaField:SchemaField 组件是专门用于解析JSON-Schema动态渲染表单的组件。 在使用 SchemaField 组件的时候,需要通过 createSchemaField 工厂函数创建一个 SchemaField 组件。
import React from 'react'
import { createForm } from '@formily/core'
import { FormProvider, FormConsumer, Field } from '@formily/react'
import {
FormItem,
FormLayout,
Input,
FormButtonGroup,
Submit,
} from '@formily/antd'
const form = createForm()
export default () => {
return (
<FormProvider form={form}>
<FormLayout layout="vertical">
<Field
name="input"
title="输入框"
required
initialValue="Hello world"
decorator={[FormItem]}
component={[Input]}
/>
</FormLayout>
<FormConsumer>
{() => (
<div
style={{
marginBottom: 20,
padding: 5,
border: '1px dashed #666',
}}
>
实时响应:{form.values.input}
</div>
)}
</FormConsumer>
<FormButtonGroup>
<Submit onSubmit={console.log}>提交</Submit>
</FormButtonGroup>
</FormProvider>
)
}
从以上例子中,我们可以学到很多东西:
- createForm 用来创建表单核心领域模型,它是作为 MVVM 设计模式的标准 ViewModel
- FormProvider 组件是作为视图层桥接表单模型的入口,它只有一个参数,就是接收 createForm 创建出来的 Form 实例,并将 Form 实例以上下文形式传递到子组件中
- FormLayout 组件是用来批量控制 FormItem 样式的组件,这里我们指定布局为上下布局,也就是标签在上,组件在下
- Field 组件是用来承接普通字段的组件
- name 属性,标识字段在表单最终提交数据中的路径
- title 属性,标识字段的标题
- 如果 decorator 指定为 FormItem,那么在 FormItem 组件中会默认以接收 title 属性作为标签
- 如果指定为某个自定义组件,那么 title 的消费方则由自定义组件来承接
- 如果不指定 decorator,那么 title 则不会显示在 UI 上
- required 属性,必填校验的极简写法,标识该字段必填
- 如果 decorator 指定为 FormItem,那么会自动出现星号提示,同时校验失败也会有对应的状态反馈,这些都是 FormItem 内部做的默认处理
- 如果 decorator 指定为自定义组件,那么对应的 UI 样式则需要自定义组件实现方自己实现
- 如果不指定 decorator,那么 required 只是会阻塞提交,校验失败不会有任何 UI 反馈。
- initialValue 属性,代表字段的默认值
- decorator 属性,代表字段的 UI 装饰器,通常我们都会指定为 FormItem
- 注意 decorator 属性传递的是数组形式,第一个参数代表指定组件类型,第二个参数代表指定组件属性
- component 属性,代表字段的输入控件,可以是 Input,也可以是 Select,等等
- 注意 component 属性传递的是数组形式,第一个参数代表指定组件类型,第二个参数代表指定组件属性
- FormConsumer 组件是作为响应式模型的响应器而存在,它核心是一个 render props 模式,在作为 children 的回调函数中,会自动收集所有依赖,如果依赖发生变化,则会重新渲染,借助 FormConsumer 我们可以很方便的实现各种计算汇总的需求
- FormButtonGroup 组件作为表单按钮组容器而存在,主要负责按钮的布局
- Submit 组件作为表单提交的动作触发器而存在,其实我们也可以直接使用 form.submit 方法进行提交,但是使用 Submit 的好处是不需要每次都在 Button 组件上写 onClick 事件处理器,同时它还处理了 Form 的 loading 状态,如果 onSubmit 方法返回一个 Promise,且 Promise 正在 pending 状态,那么按钮会自动进入 loading 状态
实战
项目中主要使用 JSON Schema 的方式去创建表单
表单字段内容设置参考链接:
schema:https://react.formilyjs.org/zh-CN/api/shared/schema
schemaField:https://react.formilyjs.org/zh-CN/api/components/schema-field
IFieldFactoryProps:https://core.formilyjs.org/api/models/form#ifieldfactoryprops
select 组件使用异步 options 数据
使用 onFieldInit 进行设置
private baseInfoForm = createForm<BaseInfoDetail>({
initialValues: {
...DEFAULT_BASE_INFO,
},
effects() {
onFieldInit('absId', (field: IFieldState) => {
fetchHolidayTypeList().then((res) => {
field.dataSource =
res?.data?.map((holidayType) => ({
id: holidayType.id,
name: holidayType.name,
})) || [];
});
});
},
});
使用 onFormInit 进行设置
private baseInfoForm = createForm<BaseInfoDetail>({
initialValues: {
...DEFAULT_BASE_INFO,
},
effects() {
onFormInit((form: Form)) => {
fetchHolidayTypeList().then((res) => {
form.setFieldState('absId', (fieldState: IGeneralFieldState) => {
fieldState.dataSource =
res?.data?.map((holidayType) => ({
id: holidayType.id,
name: holidayType.name,
})) || [];
});
});
});
},
});
在 componentDidMount 中设置
表单数据联动
参考文档:https://formilyjs.org/zh-CN/guide/advanced/linkages
https://react.formilyjs.org/zh-CN/api/shared/schema#schemareactions
schema 属性值:https://react.formilyjs.org/zh-CN/api/shared/schema#%E5%B1%9E%E6%80%A7
IGeneralFieldState:https://core.formilyjs.org/zh-CN/api/models/field#ifieldstate
SchemaReaction 类型
import { IGeneralFieldState } from '@formily/core'
type SchemaReaction<Field = any> =
| {
dependencies?: string[] | Record<string, string> // 依赖的字段路径列表,只能以点路径描述依赖,支持相对路径,如果是数组格式,那么读的时候也是数组格式,如果是对象格式,读的时候也是对象格式,只是对象格式相当于是一个alias
when?: string | boolean // 联动条件
target?: string // 要操作的字段路径,支持FormPathPattern路径语法,注意:不支持相对路径!!
effects?: SchemaReactionEffect[] // 主动模式下的独立生命周期钩子
fulfill?: {
// 满足条件
state?: IGeneralFieldState // 更新状态
schema?: ISchema // 更新Schema
run?: string // 执行语句
}
otherwise?: {
// 不满足条件
state?: IGeneralFieldState // 更新状态
schema?: ISchema // 更新Schema
run?: string // 执行语句
}
}
| ((field: Field) => void) // 可以复杂联动
从类型上看有两种方式可以设置 schemaReaction 可以使用显示配置的方式或者函数控制 field 属性进行。
常用的方式为使用配置条件对象,如果有更加复杂的联动规则,推荐使用函数的方式进行联动设置。
进行联动配置时,可以配置 state、schema 和执行语句。
配置 state 时需要查看状态类型 IGeneralFieldState 中相对应的属性,schema 是需要查看 ISchema 中相对应的属性。
import React from "react";
import { createForm } from "@formily/core";
import { createSchemaField } from "@formily/react";
import { Form, FormItem, Input } from "@formily/antd";
const SchemaField = createSchemaField({
components: {
Input,
FormItem
},
scope: {
requiredReactions(field) {
const requiredField = field.query("required_1").take();
console.log("requiredField: ", Math.random(), requiredField.value);
if (!requiredField) {
return;
}
field.setState({
value: requiredField.value
});
}
}
});
const requireSchema = {
type: "object",
properties: {
required_1: {
name: "required_1",
title: "必填",
type: "string",
required: true,
"x-decorator": "FormItem",
"x-component": "Input"
},
required_2: {
name: "required_2",
title: "必填",
type: "string",
"x-validator": {
required: true
},
"x-decorator": "FormItem",
"x-component": "Input"
},
required_3: {
name: "required_3",
title: "必填",
type: "string",
"x-validator": [
{
required: true
}
],
"x-decorator": "FormItem",
"x-component": "Input",
"x-reactions": "{{requiredReactions}}"
// "x-reactions": {
// dependencies: ["required_1"],
// fulfill: {
// state: {
// value: "{{$deps[0]}}"
// }
// }
// }
}
}
};
export default class FormPart extends React.Component {
form = createForm({
initialValues: {
required_1: 1
}
});
render() {
return (
<>
<Form form={this.form} labelCol={6} wrapperCol={10}>
<SchemaField schema={requireSchema} />
</Form>
<div onClick={() => console.log(this.form.values)}> 点击 </div>
</>
);
}
}
Reaction 联动触发的时机
使用联动需要注意触发时机。
- 表单初始化时
- 依赖值初始化时
- 依赖值改变时
Reaction 函数主要设置到被联动的字段配置上,虽然也可以设置到被依赖字段配置上,但是需要注意被依赖字段是否有其他依赖值,容易引发一系列意想不到的联动。
使用 Effect 进行联动
effect 主要是监听被依赖字段值的改变(onFieldValueChange)。
import React from 'react'
import { createForm, onFieldValueChange } from '@formily/core'
import { createSchemaField, FormConsumer } from '@formily/react'
import { Form, FormItem, Input, Select } from '@formily/antd'
const form = createForm({
effects() {
// 字段值改变时触发
onFieldValueChange('select', (field) => {
console.log('effects', Math.random());
form.setFieldState('input', (state) => {
// 对于初始联动,如果字段找不到,setFieldState会将更新推入更新队列,直到字段出现再执行操作
state.display = field.value
})
})
},
})
const SchemaField = createSchemaField({
components: {
FormItem,
Input,
Select,
},
})
export default () => (
<Form form={form}>
<SchemaField>
<SchemaField.String
name="select"
title="控制者"
default="visible"
enum={[
{ label: '显示', value: 'visible' },
{ label: '隐藏', value: 'none' },
{ label: '隐藏-保留值', value: 'hidden' },
]}
x-component="Select"
x-decorator="FormItem"
/>
<SchemaField.String
name="input"
title="受控者"
x-component="Input"
x-decorator="FormItem"
/>
</SchemaField>
<FormConsumer>
{() => (
<code>
<pre>{JSON.stringify(form.values, null, 2)}</pre>
</code>
)}
</FormConsumer>
</Form>
)
使用 addEffects 的方式添加 effect
const form = this.props.form;
form.addEffects('handleReaction', () => {
onFieldValueChange('require_1', (field) => {
// do something
});
});
异步数据校验
官方示例:https://formilyjs.org/zh-CN/guide/advanced/validate#%E5%BC%82%E6%AD%A5%E6%A0%A1%E9%AA%8C
按照校验时机分为:onInput
、 onBlur
、onFocus
。
import React from 'react'
import { createForm } from '@formily/core'
import { createSchemaField } from '@formily/react'
import { Form, FormItem, Input } from '@formily/antd'
const form = createForm()
const SchemaField = createSchemaField({
components: {
Input,
FormItem,
},
})
const schema = {
type: 'object',
properties: {
async_validate: {
title: '异步校验',
required: true,
'x-validator': `{{(value) => {
return new Promise((resolve) => {
setTimeout(() => {
if (!value) {
resolve('')
}
if (value === '123') {
resolve('')
} else {
resolve('错误❎')
}
}, 1000)
})
}}}`,
'x-component': 'Input',
'x-decorator': 'FormItem',
},
async_validate_2: {
title: '异步校验(onBlur触发)',
required: true,
'x-validator': {
triggerType: 'onBlur',
validator: `{{(value) => {
return new Promise((resolve) => {
setTimeout(() => {
if (!value) {
resolve('')
}
if (value === '123') {
resolve('')
} else {
resolve('错误❎')
}
}, 1000)
})
}}}`,
},
'x-component': 'Input',
'x-decorator': 'FormItem',
},
},
}
export default () => (
<Form form={form} labelCol={6} wrapperCol={10}>
<SchemaField schema={schema} />
</Form>
)
使用 debounce 对数据请求接口进行包裹
目的:主要减少 onInput
时请求接口进行数据校验的频率。
import React from 'react'
import { createForm } from '@formily/core'
import { createSchemaField } from '@formily/react'
import { Form, FormItem, Input } from '@formily/antd'
import { debounce } from 'lodash'
const form = createForm()
const SchemaField = createSchemaField({
components: {
Input,
FormItem,
},
})
const debounceValidator = debounce(() => {
return new Promise((resolve) => {
setTimeout(() => {
if (!value) {
resolve('')
}
if (value === '123') {
resolve('')
} else {
resolve('错误❎')
}
}, 100)
})
}, 300)
const schema = {
type: 'object',
properties: {
async_validate: {
title: '异步校验',
required: true,
'x-validator': debounceValidator,
'x-component': 'Input',
'x-decorator': 'FormItem',
},
},
}
export default () => (
<Form form={form} labelCol={6} wrapperCol={10}>
<SchemaField schema={schema} />
</Form>
)
模拟异步请求
请求内容就是被层层包裹,中间进行数据逻辑处理。
试图优化,但是不起效果,以下这段代码有一个延迟返回校验结果的 bug
import React from "react";
import { createForm } from "@formily/core";
import { createSchemaField } from "@formily/react";
import { Form, FormItem, Input } from "@formily/antd";
import { debounce } from "lodash";
const form = createForm();
const SchemaField = createSchemaField({
components: {
Input,
FormItem
}
});
const mockRequest = (value) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (!value) {
resolve("");
}
if (value === "123") {
resolve("");
} else {
reject("错误❎");
}
}, 100);
});
};
const debounceValidator = debounce((value) => {
// 如果不进行 return 则不起效果
return mockRequest(value)
.then(() => {
return "";
// return Promise.resolve("");
})
.catch((err) => {
console.log(err);
return err;
// return Promise.resolve(err);
});
}, 300);
const schema = {
type: "object",
properties: {
async_validate: {
title: "异步校验",
required: true,
"x-validator": debounceValidator,
"x-component": "Input",
"x-decorator": "FormItem"
}
}
};
export default () => (
<Form form={form} labelCol={6} wrapperCol={10}>
<SchemaField schema={schema} />
</Form>
);
最终方案
能够实时显示接口请求的异步校验结果
import React from "react";
import { createForm } from "@formily/core";
import { createSchemaField } from "@formily/react";
import { Form, FormItem, Input } from "@formily/antd";
import { debounce } from "lodash";
const form = createForm();
const SchemaField = createSchemaField({
components: {
Input,
FormItem
}
});
const requestMock = (value) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (!value) {
resolve("");
}
if (value === "123") {
resolve("");
} else {
reject("错误❎");
}
}, 100);
});
};
const debounceValidator = debounce((value, callback) => {
requestMock(value)
.then(() => {
console.log("success");
callback("");
})
.catch((err) => {
console.log(err);
callback(err);
});
}, 300);
const schema = {
type: "object",
properties: {
async_validate: {
title: "异步校验",
required: true,
"x-validator": (value) => {
return new Promise((resolve) => {
debounceValidator(value, resolve);
});
},
"x-component": "Input",
"x-decorator": "FormItem"
}
}
};
export default () => (
<Form form={form} labelCol={6} wrapperCol={10}>
<SchemaField schema={schema} />
</Form>
);
formily 校验的核心代码
源码链接:https://github1s.com/alibaba/formily/blob/HEAD/packages/validator/src/parser.ts
表单 EFFECT HOOK 使用
踩坑 issue:表单切换显隐(组件卸载并重新挂载)后,所有 effects 失效
最后的解决方案是在需要使用 formily hook 的组件中手动 addEffects 添加 formily hook。
踩坑的主要原因是,公司自研的组件库 tabs 组件切换 tab 时是通过组件的卸载来进行切换的,卸载了的 formily 校验就会完全失效永远走的是 success。突然想到跟我最开始的时候触发的一个坑一样的,卸载掉的组件触发校验就是永远都是 success。
参考链接:
Form Effect Hooks:https://core.formilyjs.org/zh-CN/api/entry/form-effect-hooks
Field Effect Hooks:https://core.formilyjs.org/zh-CN/api/entry/field-effect-hooks
常用的 effect hooks
onFieldValueChange:用于监听某个字段值变化的副作用钩子。这个钩子会在 form 设置初始化值和 form.setValues 时触发。
onFieldInputValueChange:用于监听某个字段 onInput 触发的副作用钩子。仅仅想在用户触发了 input 事件后响应值的变化,也可以监听到选项框切换选项,check box 触发勾选等情况。
onFieldReact:用于实现字段响应式逻辑的副作用钩子,它的核心原理就是字段初始化的时候会执行回调函数,同时自动追踪依赖,依赖数据发生变化时回调函数会重复执行。内部会自动收集依赖。用户操作手动改变赋值时不会触发,使用 api setValue 时会触发。
不知道会不会有升级 formily 的坑。
⚠️:这个 hooks 会追踪依赖,依赖改变就会触发 onFieldReact hooks
onFormValidateStart:用于监听表单校验开始的副作用钩子。
onFormReact: 用于实现表单响应式逻辑的副作用钩子,它的核心原理就是表单初始化的时候会执行回调函数,同时自动追踪依赖,依赖数据发生变化时回调函数会重复执行。内部会自动收集依赖,内部依赖值改变就会触发 hooks。
如果 onFormReact 内部监听的数据没有 field 组件去承接,会在页面隐藏或者组件销毁时崩溃。
原因:onFormReact 中做了额外的 onFieldInit 收集 dispose 函数,并在销毁时进行触发,如果没有 field 组件去承接则就没有 dispose 函数,dispose 函数运行没有做判空处理。
踩坑
初始使用的时候,对定义不是很清晰,所以踩了很多不必要的坑。
一个 form 实例对应一个 FormProvider 组件
- 如果使用多个 FormProvider 组件对应一个 form 实例,则会触发意想不到的 bug。
- 需要注意 form 显隐控制导致的 validate 失效问题。
FormProvider 组件中可以使用多个 SchemaField 组件
field 属性值部分需要注意的地方
使用 visible 属性设置隐藏(none)的时候,字段隐藏则会为 undefined,在获取 form.values 时会过滤掉。
hidden 属性设置隐藏(hidden)的时候,字段隐藏,但是在获取 form.values 时仍然保留字段值。
active
触发 onFocus 为 true,触发 onBlur 为 false
visited
触发过 onFocus 则永远为 true
inputValue
触发 onInput 收集到的值
inputValues
触发 onInput 收集到的多参值
hidden
为 true 时, display 为 hidden;为 false 时 display 为 visible
visible
为 true 时, display 为 visible;为 false 时 display 为 none
参考链接:https://core.formilyjs.org/zh-CN/api/models/field#%E8%AF%A6%E7%BB%86%E8%A7%A3%E9%87%8A