记录一次 antd 高阶组件封装以及痛失 pr 的经历

448 阅读3分钟

前情提要

在浏览掘金的时候,我看到了一篇关于实现类型提示完整的高阶组件的文章 , React Hook + Typescript 实现一个类型提示完整的高阶组件(HOC)。于是我便想把项目中的 Form 组件也用高阶组件封装一下。然而,在封装过程中,我发现了一个 antd 组件的 bug。但是值得称赞的是,提了 issue 以后,不到半小时就有人提供了 pr 修复了这个问题。。不得不说,antd 社区的响应速度值得称赞

封装过程

input 组件为例子,我想达到的效果如下

import { FormInput } from 'components/Form' 

<FormInput
  label="姓名"
  name="name"
  rules={[{ required: true, message: '请输入姓名'}]}
  bordered={false}
/>

作为一个基于 typescript 的高阶组件,类型提示是必不可少的。antd 组件给我们提供了像 FormItemProps, InputPropsSelectProps 等等的类型。因此,我们可以直接使用这些类型来给我们的组件定义类型

下面是核心的高阶组件代码:

const WithFormItem = <T,>(Component: React.ComponentType<T>) => {
  const FormItem = (props: FormItemProps & T) => {
    const {
      colon,
      dependencies,
      extra,
      getValueFromEvent,
      getValueProps,
      hasFeedback,
      help,
      hidden,
      htmlFor,
      label,
      labelAlign,
      labelCol,
      messageVariables,
      name,
      normalize,
      noStyle,
      preserve,
      required,
      rules,
      shouldUpdate,
      tooltip,
      trigger,
      validateFirst,
      validateStatus,
      validateTrigger,
      valuePropName,
      wrapperCol,
      ...otherProps
    } = props;

    const formItemProps: FormItemProps = {
      colon,
      dependencies,
      extra,
      getValueFromEvent,
      getValueProps,
      hasFeedback,
      help,
      hidden,
      htmlFor,
      label,
      labelAlign,
      labelCol,
      messageVariables,
      name,
      normalize,
      noStyle,
      preserve,
      required,
      rules,
      shouldUpdate,
      tooltip,
      trigger,
      validateFirst,
      validateStatus,
      validateTrigger,
      valuePropName,
      wrapperCol,
    };

    return (
      <Form.Item {...formItemProps}>
        <Component {...(otherProps as T & FormItemProps)} />
      </Form.Item>
    );
  };
  return React.memo(FormItem);
};

主要问题就是区分 FormItemProps 和其他 Componentprops,搜索了半天也没有特别好的解决方法 , 最后决定直接穷举出 FormItemProps 中的所有属性,然后在将剩余的属性传递给 Component,这样就可以实现类型提示了。

最后我们使用这个高阶组件来简单封装一下一些自己项目的通用代码

import {
  FormItemProps,
  Input,
  Form,
  InputProps,
  Select,
  SelectProps,
  DatePickerProps,
  DatePicker,
  InputNumber,
  InputNumberProps,
} from 'antd';
import { RangePickerProps } from 'antd/es/date-picker';
import { TextAreaProps } from 'antd/es/input';

export const FormItem = {
  Input: WithFormItem<InputProps>(Input),
  Select: WithFormItem<SelectProps>(Select),
  DatePicker: WithFormItem<DatePickerProps>(DatePicker),
  TextArea: WithFormItem<TextAreaProps>(Input.TextArea),
  InputNumber: WithFormItem<InputNumberProps>(InputNumber),
  RangePicker: WithFormItem<RangePickerProps>(DatePicker.RangePicker),
};

export const FormInput = (props: FormItemProps & InputProps) => {
  return <FormItem.Input allowClear placeholder={`请输入${props.label}`} {...props} />;
};

export const FormSelect = (props: FormItemProps & SelectProps) => {
  return (
    <FormItem.Select
      allowClear
      placeholder={`请选择${props.label}`}
      getPopupContainer={(triggerNode) => triggerNode.parentNode}
      {...props}
    />
  );
};

export const FormDatePicker = (props: FormItemProps & DatePickerProps) => {
  return <FormItem.DatePicker allowClear placeholder={`请选择${props.label}`} {...props} />;
};

export const FormRangePicker = (props: FormItemProps & RangePickerProps) => {
  return <FormItem.RangePicker allowClear allowEmpty={[true, true]} {...props} />;
};

export const FormInputNumber = (props: FormItemProps & InputNumberProps) => {
  return <FormItem.InputNumber placeholder={`请输入${props?.label}`} {...props} />;
};

export const FormTextArea = (props: FormItemProps & TextAreaProps) => {
  return <FormItem.TextArea allowClear rows={4} placeholder={`请输入${props?.label}`} {...props} />;
};

bug 的出现

就在我美滋滋的准备在项目中使用这个封装好的组件的时候,我发现了一个 bugbug 的复现特别简单

export default function App() {
  return (
    <Form>
      <FormInput
        name="username"
        label="Name"
        rules={[{ required: true, message: 'Please input your name' }]}
        placeholder="Please input your name"
      />
      <Form.Item
        name="username"
        label="Name"
        rules={[{ required: true, message: 'Please input your name' }]}
      >
        <Input placeholder="Please input your name" />
      </Form.Item>
    </Form>
  );
}

基本可以说相同的代码,但是封装以后必选的样式就会丢失

image (1).png

于是我就去 antdgithub 仓库提了一个 issue,顺便体验了一波 stackblitz。在提 issue 的过程中,我逐渐发现了 bug 的问题所在,下面也有社区的大佬们直接贴出了具体代码

juejin.jpg

可以看出来这个 bug 其实挺简单,就是后面的 props 把前面 isRequired 的值给覆盖了。所以我想着是不是可以混个 pr,刚 fork 完代码,发现已经有人提交了 pr,还加上测试用例已经在跑 ci 了。惊了,antd 的社区真的很活跃,而且很多人都很热心,我也很佩服。

总结

通过这次封装组件的经历,我不仅学会了如何使用 React HooksTypeScript 编写一个具有类型提示的高阶组件,还发现了一个 antdbug,并提了一个 issue。虽然没有成功混上 pr 有点可惜,但是下次如果再有这样的机会我肯定会先自己尝试一下,如果还是不行,再去提 issue