从antd-design看如何实现一个Button组件,它需要哪些功能

570 阅读5分钟

「这是我参与2022首次更文挑战的第1天,活动详情查看:2022首次更文挑战」。

button按钮是我们前端在业务开发过程中必不可少的一部分,在多处都有使用,通常在页面上或弹框用来某些操作,其实就是一个操作命令,用户点击后出发命令,来执行相应的业务逻辑,那我们如何来封装一个Button组件呢?

antd-design中为我们提供了五种按钮,包括主按钮、默认按钮、虚线按钮、文本按钮以及链接按钮用于不同的操作,不同的按钮类型有不同的操作语义,再配合四种不同的状态属性,更加直观的指引用户执行某些操作,更加清晰。加下来我们看看antd-design中的Button组件是怎么实现的,进行简单的分析。

组件属性

// 我们封装的 Button组件 所需要的 基本的props
export interface BaseButtonProps {
  type?: ButtonType; // 按钮类型'default', 'primary', 'dashed', 'link', 'text'
  icon?: React.ReactNode; // 是否显示图标、图标节点
  shape?: ButtonShape; // 形状
  size?: SizeType; // 大小
  loading?: boolean | { delay?: number }; //  是否展示 loading、或者延时几秒展示
  prefixCls?: string; // 类名前缀
  className?: string; // 传入的类名
  ghost?: boolean; // 幽灵属性,使按钮背景透明
  danger?: boolean; // 是否是危险按钮
  block?: boolean; // 将按钮宽度调整为其父宽度的选项
  children?: React.ReactNode; // 包裹子节点
}

// 按钮链接的属性,a链接,用于跳转的相关属性
export type AnchorButtonProps = {
  href: string;
  target?: string;
  onClick?: React.MouseEventHandler<HTMLElement>;
} & BaseButtonProps &
  Omit<React.AnchorHTMLAttributes<any>, 'type' | 'onClick'>;
  
 // button 节点原始的属性
export type NativeButtonProps = {
  htmlType?: ButtonHTMLType;
  onClick?: React.MouseEventHandler<HTMLElement>;
} & BaseButtonProps &
  Omit<React.ButtonHTMLAttributes<any>, 'type' | 'onClick'>;
  
// 我们Button组件的所有的props
export type ButtonProps = Partial<AnchorButtonProps & NativeButtonProps>;

我们知道Button组件分为普通按钮和链接按钮,主要用原生的button标签和a标签实现,所以当我们拿到开发者传入的属性后,对props进行区分,是返回button标签和a标签就好了,然后对传入的属性进行处理,传入对应的标签就实现了,就是简单的思路。

  1. 定义了一个InternalButton组件,接受参数props, ref,然后导出const Button = React.forwardRef(InternalButton)React.forwardRef主要用于转发ref
  2. InternalButton组件对传入的props进行了解构,以及默认值赋值,同时由于支持loading展示,所以创建了一个为innerLoadingstate来控制显隐
  3. useEffect中来修改innerLoading的值,然后来触发组件更新,是否展示loading,熟悉判断用户传入的loading属性的值,是布尔类型还是对象类型(直接显示,还是需要延时),然后赋值给变量loadingOrDelay:true/false或者是数字(延时时间),判断loadingOrDelay的类型,如果是数字就设置定时器,延时更改innerLoading的值(就是延时显示),否则直接更改innerLoading的值。这是loading部分
  4. 然后他写了一个handleClick方法,用户点击的时候调用,在该方法中调用传入的onClick(e)方法,如果是禁用或者loading状态,就取消事件的默认行为
  5. 接着对classes类名进行处理,根据传入的props组合不同的类名
  6. 对icon进行处理,判断显示那个icon,是传入的还是默认的,然后innerLoading判断是否需要显示
  7. 对children进行处理
  8. 判断应该返回button还是a标签,将属性传入,就好了

大体的流程就是这样,但肯定不止这么多,antd-design中还做了其他处理,如数据校验,子节点处理等等更加严谨的处理,但是一个Button组件的主要部分就是这么多,也欢迎大家聊聊,小弟只能看懂这么多了

// Button 组件
const InternalButton = (props, ref) => {
  const {
    loading = false, // 是否展示 loading、或者延时几秒展示
    prefixCls: customizePrefixCls,
    type = 'default', //  'default', 'primary', 'ghost', 'dashed', 'link', 'text'
    danger, // 是否是危险按钮
    shape = 'default', // 形状
    size: customizeSize, // 大小
    className, // 传入的类名
    children, // 包裹子节点
    icon, // 按钮图标
    ghost = false, // 幽灵属性,使按钮背景透明
    block = false, // 将按钮宽度调整为其父宽度的选项
    /** If we extract items here, we don't need use omit.js */
    // React does not recognize the `htmlType` prop on a DOM element. Here we pick it out of `rest`.
    htmlType = 'button' as ButtonProps['htmlType'], //   设置 button 原生的 type 值
    ...rest // 其他属性
  } = props;

  // 是否要展示loading
  const [innerLoading, setLoading] = React.useState<Loading>(!!loading);
 
  // loading是对象时,取 delay 属性或true,否则就是转化为boolean类型
  const loadingOrDelay: Loading =
    typeof loading === 'object' && loading.delay ? loading.delay || true : !!loading;

  React.useEffect(() => {
    let delayTimer: number | null = null;

    // 是数字,说明传入的 loading 是对象,存在 loading.delay,设置定时器显示loading
    if (typeof loadingOrDelay === 'number') {
      delayTimer = window.setTimeout(() => {
        delayTimer = null;
        setLoading(loadingOrDelay);
      }, loadingOrDelay);
    } else {
      setLoading(loadingOrDelay);
    }

    return () => {
      if (delayTimer) {
        window.clearTimeout(delayTimer);
        delayTimer = null;
      }
    };
  }, [loadingOrDelay]);

  React.useEffect(fixTwoCNChar, [buttonRef]);

  const handleClick = (e: React.MouseEvent<HTMLButtonElement | HTMLAnchorElement, MouseEvent>) => {
    const { onClick, disabled } = props;

    // 禁止默认行为
    if (innerLoading || disabled) {
      e.preventDefault();
      return;
    }
    // 调用 onClick 事件
    onClick(e);
  };

  const sizeClassNameMap = { large: 'lg', small: 'sm', middle: undefined };
  // customizeSize 是传入的size; size是上下文中的
  const sizeFullname = customizeSize || size; // 不传的话应该是 middle 或者不存在
  // 不穿的话应该是 undefined 或者 ‘’
  const sizeCls = sizeFullname ? sizeClassNameMap[sizeFullname] || '' : '';

  // 按钮类型,loading时显示loading按钮,其他时候显示传入的
  const iconType = innerLoading ? 'loading' : icon;

  // 类名组合
  const classes = classNames(
    prefixCls,
    {
      [`${prefixCls}-${shape}`]: shape !== 'default' && shape, //不同形状不同类名
      [`${prefixCls}-${type}`]: type,
      [`${prefixCls}-${sizeCls}`]: sizeCls,
      [`${prefixCls}-icon-only`]: !children && children !== 0 && !!iconType,
      [`${prefixCls}-background-ghost`]: ghost && !isUnborderedButtonType(type),
      [`${prefixCls}-loading`]: innerLoading,
      [`${prefixCls}-two-chinese-chars`]: hasTwoCNChar && autoInsertSpace,
      [`${prefixCls}-block`]: block,
      [`${prefixCls}-dangerous`]: !!danger,
      [`${prefixCls}-rtl`]: direction === 'rtl',
    },
    className,
  );

  // icon 节点,显示传入的图标,或显示 默认的loading
  const iconNode =
    icon && !innerLoading ? (
      icon
    ) : (
      <LoadingIcon existIcon={!!icon} prefixCls={prefixCls} loading={!!innerLoading} />
    );

  // 子节点,在外面使用时 Button 组件包裹的
  const kids =
    children || children === 0
      ? spaceChildren(children, isNeedInserted() && autoInsertSpace)
      : null;

  // ...rest 中未解构的属性
  const linkButtonRestProps = omit(rest as AnchorButtonProps & { navigate: any }, ['navigate']);

  // 有 href 说明是链接按钮
  if (linkButtonRestProps.href !== undefined) {
    return (
      <a {...linkButtonRestProps} className={classes} onClick={handleClick} ref={buttonRef}>
        {iconNode}
        {kids}
      </a>
    );
  }

  // 其他按钮,将处理好的属性传入原生 button节点 就好了
  const buttonNode = (
    <button
      {...(rest as NativeButtonProps)}
      type={htmlType}
      className={classes}
      onClick={handleClick}
      ref={buttonRef}
    >
      {iconNode}
      {kids}
    </button>
  );

  if (isUnborderedButtonType(type)) {
    return buttonNode;
  }

  return <Wave disabled={!!innerLoading}>{buttonNode}</Wave>;
};

// React.forwardRef 用来转发 refs,用户可能给我们的 Button组件 绑定 ref
const Button = React.forwardRef<unknown, ButtonProps>(InternalButton) as CompoundedComponent;

export default Button;