組件庫重構,支持antd 4.x

前言

React 15.x 升 React 16.x 是一次內部重構,對於使用者來講,原來的使用方式仍然可用,額外加了新的功能;而Antd 3.x 升 Antd 4.x, 在個人認知範圍裏,能夠稱做是飛(po)躍(huai)性的重構, 由於之前不少寫法都不兼容了,組件代碼重構,使用者的代碼也得重構。但此次重構解決了3.x的不少問題,好比:react

  • 因爲Icon沒法按需加載致使打包體積過大;
  • 因爲Form表單項變化會形成其餘表單項全量渲染,大表單會有性能問題;
  • 時間庫moment包體積太大。

說這麼多,仍是直接來張圖吧,我我的項目的打包體積變化:Antd 3.x VS Antd 4.x git

20200629171830

20200627171745

升4.x以後,gzip少了150kb,也就是包大小少了500多kb,這不香麼。

關於升級

我和個人小組,爲了用更爽的方式來開發迭代,針對於antd的Form和Table等組件作了一些簡單的二次封裝,造成了組件庫antd-doddle。雖然Antd 4.x推出快小半年了,受疫情影響,今年業務迭代比較緩慢,沒有新系統,也以爲暫時不必去重構業務代碼,因此一直只關注不動手。最近比較閒,組件庫針對Antd 4.0作了適應性重構,做爲一個膠水層,最大程度的去磨平4.0版本Form這種破壞性變更,減小之後業務代碼升級4.x版本的調整量。github

Antd 4.x到底作了哪些變化,在官方文檔能夠看到。spring

這篇文章主要講針對於4.x Form的變化,我重構組件庫的思路。npm

Antd-doddle 2.x文檔地址, 支持4.x: http://doc.closertb.site, 首次加載較慢,請耐心等候。設計模式

Antd-doddle 1.x文檔地址, 支持3.x: http://static.closertb.site, 首次加載較慢,請耐心等候。api

試用項目Git 地址antd

項目在線試用地址, 請勿亂造koa

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項目或查看項目文檔

文章原地址

相關文章
相關標籤/搜索