2023-02-02
表单
在 React 中裸用表单需要维护大量的 value 和 onChange,自然需要选择合适的表单方案。那么在做技术选型前,不妨先列出我们对于react 表单方案的期望。
基础功能
这些功能点是每个表单方案必须拥有的,也都比较基础,市面上大部分表单方案 star 多的少的、自己造轮子的 也都会囊括这些功能。
✔️ 收集表单数据
✔️ 管理表单状态(未验证、未提交、校验状态等)
✔️ 支持表单校验
✔️ 支持自定义触发校验时机(submit/hover/实时/自定义等)
✔️ 支持自定义校验错误后的信息展示
✔️ 支持自定义组件/接入第三方UI库
罗列表单方案
通过搜索引擎找出 比较常用的 / 成熟的 / 热门的 表单方案:
· Antd Form
· Fusion Next Form
· formik
· react-final-form
· NoForm
· uform
· redux-form
· react-jsonschema-form
· Informed
· formal
这么多表单方案,该如何做选择?我们先大致过一遍,redux-form 依赖 redux,dan 都说 You Might Not Need Redux,从耦合性的角度考虑表单方案不应该依赖 redux ,同理 Fusion Next Form 是 fusion 内建的表单方案,react-jsonschema-form 强依赖了 Bootstrap,暂不考虑。Antd Form 基于的 rc-form 可以脱离 Antd 使用。
hooks 形式的有 Informed、formal、formik@v2.x、react-final-form-hooks。对于新的一些技术我更看重他的使用场景,使用新技术会带来什么好处?曾经我也激进过,一味求新,现在更多的是把技术当作实现产品的工具。目前来看这些并没有带来特别明显的优势,反倒是需要承担“小白鼠”的角色,所以暂且先观望看看。
关注点
初筛完一轮后,详细看了文档,整理出了如下一些关注点,希望通过这些功能点对这些方案做一次横向的对比。
表单的描述形式
既然是 react 的表单方案,大部分都是基于 JSX 的。表单的最上层大同小异,无非会有个 Form 或是自己的 Class,在这里不做讨论,更多讨论的是表单项的代码书写方式。
JSX + JSON
第一类的代表是 rc-form、formal,在 JSX 里写一个 JSON 描述校验规则,将和表单项有关的信息(字段名,校验规则等)都集中在一处描述,通过展开运算符向 UI 组件传入“处理好的”props,自动绑定 value、onChange。 这个应该是最常用的,我的感受是表单一旦多起来或是代码写多了,会占用大量篇幅,满屏幕的 JSON 可能会有点视觉疲劳。
// createForm()(Component)
render() {
const { getFieldProps } = this.props.form;
return (
<input {...getFieldProps('name', {
rules: {
required: true,
message: 'Please input your name!'
}
})}/>
);
}
表单元素抽象概念
第二大类则是有 Field 或是 FormItem (之后简称为 F)的表单元素概念,这样的设计我更加看好,将 JSON 的写法改成了正常的 JSX,整体感官上舒服了不少,也有比较多的库都实现了类似的 API。F 作为表单元素的各种抽象,对外提供一致接口,例如字段名、校验规则、表单域组件、value 等等。
<F type="email" name="email" placeholder="Email" />
<F component="select" name="color">
<option value="red">Red</option>
<option value="green">Green</option>
<option value="blue">Blue</option>
</F>
<F name="firstName" component={CustomInputComponent} />
<F name="age">
{({ input, meta }) => (
<div>
<label>Age</label>
<input {...input} type="text" placeholder="Age" />
{meta.error && meta.touched && <span>{meta.error}</span>}
</div>
)}
</F>
那么表单元素 F 包含了什么?表单标签?表单域?错误提示?每个库对此有不同的理解。
formik
个人理解 formik 更倾向于 F 是一个纯粹的表单域(Input, Select ...),Field 的 component 字段默认就是 input,认为“完整”的 F 是 Fieldset,这个在 demo 中有体现,当然 API 也并没有限制开发者自由发挥,Field 支持 render props。
react-final-form
react-final-form 的 F 设计的比较开放,并没有官方的说法,一千个前端就有一千个哈姆雷特,F 是什么由开发者定义,提供了三个字段 component, render, children 供选择。
uform
uform 的 F 是什么由开发者定义,API 设计得和前两者不太一样,F 默认是一个表单域,开发者可以通过 registerFieldMiddleware 这个 API 设置 F 的 Wrapper。
NoForm
NoForm 对于 F 有自己的理解,认为 F 是一个完整的表象元素抽象,在文档中有详细描述。他给我的感受和前几个不太一样,他为开发者设计好了表单的一切,试图给出一个最佳实践,什么元素(表单标签、错误提示、表单域前缀、后缀等)应该放在哪里,对应的你需要传入哪个参数都帮你决策好了,当然这样的设计也牺牲掉了一部分灵活性,使用起来更像一个表单域的 Wrapper。
<F label="input" name="input">
<Input />
</F>
UI 组件适配
现在前端开发项目已经离不开 UI 组件库了,作为表单方案,如何接入组件库也是一个关注点。
rc-form、formal
通过特定的 API 结合 Spread syntax 往 UI 组件传递参数,主要是 value 和 onChange,不需要专门编写对应的适配 UI 组件,能够快速接入。
import { Input } from 'antd';
{getFieldDecorator('name', {
rules: [{ required: true }],
//valuePropName: 'checked', // <Switch/>
})(
<Input />
)}
表单元素概念的设计通常还会有一层 “适配层” 亦或是 “接入层”,类似 Adapter / Wrapper 的概念,用于更快速的接入第三方 UI 组件库。上层的 F 负责维护 value,error、onChange 等等,适配层根据下层 UI 组件的要求传递这些属性,下层的 UI 组件负责纯展示。各个库对适配层的设计也不尽相同。
formik
formik 的 Field 会传递两个特定的参数,分别是 field 和 form,前者主要包含 value, onChange, onBlur,后者包含 isSubmitting, touched, errors 等一些表单状态及工具方法。因为大部分 UI 组件接收的是 value 和 onChange,所以需要专门编写对应的适配层。
import { Input } from 'antd';
const InputWrapper = ({ field: {name}, form: { touched, errors }, ...restProps }) => (
<div>
<Input {...field} {...restProps}/>
{touched[field.name] &&
errors[field.name] && <div className="error">{errors[field.name]}</div>}
</div>
)
react-final-form
react-final-form 和 formik 在 F 设计上雷同,提供了 input 和 meta,分别对应 formik 的 field 和 form,在字段上有些许差异,就不做多余的描述了,见官方demo:
https://codesandbox.io/s/40mr0v2r87
https://codesandbox.io/s/9ywq085k9w
uform
uform 提供了 registerFormField 用于注册表单字段组件,registerFieldMiddleware 用于设置 wrapper。当然作为阿里内部的表单框架,自带了自家的两个UI库适配层 @uform/antd, @uform/next,还是深度定制的。
import { Input } from 'antd'
// 最简版
registerFormField('testInput', Input)
registerFieldMiddleware(Field => {
return props => {
const { errors, schema } = props
// errors handle todo
return React.createElement(
'div',
{},
React.createElement(span, {}, schema.title),
React.createElement(Field, props)
)
}
})
<Field type="testInput" label="name" />// 应用
值得一提的是这个 string type 的设计,写表单的时候不需要从外部 import 组件,预先注册好之后,Field 组件就会帮助开发者匹配相对应的组件,是一个能够提升工作幸福感的设计。
NoForm
如上文提到 NoForm 的 FormItem 本身更像是个表单域的 Wrapper,而且 FormItem 在内部会做一个 cloneElement,将处理好的 props 传递给子组件,因此接入 UI 库理论上甚至不需要编写额外的适配层。但是出于对外提供一致接口的考虑,比如 Switch 的值是 checked,Input 是 value,还是需要有这个 UI 适配层。当然同作为阿里内部的框架,也提供了两个组件库的适配层 nowrapper。
import { Input } from 'antd'
<FormItem label="input" name="input">
<Input />
</FormItem>
表单校验
表单校验是每个表单方案绕不过的一道坎,也是我们的重点关注点之一。通常会支持表单级、字段级的验证,常规功能在这里就不讨论了,归纳总结了一些看到的特点:
校验规则外置
通常的校验规则是与 F 一一对应,在 F 相关的 JSX 中描述,每个 F 上会有类似 validate 的字段 或是 通过展开运算符传入,代表是 react-final-form, uform, rc-form, formal。
F name="email" validate={...}>
...
</F>
formik, NoForm, formal 支持校验规则外置,这样的设计应该是基于“关注点分离”的原则,JSX 用于组织 UI,校验规则并不是 UI 的固有属性应该分离出来,从而进一步降低耦合性,目的是帮助我们写出清晰、易维护的代码。
formik 官方推荐使用 Yup(一个功能强大的规则校验工具,链式风格的 API 设计),并针对 Yup 做了优化,提供 validationSchema 属性方便接入。
NoForm 提供了 validateConfig 用于外置校验规则,需按规则传入 JSON 对象,用 async-validator 作为校验工具。
const validateRules = {
email: string()
.email('Invalid email')
.required('Required'),
// ...
}
<Form validateRules={validateRules}>
<F name="email">
...
</F>
</Form>
动态校验
有一类场景例如注册需要输入的两次密码一致。还有一类场景是表单有联动,选择了 B 后,C 需要必填。我把这些称为 动态校验,表单校验会依赖用户的输入 或是 表单联动带来的校验规则的改变。
formik 推荐的 Yup 提供了 ref 获取其他字段的引用。
let schema = object({
baz: ref('foo.bar'),
foo: object({
bar: string(),
}),
x: ref('$x'),
});
schema.cast({ foo: { bar: 'boom' } }, { context: { x: 5 } });
// => { baz: 'boom', x: 5, foo: { bar: 'boom' } }
NoForm 的 validateConfig 支持动态配置。
const validateConfig = {
username: {type: "string", required: true},
age: (values, context) => { // dynamic validate config
const { username } = values;
return {type: "string", required: !!username };
}
}
其他方案都提供了类似 values 的字段供回调函数拿到所有字段值用于处理逻辑,例如 react-final-form validate 的 allValues, uform 的 x-rules,rc-form validateFields 的 values 等等。除此之外 uform 还引入了 effects 的概念来解决联动问题。
处理联动
数据联动,归根结底是字段间的相互依赖关系,同时附加了依赖动作,同时依赖动作的执行是存在时序的。
联动在表单中比较常见,比较理想的是框架能够约束开发者优雅的去处理联动,拒绝面条式代码。最常见的是表单项的显示隐藏,NoForm 的 If、react-final-form 的 demo 都提供了类似 jsx-control-statements 的思路,更像是 JSX 函数表达式的语法糖。
// 伪代码
<F label="showA" name="showA">
<CheckBox />
</F>
<If condition={ showA === true }>
<F label="A" name="A">
<Input />
</F>
</If>
其他更复杂的联动场景只有 uform 交出了自己的答卷,在其 文档 中有较为详细的罗列及其解决方案,当然解决问题的同时也引入了很多其他概念。
数组、嵌套表单
表单嵌套、数组字段这两类场景在日常开发中也经常遇到,表单方案对此也有不同的设计,试图帮助开发者更优雅的处理此类场景。
字段的嵌套结构
rc-form、react-final-form、 formik 的字段名支持点括号语法,即支持以嵌套结构定义字段名,例如 object.a.b、array[2] 等。
<F name="object.a">
...
</F>
uform、 NoForm 则是根据节点的父子关系来定义字段的结构。
<F name="object">
<F name="a">
...
</F>
</F>
数组类型的字段
此类场景一般还会伴随着表单项的动态增删,因此 react-final-form、 formik 除了支持点括号外还提供了工具类 FieldArray,提升开发体验。具体见 formik FieldArray demo、react-final-form FieldArray demo。
而 uform 提供了 createArrayField,NoForm 则提供了 repeater。
性能开销
性能是每个框架绕不开的话题,在开始之前,我有这样的一个思考:表单的性能问题遇到的多嘛?
结合自己的工作经验,个人认为表单性能问题通常情况下不会遇到,个别情况下会存在,例如表单嵌套,大型项目的配置页等场景。排除特殊场景,一个页面中包含大量表单项本身就是不合理的,这样的设计会影响用户体验(脑补画面:用户正在操作满屏幕的表单),所以在问题成为问题之前可能不需要倾注太多精力,比较好的策略是出现问题解决问题,当然有足够的精力能够提前知晓/解决问题也是好的。
回过头来在做技术调研时还是需要有一个全面的考量,性能开销的关注点主要在于单个表单项的改动是否会引起整个表单重新渲染,即观察用户在某些字段中输入值时,其他无关联的表单项会不会 rerender。
经测试 react-final-form 和 uform 做到了这点,在内部实现上均使用了发布订阅,每个表单项只会订阅和自己相关的改动,实现单个改动不影响全量。其他表单方案的状态管理至上而下,均会造成整个表单重新渲染,formik 因此提供了 FastField 用于改进其性能,内置 shouldComponentUpdate 阻止不必要的渲染。
序列化
表单的序列化常见于 动态表单需求 或是 可视化搭建系统上,这类序列化场景主要看业务需求,通常会约定一个 JSON DSL 定义数据结构用于描述表单。
这类需求一般比较偏业务,通用的可能并不好用,所以框架上能做的并不多,有个别框架提供了自己的方案,比较有名的是 react-jsonschema-form, uform 也有自己的理解并提供了 Form Schema。
其他关注点
· less magic,这是一个加分项,内部实现的越简单意味着潜在的 bug 越少,调试更简单,黑魔法是一把双刃剑。
· 确保开源项目的可维护性,即作者有没有充足的热情持续维护下去,可以观察遗留的 issue 数量、社区讨论、pr 跟进情况等等。
小结
框架的内部实现方式有很多,设计的选择和权衡也不同,目标也不同,不能一概而论。比如在我看来,uform、 NoForm 想做的是一个开箱即用的方案,我什么都给你做好了,UI适配层、联动方案、校验啥都有,直接用就好;而其他方案都做的比较精简,只提供基础通用的部分,其他的交给开发者自行选择设计,带来的好处是约束少可发挥空间大。
上一篇:MySQL主主复制的实现
下一篇:Sitespeed使用教程
开班时间:2021-04-12(深圳)
开班盛况开班时间:2021-05-17(北京)
开班盛况开班时间:2021-03-22(杭州)
开班盛况开班时间:2021-04-26(北京)
开班盛况开班时间:2021-05-10(北京)
开班盛况开班时间:2021-02-22(北京)
开班盛况开班时间:2021-07-12(北京)
预约报名开班时间:2020-09-21(上海)
开班盛况开班时间:2021-07-12(北京)
预约报名开班时间:2019-07-22(北京)
开班盛况Copyright 2011-2023 北京千锋互联科技有限公司 .All Right 京ICP备12003911号-5 京公网安备 11010802035720号