Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

组件库重构,支持antd 4.0 #54

Open
closertb opened this issue Jun 29, 2020 · 0 comments
Open

组件库重构,支持antd 4.0 #54

closertb opened this issue Jun 29, 2020 · 0 comments
Labels
FE Engineering 前端工程化相关 JS 原生JS相关

Comments

@closertb
Copy link
Owner

closertb commented Jun 29, 2020

前言

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

20200629171830

20200627171745

升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,我自己感受到的变化是:

  1. 舍去了Form.create高阶组件包裹表单的写法,而改采用hooks或ref的方式去操作form实例;
  2. 以前每个表单组件的数据绑定是通过getFieldDecorator这个装饰方法完成,现在改由FormItem来完成;
  3. 初始values设置,以前是通过getFieldDecorator设置,并且是动态的,即value改变,表单值跟随改变。现在是在Form最外层设置,但是以defaultValue形式设置,即value改变,表单值不跟随变化;
  4. 最大的改变就是增量式更新,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来实现,这也是官方推荐的方案;

20200629215947

其本质是,设置了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的提前设置,这个功能暂时就取消了;
  • 样式文件的按需加载;

还有,还有一些未考虑好的的新增特性。

使用对比

实现一个小的编辑框,类似下面这样:

20200629230026

重构前的代码

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项目或查看项目文档

@closertb closertb added JS 原生JS相关 FE Engineering 前端工程化相关 labels Jun 29, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
FE Engineering 前端工程化相关 JS 原生JS相关
Projects
None yet
Development

No branches or pull requests

1 participant