前言
React 15.x 升 React 16.x 是一次内部重构,对于使用者来说,原来的使用方式仍然可用,额外加了新的功能;而 Antd 3.x 升 Antd 4.x, 在我的认知范围里,可以称作是飞 (po) 跃(huai)性的重构, 因为以前很多写法都不兼容了,组件代码重构,使用者的代码也得重构。但这次重构解决了 3.x 的很多问题,比如:
- 由于 Icon 无法按需加载导致打包体积过大;
- 由于 Form 表单项变化会造成其他表单项全量渲染,大表单会有性能问题;
- 时间库 moment 包体积太大。
说这么多,还是直接来张图吧,我个人项目的打包体积变化:Antd 3.x
VS Antd 4.x
升 4.x 之后,gzip 少了 150kb,也就是包大小少了 500 多 kb,这不香么。
关于升级
我和我的小组,为了用更爽的方式来开发迭代,针对于 antd 的 Form 和 Table 等组件做了一些简单的二次封装,形成了组件库 antd-doddle。虽然 Antd 4.x 推出快小半年了,受疫情影响,今年业务迭代比较缓慢,没有新系统,也觉得暂时没必要去重构业务代码,所以一直只关注不动手。最近比较闲,组件库针对 Antd 4.0 做了适应性重构,作为一个胶水层,最大程度的去磨平 4.0 版本 Form 这种破坏性变动,减少以后业务代码升级 4.x 版本的调整量。
Antd 4.x 到底做了哪些变化,在官方文档可以看到。
这篇文章主要讲针对于 4.x Form 的变化,我重构组件库的思路。
Antd-doddle 2.x 文档地址, 支持 4.x: http://doc.closertb.site, 首次加载较慢,请耐心等候。
Antd-doddle 1.x 文档地址, 支持 3.x: http://static.closertb.site, 首次加载较慢,请耐心等候。
试用项目 Git 地址
项目在线试用地址, 请勿乱造
FormGroup 重构思路
Form 组件变化
4.x 中除了 Icon,最大的更改就在于 Form,我自己感受到的变化是:
- 舍去了 Form.create 高阶组件包裹表单的写法,而改采用 hooks 或 ref 的方式去操作 form 实例;
- 以前每个表单组件的数据绑定是通过 getFieldDecorator 这个装饰方法完成,现在改由 FormItem 来完成;
- 初始 values 设置,以前是通过 getFieldDecorator 设置,并且是动态的,即 value 改变,表单值跟随改变。现在是在 Form 最外层设置,但是以 defaultValue 形式设置,即 value 改变,表单值不跟随变化;
-
最大的改变
就是增量式更新,3.x 版本,任意表单项改变,都会造成 Form.create 包裹的全部表单项重新 render,这是非常大的性能消耗;而 4.x 之后,任意表单项改变,只有设置了 shouldUpdate 属性的表单项有可能执行 render,类似于 React 16 新增的componentShouldUpdate
;
根据上面的变化点,由外向内层层剖析,针对性的做重构;
FormGroup 的变化
由于以前 FormGroup 组件,除了收集 form 方法和公共配置,也作为一个标识,接管了组件内部的渲染层;3.x 版本其 form 实例由 Form.create,即业务代码提供;4.x 与其相似,只不过是通过 hooks 生成 form 实例。
变化点主要在于 4.x 版本 Form 要提供 initialValues 的设置,且这是一个 defaultValue 的设置,所以我们需要拓展,让其支持 values 为异步数据时,表单项的值能跟随其改变, 其原理很简单,监听 value 的变化,并重置表单数据,实现代码如下:
// 伪代码,只涉及相关改动
const FormGroup = (props, ref) => {const { formItemLayout = layout, children, datas = {}, ...others } = props;
// 兼容了非 hooks 组件调用的写法,内部再声明一个 ref,以备用;const insideRef = useRef();
const _ref = ref || insideRef;
const formProps = {initialValues: {}, // why
...formItemLayout,
...others
};
// 如果 datas 值变化,重置表单的值
useEffect(() => {const [data, apiStr] = Type.isEmpty(datas) ? [undefined, 'resetFields'] : [datas, 'setFieldsValue'];
// 函数式组件采用 form 操作;if (props.form) {props.form[apiStr](data);
return;
}
// 如果是类组件,才采用 ref 示例更新组件
if (typeof _ref === 'object') {_ref.current[apiStr](data);
}
}, [datas]);
return (<Form {...formProps} ref={_ref}>
{deepMap(children, extendProps, mapFields)}
</Form>);
};
上面有句代码 initialValues: {},
会让人困惑, 为什么没有赋值为 datas 呢;这个又是 antd 的一个隐藏知识点,举个例子:
form.setFieldsValue({name: 'antd', age: 18});
// 后面想清空
form.setFieldsValue({});
// 最后发现上面清空根本没生效,原因可以自己想想
所以当我们想做一些需求,比如先编辑一个表单,表单中有数据了;但没做操作关闭了,然后点了新增按钮,传了一个空对象,这事就发现 bug 了, 上一次编辑的数据还在,除了这个,还有一些其他的业务场景会用到,在 antd 中也有一个类似的 issue:
在表单里有很多元素且拥有 initialValue 的时候, 如何简单的清空表单
所以在这个组件设计上,就将 initialValues
默认置空数据,然后设置的数据采用 setFieldsValue
来重置。如果想清空表单,直接传入一个空数据,被组件检测到后,内部调用 resetFields
来实现。快夸一下天才的我。
FormRender 的变化
相比于 FormGroup 的变化,子组件 FormRender 相比较就变化小一点,主要在适应第二点变动,用代码的方式更直观:
// 3.x
const render = renderType[type];
content = (
<FormItem
label={name}
rules={gerateRule(required, pholder, rules)}
{...formProps}
>
{getFieldDecorator(key, {
initialValue: data,
rules: gerateRule(required, pholder, rules)
})(render({ field: common, name, enums: selectEnums, containerName}))}
</FormItem>);
重构之后
// 4.x
const render = renderType[type];
content = (
<FormItem
name={key}
label={name}
rules={gerateRule(required, pholder, rules)}
{...formProps}
>
{render({ field: common, name, enums: selectEnums, containerName})}
</FormItem>);
联动表单的实现变化
主要实现三种联动,根据其他表单项的变化,来改变关联表单项
- 是否渲染或改变渲染方式;
- 是否禁用;
- 校验规则
由于以前每次表单项变化,都会引起其他表单项 render,所以可以暴力的通过 FormGroup 增加数据层 (useRef) 与监 (bao) 听(guo)每个表单项的 onChange 来实现;
新的 Form 新增了 onFormChange 回调来支持增量式数据收集,但第四点变动,让老的方案 GG;表单项的联动需要依赖 shouldUpdate 来实现,这也是官方推荐的方案;
其本质是,设置了 shouldUpdate 属性的 FormItem,其仅仅作为一个容器,这个容器监听了类似 onFormChange 这种事件,然后根据 shouldUpdate 来判断是否需要重新渲染容器内的子元素,子元素渲染实现是一个应用 React renderPrrops 的设计模式;
所以联动方案似乎变得更简单了, 就多一层 FormItem 包裹,看部分代码实现:
const render = renderType[type];
content = shouldUpdate ? (<FormItem shouldUpdate={shouldUpdate} noStyle>
{form => {const datas = form.getFieldsValue();
const require = typeof required === 'function' ? required(initData, datas) : required;
const disabled = typeof disableTemp === 'function'
? disableTemp(initData, datas) : disableTemp;
return finalEnable(initData, datas) ?
(<FormItem
key={key}
name={key}
label={name}
dependencies={dependencies}
rules={gerateRule(require, pholder, rules)}
{...formProps}
{...otherFormPrrops}
>
{render({ field: Object.assign(common, { disabled}), name, enums: selectEnums, containerName })}
</FormItem>) : selfRender(datas, form)}
}
</FormItem>):/* 非联动实现 */
其他
除了上面这些变化,其实还有很多边边角角的变化,比如:
- 搜索框组件其实也是依赖 FormGroup 来实现的,所以内部也做了一些小调整,但业务代码就完全不需要做改动;
- 以前支持了 RangePicker 的数据自动组装,但由于 4.x 支持 DayJs 和 Moment 时间库切换,加上 initValues 的提前设置,这个功能暂时就取消了;
- 样式文件的按需加载;
还有,还有一些未考虑好的的新增特性。
使用对比
实现一个小的编辑框,类似下面这样:
重构前的代码
import React from 'react';
import {FormGroup} from 'antd-doddle';
import {editFields} from './fields';
const {FormRender} = FormGroup;
function Edit({id, form, data}) {const { getFieldDecorator} = form;
return (<FormGroup getFieldDecorator={getFieldDecorator} required>
{editFields.map(field => <FormRender key={field.key} field={field} data={data} />)}
</FormGroup>
);
}
export default FormGroup.create()(Edit);
重构之后
import React from 'react';
import {FormGroup} from 'antd-doddle';
import {editFields} from './fields';
const {FormRender} = FormGroup;
function Edit({id, data, ...others}) {const [form] = FormGroup.useForm();
return (<FormGroup required form={form} datas={data}>
{editFields.map(field => <FormRender key={field.key} field={field} />)}
</FormGroup>
);
}
export default Edit
不仔细看,是不是不易察觉到变化
重构感受
因为用的还不像 3.x 版本那么熟练,很多特性还没用上,所以先重构一个简易版本,来完成日常场景,后面再慢慢迭代。
如果感兴趣,可以 fork 项目或查看项目文档
文章原地址