这是我们团队号工程化系列文章的第 4 篇,全系列文章如下:
团队尚有HC,感兴趣的小伙伴可以私信~(注明期望岗位城市:北京、上海、杭州)
1. 中台表单有哪些难点
中后台领域最复杂的场景之一就是表单场景 ,它的主要特点是大、难、杂:
- 字段数量多,如何让性能不随字段数量增加而变差
- 字段关联逻辑复杂,如何更简单的实现复杂的联动逻辑,怎么样才能不影响到性能
- 表单数据管理复杂,有前后端格式不一致、同步默认值与异步默认值合并逻辑复杂、跨表单数据通信等情况
- 表单状态管理复杂,比如自增的表单
- 表单动态配置化
这些问题在React技术栈下会更加凸显
由于React不提供数据的双向绑定,在使用React构建表单系统时,我们需要反复的:监听事件 -> useState,从而在view和model之间同步数据。另外,频繁的useState也为表单的渲染增加了难度,一个不经过主动优化的React表单,极易出现性能问题。关于react的渲染方式可以看这个juejin.cn/post/720208...
下面这个视频作为例子, 动其中一个表单将会引发整个页面的全量渲染(如果 嵌套表单+复杂表单 性能岂不是玩完)
js
import { useState } from "react";
function App() {
const [value, setValue] = useState({});
const name = ['满','天','神','佛','皆','听','我','令','啊','哈']
return (
<>
{new Array(10).fill(1).map((item, index) => (
<form>
<p>
<label>
<span style={{padding:'0px 20px'}}> {name[index]}</span>
<input
type="text"
value={value[index]}
onChange={(event) => {
setValue({...value, [index]: event.target.value });
}}
/>
</label>
<input type="submit" value="Submit" />
</p>
</form>
))}
</>
);
}
export default App;
难道, react就不应该开发表单吗 ???🐶
选vue吧。no!!!
所以大家会发现,在热门的表单解决方案中,基本就是react的天下(狗头保命
2. React 官方的解决方案
官方给出了两种不同的表单方案, 基于受控组件以及基于非受控组件的表单实现,所有第三方表单都可以认为是这两种方案的延伸及 封装,所以这里重点介绍一下
受控组件
在 HTML 中,<input>
、<textarea>
和 <select>
等表单元素通常会维护自己的状态并进行更新基于用户输入。在 React
中,可变状态通常保存在组件的 state 属性中,并且仅使用 useState 进行更新。
我们可以通过使 React
状态成为"单一事实来源"来将两者结合起来。然后,呈现表单的 React
组件还控制后续用户输入时该表单中发生的情况。通过这种方式由 React 控制其值的输入表单元素称为"受控组件"。
大白话:父组件完全控制该组件的状态及回调函数, 子组件的状态变更需要通过回到函数通知到父组件, 由父组件完成状态变更后再将新值传回子组件
js
import { useState } from "react";
function Controlled() {
const [value, setValue] = useState(11);
return (
<form>
<p>
<label>
<span style={{padding:'0px 20px'}}> 受控的</span>
<input
type="text"
value={value}
onChange={(event) => {
setValue(event.target.value);
}}
/>
</label>
</p>
</form>
);
}
export default Controlled;
非受控组件
非受控组件将状态存储在自身内部, 我们可以借用React
中的 ref
来访问它,它们可以自行记住你输入的内容,你可以使用一个 ref
对象来获取它们的数值 value
js
import { useRef } from "react";
function UnControlled() {
const value = useRef(null);
return (
<>
<form>
<p>
<label>
<span style={{ padding: "0px 20px" }}> 非受控的</span>
<input
type="text"
ref={value}
onChange={(event) => {
value.current = event.target.value;
}}
/>
</label>
</p>
</form>
<input
type="submit"
value="Submit"
onClick={() => console.log(value.current)}
/>
</>
);
}
export default UnControlled;
小结
关于受控 vs 非受控的选择,大部分文档认为应该优先考虑受控模式, 可惜还是没有找到一个能让人信服的理由. 大量文档无非是列举受控之于非受控更灵活, 符合 React 单项数据流,但其实受控和非受控在某些场景可以相互模拟对方,所以选择什么类型进行开发,还是要结合当下的需求。
但是官方给出的方案虽然简洁直观, 但是直接拿来写需求的话还是很简陋, 场景稍微一复杂效率就不是很高,所以很自然地, React社区给出了相当多的三方表单方案, 下面我们会逐个分析比较热门的方案,以及这些方案的共通处以及优缺点~
3.React社区表单方案
竞品对比
我们先看一下这是很多人下的结论,如下图,接下来我们按图索骥慢慢剖析,论证一下每个第三方表单的优缺点
能力 | rc-form | rc-field-form | React Hook Form | Formily1.X | Formily2.x |
---|---|---|---|---|---|
自定义组件接入成本 | 高 | 低 | 高 | 低 | 低 |
性能 | 不好 | 较好 | 好 | 非常好 | 非常好(精确渲染) |
是否支持动态渲染 | 否 | 否 | 否 | 是 | 是 |
是否开箱即用 | 是 | 是 | 否 | 是 | 是 |
是否支持跨端 | 否 | 否 | 否 | 是 | 是 |
开发效率 | 一般 | 一般 | 一般 | 高 | 高 |
学习成本 | 低 | 低 | 低 | 很高 | 高 |
视图代码可维护性 | 低 | 低 | 低 | 高 | 高 |
场景化封装能力 | 无 | 无 | 无 | 有 | 有 |
是否支持表单预览态 | 否 | 否 | 否 | 是 | 是 |
体积 | 一般 | 一般 | 小 | 大 | 小 |
1. Antd Form 3.X (rc-form)
虽然是个比较具有年代感的组件和解决方法了,但是我还是想从它入手
为什么要使用 rc-form
主要是使用HOC
减少数据同步的重复机械操作,因为React
中需要我们手动调用 hooks
实现数据驱动视图的改变,但是表单多的时候,难道页面要写十几个onChange
事件去实现页面的数据驱动视图的更新吗,这个时候 rc-form
就应运而生了,rc-form
创建一个数据集中管理仓库,这个仓库负责统一收集表单数据验证、重置、设置、获取值等逻辑操作,这样我们就把重复无用功交给 rc-form
来处理了,以达到代码的高度可复用性
示例
js
// 不使用rc-form
import React, { useState } from "react";
export default function App() {
const [name, setName] = useState("");
const [age, setAge] = useState("");
const [password, setPassword] = useState("");
return (
<form action="">
<label for="">用户名: </label>
<input type="text" value={name} onChange={(e)=>setName(e.target.value)} />
<br />
<label for="">年龄: </label>
<input type="text" value={age} onChange={(e)=>setAge(e.target.value)} />
<br />
<label for="">密码: </label>
<input type="text" value={password} onChange={(e)=>setPassword(e.target.value)} />
<br />
</form>
);
}
// 使用rc-form
import { createForm, formShape } from "rc-form";
class Form extends React.Component {
static propTypes = { form: formShape };
submit = () => {
this.props.form.validateFields((error, value) => {
console.log(error, value);
});
};
render() {
let errors;
const { getFieldProps, getFieldError } = this.props.form;
return (
<form>
{" "}
<label>姓名:</label>{" "}
{getFieldDecorator("username", {
rules: [{ required: true, message: "请输入用户名!" }],
initialValue: "initialValue",
})(<input type="text" />)}{" "}
<br /> <label>密码:</label>{" "}
{getFieldDecorator("password", {
rules: [
{ required: true, message: "请输入密码!" },
{ pattern: /^[a-z0-9_-]{6,18}$/, message: "只允许数字!" },
],
})(<input type="password" style={{ marginTop: "15px" }} />)}{" "}
<br />{" "}
<button onClick={handleSubmit} style={{ marginTop: "15px" }}>
{" "}
提交{" "}
</button>{" "}
</form>
);
}
}
export createForm()(Form);
性能问题
在官方例子里面,我们也跑跑性能,可以看到任何一个表单的变动,都将导致整个表单层面的渲染
设计思路
小结
在rc-form出的时候,react还没迎来hooks的时代,当时流行的通用解决方法是HOC
(前端真是迭代非常快 ),所以rc-form
从官方给的受控组件出发设计,设计了一个HOC
,接管表单内部的空间,后续交互把输入输出等放到组件内部,解放了用户,使用户不再需要给每个表到绑定一个事件,在当下算是在复用的方面解决了很大的不足,但是由于它使用了forceUpdate
来更新数据来驱动ui一个表单变更会导致整个表单更新,这个性能肯定是严重不足的,这也是常常被诟病的一点,但是时代在发展 , 在之后也有专门的第三方表单为了性能这块下了功夫.
2. Antd Form4.X (rc-field-form)
基于上述提到的一些缺陷 , 那我们分析下面这个,就从性能入手,大家看看我在官网例子中做的测试,可以看到rc-field-form
已经在性能上取得了不错的成绩 , 实现了精准渲染,性能提升非常多
示例
js
import React, { Component, useEffect} from 'react'
import Form, { Field } from 'rc-field-form'
const nameRules = {required: true, message: '请输入姓名!'}
const passwordRules = {required: true, message: '请输入密码!'}
export default function FieldForm(props) {
const [form] = Form.useForm()
const onFinish = (val) => {
console.log('onFinish', val)
}
const onFinishFailed = (val) => {
console.log('onFinishFailed', val)
}
return (
<div>
<h3>FieldForm</h3>
<Form form={form} onFinish={onFinish} onFinishFailed={onFinishFailed}>
<Field name='username' rules={[nameRules]}>
<Input placeholder='请输入姓名' />
</Field>
<Field name='password' rules={[passwordRules]}>
<Input placeholder='请输入密码' />
</Field>
<button>Submit</button>
</Form>
</div>
)
}
这种写法还是非常便捷的,不再需要像 antd3
一样使用高阶函数包裹一层。而是直接通过 Form.useForm()
获取到 formInstance
实例, formInstance
实例身上承载了表单需要的所有数据及方法。 通过 form.setFieldsValue({username: ''})
这段代码就不难发现,可以通过 form
去手动设置 username
的初始值。也可以理解成所有的表单项都被 formInstance
实例接管了,可以使用 formInstance
实例做到任何操作表单项的事情。 formInstance
实例也是整个库的核心。
设计思路
支持嵌套数据结构
rc-field-form
还有一个巨大的优点 , 那就是对嵌套数据结构的支持, 比如我们知道实际上用户填写表单最终得到的实际上是一个大json,对于比较简单的场景, 可能表单的json只有一级,
js
{
"name": "抱枕",
"age": "18",
"password": "12345676"
}
但是往往在复杂场景中,json确是很多级来嵌套的
js
{
"name": "抱枕",
"age": "18",
"password": "12345676",
"dafaultAddressList": [
{
"city":"上海",
"code":"19号"
},
{
"city":"北京",
"code":"20号"
}
]
}
在这个时候,我们简单的例子就不够用了,所以在AntdForm里面的表现为,下面是官网例子
js
<Form form={form} name="dynamic_form_complex" onFinish={onFinish} autoComplete="off">
<Form.List name="sights">
{(fields, { add, remove }) => (
<>
{fields.map(field => (
<Space key={field.key} align="baseline">
<Form.ItemnoStyleshouldUpdate={(prevValues, curValues) =>
prevValues.area !== curValues.area || prevValues.sights !== curValues.sights
}>
{() => (
<Form.Item{...field}label="Sight"name={[field.name, 'sight']}rules={[{ required: true, message: 'Missing sight' }]}>
<Select disabled={!form.getFieldValue('area')} style={{ width: 130 }}>
{(sights[form.getFieldValue('area') as SightsKeys] || []).map(item => (
<Option key={item} value={item}>
{item}
</Option>
))}
</Select>
</Form.Item>
)}
</Form.Item>
<Form.Item{...field}label="Price"name={[field.name, 'price']}rules={[{ required: true, message: 'Missing price' }]}>
<Input />
</Form.Item>
<MinusCircleOutlined onClick={() => remove(field.name)} />
</Space>
))}
<Form.Item>
<Button type="dashed" onClick={() => add()} block icon={<PlusOutlined />}>
Add sights
</Button>
</Form.Item>
</>
)}
</Form.List>
</Form>
在官网rc-field-form给出了这种设计的说明
所以的字段, 无论层级, 都是被拍平管理的, 虽然本质上可以认为是一颗树, 但是关于结构的信息只体现在了 name
,
对于表单项的操作都是接受NamePath
然后对对应的匹配项进行操作, 换个维度来理解的话, 本质上你传入的 NamePath
就是需要操作节点的路径,在这里性能不足的就是就是当操作的目标节点为非叶子节点时, 更新需要同步到它的所有子孙上去.
小结
Form
组件, Field
组件是通过一个全局的 context
作为纽带关联起来的,它们共享 FormStore
中的数据方法,非常类似 redux
工作原理, 多读各个库源码好处还是很多的。
通过把每个 Field
组件实例注册到全局的 FormStore
中,实现了在任意位置调用 Field
组件实例的属性和方法,这也是为什么 Field
使用 class
组件编写的原因(因为函数组件没有实例)。
rc-field-form
以及其他一些表单都采取了自己独立更新,依赖项目走订阅更新的路子,这本质上就是带来了更好的通信效率及更优秀的表单性能。其实单单对比3.x
以及 4.x
背后的表单,我们可以看出表单方案的性能问题本质是在解决各个表单项与项之间及与表单整体之间的通信问题。 这里antd 4.x
利用发布订阅模式有选择地通知替代了无脑rerender
流重绘, 实质上是更细粒度的组件通讯实现了更小的通讯代价,但是美中不足的就是在嵌套表单中当操作的目标节点为非叶子节点时, 更新需要同步到它的所有子孙上去,渲染粒度还是稍稍粗了一些
但是值得肯定的是第三方表单在性能方面做出了选择,已经有了一个雏形。
3. React-Hook-form
可能大家不太了解这个,但是一定程度上它是比较流行的,号称业界性能第一的表单方案
官方号称最大限度地减少重新渲染的次数、最大限度地减少验证计算并加快挂载速度,并且是一个没有任何依赖项的小型库。
换了个思路,从非受控的角度入手,非受控表单它是通过ref来直接拿到表单组件,从而可以直接拿到表单的值,不需要对表单的值进行状态维护,就不再是使用state那一套了,这就使得非受控表单可以减少很多不必要的渲染。
但是非受控表单也存在着它的问题,在动态校验、动态修改(联动)方面不是很方便,因为搞成了跳出react
来直接操作原生的这么一起思路,于是在react hooks
出现以后诞生了一个以非受控思想为基础的表单库 react-hook-form
, 这个表单的设计思路很新奇,完全是拥抱原生, 拥抱非受控。 核心思路是各个组件自身维护各自的ref, 当校验、提交等发生时,便通过这些ref来获取表单项的值。
我们看演示可以看出,普通的输入,他是直接避开了react
的渲染逻辑,使用了ref
拥抱了原生,但是涉及到联动或者校验,还是会导致表单全量渲染,这点在性能上算是一个短板
示例
js
import React from 'react'
import ReactDOM from 'react-dom'
import { useForm } from 'react-hook-form'
function App() {
const { register, handleSubmit, errors ,control } = useForm({
defaultValues: { firstname: "抱" },
})
const onSubmit = (data) => {
console.log(data)
}
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input name="firstname" ref={register} />
<input name="lastname" ref={register({ required: true })} />
{errors.lastname && 'Last name is required'}
<input name="age" ref={register({ pattern: /\d+/ })} />
{errors.age && 'Please enter number for age'}
<input type="submit" />
</form>
)
}
ReactDOM.render(<App />, document.getElementById('root'))
对比antd3
的getFieldDecorator
方案简洁了很多,与antd4
对比,简洁度上也不分上下。而且由于 react-hook-form
是把组件的值保存在ref
中的,因此会在组件内部变化时避免整个视图重绘,这样也会给大型表单项目带来可观的性能收益。在性能敏感的场景,一方面考虑依赖的体积,另一方面考虑交互的流畅,react-hook-form
不失为一个好方案。
useFieldArray嵌套数组
js
function Test() {
const { control, register } = useForm();
const { fields, append, prepend, remove, swap, move, insert } = useFieldArray({
control,
name: "test",
});
return (
{fields.map((field, index) => (
<input key={field.id} name={`test[${index}].value`} ref={register()} />
))}
);
}
在antd3
中,对于array的处理十分繁琐。当然在4.x
版本重写Form
组件后,基本解决了这个问题,处理方式和useFieldArray
类似了
设计思路
-
通过代理引入表单状态订阅模型
-
避免不必要的计算
-
需要时隔离组件重新渲染
小结
react-hook-form
其实是受控+非受控组合的一个第三方表单,虽然值管理做到了精确渲染,但是在触发校验的时候,还是会导致表单全量渲染,因为 errors
状态的更新,内部设计是必须要整体受控渲染才能实现同步,这仅仅只是校验会全量渲染,其实还有联动,react-hook-form
要实现联动,同样是需要整体受控渲染才能实现联动,这一点的设计就比较鸡肋了,由于它不提供任何对表单渲染或布局的内置支持,因此开发人员必须手动处理这些方面。总的来说,react-hook-form
是一个强大且灵活的库,用于 React
中的表单验证和管理。它的简单性、轻量级尺寸和对性能的高度关注使其成为想要简单的表单处理解决方案的开发人员的不错选择。如果在性能敏感的场景,一方面考虑依赖的体积,另一方面考虑交互的流畅,react-hook-form
不失为一个好方案。
4. Formily1.X
大家可能在想 为什么rc-field-form
和rc-field-form
已经出现了,也已经解决了全量渲染的问题,为什么还会开源一个针对表单的包,它的好处在哪里,我们先拿两段视频做对比来举例子看看
rc-field-form
在array中的表现
Formily1.X
在array中的表现
这也算是 Formily1.X
的核心亮点之一,当然,Formily1.X
在联动场景下,同样可以做到精确更新,就是说,A/B 两个字段发生联动,如果 A 控制 B 更新,那么只会更新 B,同理,A 控制 B/C/D 更新,那么也只会更新 B/C/D,精准打击,让您的表单性能大大提升,特别在于某些表单项特别多的场景下,Formily1.X
的优势尤为明显。
示例
js
import React, { useState } from 'react'
import ReactDOM from 'react-dom'
import { Button } from 'antd'
import { Schema, SchemaForm, SchemaMarkupField as Field } from '@formily/antd'
import { Input, ArrayTable, ArrayCards } from '@formily/antd-components'
import 'antd/dist/antd.css'
const App = () => {
return (
<SchemaForm
components={{ ArrayTable, ArrayCards, Input }}
initialValues={{
userListTable: [{ username: 'hello', age: '21' }]
}}
>
<Field
title="用户列表Table"
name="userListTable"
maxItems={3}
type="array"
x-component="ArrayTable"
>
<Field type="object">
<Field name="username" x-component="Input" title="用户名" />
<Field name="age" x-component="Input" title="年龄" />
</Field>
</Field>
</SchemaForm>
)
}
ReactDOM.render(<App />, document.getElementById('root'))
可以看见,Formily1.X
的Array嵌套表单,写法比react-hook-form
和rc-field-form
更加的简洁优雅
创新点:
-
ref=action
-
onchange
等等事件 =effect
-
Formliy
也是订阅式的,他的表现更加独特。他把你所有表单的相关逻辑都收敛到了effect
里面,写法非常新颖。 -
Formily
用effect
来收敛表单的各种行为,对比其他方案的onChange
回调散落在各地,他收拢了整个表单,方便了可读性。path_rule
这个是Formily
自定的DSL
,可以精准的定位到某个表单项,又有能力做批量的处理、批量订阅,这其实也可以算一大创新。一对多,一对一,多对一都可以很好的表达。 -
核心在
$('event_type', 'path_rule')
, 意思是搜索所有path_rule
命中的表单项,订阅他们的event_type
事件,最后整体的返回是一个rxjs
流。
设计思路
Formily1.x
本质是状态机
表单由Form
对象维护,表单字段由Field
对象维护(每个字段都对应一个独立的 Field 对象
),它们的状态都由各自内部维护。表单和字段可以看成一个个节点,由 FormGraph
对象来管理。FormHeart
对象管理表单的整个生命周期,包含表单的初始化、字段状态的变动等生命周期事件,并将这些事件通知到其他对象
Formily1.x 状态机组成:
Subscribable
:实现发布/订阅功能的类,同时也是Model
、FormGraph
和FormHeart
的基础类Model(Form、Field)
:状态管理类,提供了对状态的基本操作,包括改变状态、获取状态等。注意 :Model
类只是提供了对状态的基本操作,实际的状态对象由其中的factory
属性实现,Form
类由Model
类结合FormStateFactory
组成,Field
类由Model
类结合FieldStateFactory
组成。Model
类继承自Subscribable
类,具有发布/订阅能力FormGraph
:节点管理的类。在Formily
中,表单对象对应Form
类的实例,表单字段对应Field
类的实例,Form
和Field
的实例都可以看成是一个个独立的节点,而FormGraph
就是用来管理这些节点的。它提供包括遍历、查找节点等操作,这里每个节点都以他们的路径作为唯一的标识,并且通过refrences
属性来管理节点之间的父子关系。同时继承自Subscribable
类,具有发布/订阅能力FormHeart(FormLifeCycle)
:生命周期管理类,通过lifecycle
属性来管理不同的生命周期。生命周期是Formily
中实现表单联动逻辑的核心能力。同时继承自Subscribable
类,具有发布/订阅能力FormValidator
:实现字段值校验的类
Formily1.x中的数据流是怎么做更新的?
根据分布式状态管理框架 react-eva
Formily1.x
出于性能的考虑,摒弃了 react
中的数据驱动理念,而是使用 API
的方式来进行组件之间的通信。这样的好处是可以将更新精准的局限在某个组件,而不会从根节点往下全量更新
可以把表单看成一个个的节点,就是这样~~~
我们的节点树其实就是一颗状态树,同时,它也是一颗扁平的状态树,它主要由 FormState/FieldState/VirtualFieldState
所构成
表单初始化过程
- 调用
createEva
来创建actions
和effects
,添加effects
里面的生命周期对象做订阅。 - 调用
createForm
来创建表单对象。初始化Graph、Heart、Form
和validate
对象。在生命周期Heart
类里面添加两个订阅事件(全部的生命周期和onFormWillInit
) - 调用
registerField
给每个字段注册field
。每个表单控件都对应一个字段对象。然后初始化这个field
对象,给每一个都添加钩子函数,比如setFieldState
等。也给每个字段订阅了onFieldChange
这个生命周期。 - 调用
createMutator
给每个字段创建一个操作对象mutator
,每次字段进行变更,可以实现调用mutator.validate
进行校验 - 初始化结束,触发
onFormWillInit
,也就是调用implementActions
来将所有API
挂载到actions
上
状态管理过程
- 用户修改表单值,对于一个field字段来说,值都是保存在 field 对象中,同时也保存在 form 对象中。
- 在表单被修改的时候,会调用mutator对象提供的方法(blur,change,focus...)修改表单状态
举例:mutator。change->field。setState->同步变更到Form(setFormValuesIn)->调用监听函数mutator。validate->设置错误信息到field->同步错误信息到Form。
联动逻辑的时候过程
- 初始化的时候把effects里面的生命周期监听事件放入 subscribe。
- 表单发布任何生命周期都会调用heart类里面的生命周期监听事件
- 在handle1函数内部会调用 dispatch 函数发布该类型的生命周期事件
- 然后去effects中添加的生命周期钩子函数中(即 createEva 闭包中的 subscribes 变量保存的生命周期钩子函数)寻找对应的监听函数并运行。这就是联动逻辑的运行机制
小结
-
Formily1.X 初衷是大而全,对于场景的预设考虑比较完备,但是我感觉它还是比较适合高复杂度,表单联动多,有比较极端的性能要求的表单场景
-
如果只是简单场景确实没必要,因为Antd 4.x 已经摆脱了全量渲染,如果能合理地利用 dependencies / shouldUpdate 其实性能表现表现上已经足够好。
-
如果你的场景特别复杂,嵌套与联动的逻辑特别多,利用 Formily1.X 还是能够有效地帮助你收敛逻辑,降低心智负担的。
-
缺点是用户文档不够清晰, 很多地方感觉没有介绍清楚, 诸多细节并没有提到, 需要自己去摸索
-
这次引入的新概念比较多,是一次突破,比如schema、路径系统、path联动,加上文档非常简陋导致上手成本比较高,但是在第三方针对表单而言的大型库,确实做到了创新、精炼。但是实践的过程中总有风险,所以Formily2.x也出来了。
5. Formily2.X
已经讲到这里了,独立渲染已经不是什么亮点了我也就不演示了,Formily
在上面的性能已经非常突出了,那我们来看看2到底做了什么?
Formily2对比1.x解决了什么?
- 性能还是不够好 (用了
immer
) - 包体积太大(
rxjs,styled-components
等) - 设计不够优雅
- 内部设计不够完善
Formily2.X
相比于 Formily1.X
,差别非常大,存在大量 Break Change。V1 和 V2 是无法做到平滑升级的,Formily2.X
不想再做脏检查,1.X基于immerjs
迭代了很多版,一开始的版本脏检查次数非常多,一直优化到至今,还是有一些脏检查不可避免,因为它根本没有像Mobx
一样的依赖追踪机制,所以放弃immer
直接自己造一个Reactive
推到重来。
因为用过Formily1.X
的都会知道,文档非常简略,上手成本比较高,算是第一次大型表单解决方案的一个实验,Formily2.X
的项目初衷就是为了降低大家的学习成本,当然Formily2.X
也有自己的缺点,这个我们稍后会提到
Formily2.x整体架构
示例
js
import React, { useState, useEffect } from 'react'
import { createForm } from '@formily/core'
import { createSchemaField } from '@formily/react'
import { Card, Spin } from 'antd'
const form = createForm({
validateFirst: true,
})
const SchemaField = createSchemaField({
components: {
FormItem,
FormGrid,
FormLayout,
Input,
DatePicker,
Cascader,
Select,
ArrayItems,
Editable,
},
scope: {},
})
export default () => {
const [loading, setLoading] = useState(true)
useEffect(() => {
setTimeout(() => {
form.setInitialValues({
contacts: [
{ name: '张三' },
{ name: '李四' },
],
})
setLoading(false)
}, 2000)
}, [])
return (
<div
style={{
display: 'flex',
justifyContent: 'center',
background: '#eee',
padding: '40px 0',
}}
>
<Card title="编辑用户" style={{ width: 620 }}>
<Spin spinning={loading}>
<Form
form={form}
labelCol={5}
wrapperCol={16}
onAutoSubmit={console.log}
>
<SchemaField>
<SchemaField.Array
name="contacts"
title="联系人信息"
required
x-decorator="FormItem"
x-component="ArrayItems"
>
<SchemaField.Object x-component="ArrayItems.Item">
<SchemaField.Void
x-decorator="FormItem"
x-component="ArrayItems.SortHandle"
/>
<SchemaField.Void
name="popover"
title="维护联系人信息"
x-decorator="Editable.Popover"
x-component="FormLayout"
x-component-props={{
layout: 'vertical',
}}
x-reactions={[
{
fulfill: {
schema: {
title: '{{$self.query(".name").value() }}',
},
},
},
]}
>
<SchemaField.String
name="name"
required
title="姓名"
x-decorator="FormItem"
x-component="Input"
x-component-props={{
style: {
width: 300,
},
}}
/>
</SchemaField.Void>
<SchemaField.Void
x-decorator="FormItem"
x-component="ArrayItems.Remove"
/>
</SchemaField.Object>
<SchemaField.Void
x-component="ArrayItems.Addition"
title="新增联系人"
/>
</SchemaField.Array>
</SchemaField>
<FormButtonGroup.FormItem>
<Submit block size="large">
提交
</Submit>
</FormButtonGroup.FormItem>
</Form>
</Spin>
</Card>
</div>
)
}
我还是举了个Array
的例子,我感觉对于嵌套的表单,这样能更大程度的体现出它的语义化以及精简度和上手成本.可以看出这个写法和Formily1.X
完全不同,Formily2.X
所有组件几乎都做了重写,无法平滑升级.
设计思路
小结
因为现前端主流框架都是使用数据驱动视图的改变,根据数据驱动的方式我们可以将这些主流框架划分为两类,一类就是以React
为代表的 immutable
数据+纯函数模式,另一类就是 Vue 为代表的响应式数据。再以状态管理库为例,redux
使用了 immutable
数据,mobx
使用了响应式数据。Formily2.X
的性能优化之旅目前而言基本上可以说是已经快到极限了,reactive
自己造轮子实现了数据的响应式。由于之前有Formily1.X
的实践经验,2这块整个依赖关系治理了之后,整体体积减少了很多,同时可控性与稳定性也大大提升,但是Formily2.X
也是有一部份自己缺点存在,比如学习成本挺高,因为用户需要理解Formily2.X
的分层架构,每一层的API都挺多的,需要花挺长的时间慢慢学习、存在一些历史包袱API、性能还是存在提升空间.
4. 总结
以上我总结了一下不同的第三方表单的设计思路, 可以看到 ,每个第三方表单的出现都为了在当时解决某一点问题,慢慢的更新成最完善的版本,比如rc-form
设计了一个HOC
减少数据同步的重复机械操作,但是性能跟不上,所以有了react-hook-form
和rc-field-form
来解决性能的问题,他们两个从官方给出的受控和非受控方案各自出发解决了这个性能的问题,但又各自有不同的缺点,react-hook-form
在表单输入时的性能几乎可以用完美来概括,但是遇到了校验和嵌套渲染,又得触发一个全量,rc-field-form
在嵌套表单的表现也不是那么完美,当非叶子结点渲染时,子孙也会受到影响,为了解决这个问题Formily1.X
又来了,它的渲染十分精确,而且还带有了Array
表单的优雅解决方案,可以说是当时最全能的一个第三方表单,但是实践的路上有风险,因为包比较大依赖太重,被很多业务直接pass
掉,这算一个很大的痛点了,既然性能我们追求的这么完美了,Fomily2.x
从包体积、动态渲染能力、联动方案都做了全新的处理,可以说性能极高包体积也减小.可以看到历史的洪流总是相似,你们看到这个演变过程有什么思考吗?
大家如果有建议或者不同的想法,欢迎留言 也可以期待一下我们的下一篇文章~~