前言

在前端开发过程中最常用的组件非button莫属了。

需要支持的功能

属性 说明 类型 默认值 版本
disabled 按钮失效状态 boolean false
ghost 幽灵属性,使按钮背景透明 boolean false
href 点击跳转的地址,指定此属性 button 的行为和 a 链接一致 string -
htmlType 设置 button 原生的 type 值,可选值请参考 HTML 标准 string button
icon 设置按钮的图标组件 ReactNode -
loading 设置按钮载入状态 boolean \ { delay: number } false
shape 设置按钮形状,可选值为 circleround 或者不设 string -
size 设置按钮大小 large \ middle \ small
target 相当于 a 链接的 target 属性,href 存在时生效 string -
type 设置按钮类型,可选值为 primary dashed link 或者不设 string -
onClick 点击按钮时的回调 (event) => void -
block 将按钮宽度调整为其父宽度的选项 boolean false
danger 设置危险按钮 boolean false

结构

antd4.0 button主要有3个tsx文件组成

button.tsx

/* eslint-disable react/button-has-type */
import * as React from 'react';
import classNames from 'classnames';
import LoadingOutlined from '@ant-design/icons/LoadingOutlined'; // antd 4.0 icon
import omit from 'omit.js';

import Group from './button-group';
// export const configConsumerProps = [
//   'getPopupContainer',
//   'rootPrefixCls',
//   'getPrefixCls',
//   'renderEmpty',
//   'csp',
//   'autoInsertSpaceInButton',
//   'locale',
//   'pageHeader',
// ];
import { ConfigContext, ConfigConsumerProps } from '../config-provider'; // 为组件提供统一的全局化配置
import Wave from '../_util/wave';
import { Omit, tuple } from '../_util/type';
import warning from '../_util/warning';
import SizeContext, { SizeType } from '../config-provider/SizeContext';

const rxTwoCNChar = /^[\u4e00-\u9fa5]{2}$/;
const isTwoCNChar = rxTwoCNChar.test.bind(rxTwoCNChar);
function isString(str: any) {
  return typeof str === 'string';
}

//自动在两个汉字之间插入一个空格。
function insertSpace(child: React.ReactChild, needInserted: boolean) {
  //检查子项是否未定义或为null
  if (child == null) {
    return;
  }
  const SPACE = needInserted ? ' ' : '';
  // strictNullChecks oops.
  if (
    typeof child !== 'string' &&
    typeof child !== 'number' &&
    isString(child.type) &&
    isTwoCNChar(child.props.children)
  ) {
    return React.cloneElement(child, {}, child.props.children.split('').join(SPACE));
  }
  if (typeof child === 'string') {
    if (isTwoCNChar(child)) {
      child = child.split('').join(SPACE);
    }
    return <span>{child}</span>;
  }
  return child;
}

function spaceChildren(children: React.ReactNode, needInserted: boolean) {
  let isPrevChildPure: boolean = false;
  const childList: React.ReactNode[] = [];
  React.Children.forEach(children, child => {
    const type = typeof child;
    const isCurrentChildPure = type === 'string' || type === 'number';
    if (isPrevChildPure && isCurrentChildPure) {
      const lastIndex = childList.length - 1;
      const lastChild = childList[lastIndex];
      childList[lastIndex] = `${lastChild}${child}`;
    } else {
      childList.push(child);
    }

    isPrevChildPure = isCurrentChildPure;
  });

  // Pass to React.Children.map to auto fill key
  //传递给React.Children.map以自动填充键
  return React.Children.map(childList, child =>
    insertSpace(child as React.ReactChild, needInserted),
  );
}

const ButtonTypes = tuple('default', 'primary', 'ghost', 'dashed', 'danger', 'link');
export type ButtonType = typeof ButtonTypes[number];
const ButtonShapes = tuple('circle', 'circle-outline', 'round');
export type ButtonShape = typeof ButtonShapes[number];
const ButtonHTMLTypes = tuple('submit', 'button', 'reset');
export type ButtonHTMLType = typeof ButtonHTMLTypes[number];

export interface BaseButtonProps {
  type?: ButtonType;
  icon?: React.ReactNode;
  shape?: ButtonShape;
  size?: SizeType;
  loading?: boolean | { delay?: number };
  prefixCls?: string;
  className?: string;
  ghost?: boolean;
  danger?: boolean;
  block?: boolean;
  children?: React.ReactNode;
}

// Typescript will make optional not optional if use Pick with union.
// Should change to `AnchorButtonProps | NativeButtonProps` and `any` to `HTMLAnchorElement | HTMLButtonElement` if it fixed.
// ref: https://github.com/ant-design/ant-design/issues/15930
export type AnchorButtonProps = {
  href: string;
  target?: string;
  onClick?: React.MouseEventHandler<HTMLElement>;
} & BaseButtonProps &
  Omit<React.AnchorHTMLAttributes<any>, 'type' | 'onClick'>;

export type NativeButtonProps = {
  htmlType?: ButtonHTMLType;
  onClick?: React.MouseEventHandler<HTMLElement>;
} & BaseButtonProps &
  Omit<React.ButtonHTMLAttributes<any>, 'type' | 'onClick'>;

export type ButtonProps = Partial<AnchorButtonProps & NativeButtonProps>;

interface ButtonState {
  loading?: boolean | { delay?: number };
  hasTwoCNChar: boolean;
}

class Button extends React.Component<ButtonProps, ButtonState> {
  static Group: typeof Group;

  static __ANT_BUTTON = true;

  static contextType = ConfigContext;

  static defaultProps = {
    loading: false,
    ghost: false,
    block: false,
    htmlType: 'button' as ButtonProps['htmlType'],
  };

  private delayTimeout: number;

  private buttonNode: HTMLElement | null;

  constructor(props: ButtonProps) {
    super(props);
    this.state = {
      loading: props.loading,
      hasTwoCNChar: false,
    };
  }

  componentDidMount() {
    this.fixTwoCNChar();
  }

  componentDidUpdate(prevProps: ButtonProps) {
    this.fixTwoCNChar();

    if (prevProps.loading && typeof prevProps.loading !== 'boolean') {
      clearTimeout(this.delayTimeout);
    }

    const { loading } = this.props;
    if (loading && typeof loading !== 'boolean' && loading.delay) {
      this.delayTimeout = window.setTimeout(() => {
        this.setState({ loading });
      }, loading.delay);
    } else if (prevProps.loading !== loading) {
      // eslint-disable-next-line react/no-did-update-set-state
      this.setState({ loading });
    }
  }

  componentWillUnmount() {
    if (this.delayTimeout) {
      clearTimeout(this.delayTimeout);
    }
  }

  saveButtonRef = (node: HTMLElement | null) => {
    this.buttonNode = node;
  };

  handleClick: React.MouseEventHandler<HTMLButtonElement | HTMLAnchorElement> = e => {
    const { loading } = this.state;
    const { onClick } = this.props;
    if (loading) {
      return;
    }
    if (onClick) {
      (onClick as React.MouseEventHandler<HTMLButtonElement | HTMLAnchorElement>)(e);
    }
  };

  fixTwoCNChar() {
    const { autoInsertSpaceInButton }: ConfigConsumerProps = this.context;

    // Fix for HOC usage like <FormatMessage />
    if (!this.buttonNode || autoInsertSpaceInButton === false) {
      return;
    }
    const buttonText = this.buttonNode.textContent;
    if (this.isNeedInserted() && isTwoCNChar(buttonText)) {
      if (!this.state.hasTwoCNChar) {
        this.setState({
          hasTwoCNChar: true,
        });
      }
    } else if (this.state.hasTwoCNChar) {
      this.setState({
        hasTwoCNChar: false,
      });
    }
  }

  isNeedInserted() {
    const { icon, children, type } = this.props;
    return React.Children.count(children) === 1 && !icon && type !== 'link';
  }

  render() {
    const { getPrefixCls, autoInsertSpaceInButton, direction }: ConfigConsumerProps = this.context;

    return (
      <SizeContext.Consumer>
        {size => {
          const {
            prefixCls: customizePrefixCls,
            type,
            danger,
            shape,
            size: customizeSize,
            className,
            children,
            icon,
            ghost,
            block,
            ...rest
          } = this.props;
          const { loading, hasTwoCNChar } = this.state;

          warning(
            !(typeof icon === 'string' && icon.length > 2),
            'Button',
            `\`icon\` is using ReactNode instead of string naming in v4. Please check \`${icon}\` at https://ant.design/components/icon`,
          );

          const prefixCls = getPrefixCls('btn', customizePrefixCls);
          const autoInsertSpace = autoInsertSpaceInButton !== false;

          // large => lg
          // small => sm
          let sizeCls = '';
          switch (customizeSize || size) {
            case 'large':
              sizeCls = 'lg';
              break;
            case 'small':
              sizeCls = 'sm';
              break;
            default:
              break;
          }

          const iconType = loading ? 'loading' : icon;

          const classes = classNames(prefixCls, className, {
            [`${prefixCls}-${type}`]: type,
            [`${prefixCls}-${shape}`]: shape,
            [`${prefixCls}-${sizeCls}`]: sizeCls,
            [`${prefixCls}-icon-only`]: !children && children !== 0 && iconType,
            [`${prefixCls}-loading`]: !!loading,
            [`${prefixCls}-background-ghost`]: ghost,
            [`${prefixCls}-two-chinese-chars`]: hasTwoCNChar && autoInsertSpace,
            [`${prefixCls}-block`]: block,
            [`${prefixCls}-dangerous`]: !!danger,
            [`${prefixCls}-rtl`]: direction === 'rtl',
          });

          const iconNode = loading ? <LoadingOutlined /> : icon || null;
          const kids =
            children || children === 0
              ? spaceChildren(children, this.isNeedInserted() && autoInsertSpace)
              : null;

          const linkButtonRestProps = omit(rest as AnchorButtonProps, ['htmlType', 'loading']);
          if (linkButtonRestProps.href !== undefined) {
            return (
              <a
                {...linkButtonRestProps}
                className={classes}
                onClick={this.handleClick}
                ref={this.saveButtonRef}
              >
                {iconNode}
                {kids}
              </a>
            );
          }

          // React does not recognize the `htmlType` prop on a DOM element. Here we pick it out of `rest`.
          const { htmlType, ...otherProps } = rest as NativeButtonProps;

          const buttonNode = (
            <button
              {...(omit(otherProps, ['loading']) as NativeButtonProps)}
              type={htmlType}
              className={classes}
              onClick={this.handleClick}
              ref={this.saveButtonRef}
            >
              {iconNode}
              {kids}
            </button>
          );

          if (type === 'link') {
            return buttonNode;
          }

          return <Wave>{buttonNode}</Wave>;
        }}
      </SizeContext.Consumer>
    );
  }
}

export default Button;

button-group.tsx

import * as React from 'react';
import classNames from 'classnames';
import { SizeType } from '../config-provider/SizeContext';
import { ConfigConsumer, ConfigConsumerProps } from '../config-provider';

export interface ButtonGroupProps {
  size?: SizeType;
  style?: React.CSSProperties;
  className?: string;
  prefixCls?: string;
}

const ButtonGroup: React.FC<ButtonGroupProps> = props => (  //函数组件
  <ConfigConsumer>
    {({ getPrefixCls, direction }: ConfigConsumerProps) => {
      const { prefixCls: customizePrefixCls, size, className, ...others } = props;
      const prefixCls = getPrefixCls('btn-group', customizePrefixCls);

      // large => lg
      // small => sm
      let sizeCls = '';
      switch (size) {
        case 'large':
          sizeCls = 'lg';
          break;
        case 'small':
          sizeCls = 'sm';
          break;
        default:
          break;
      }

      const classes = classNames(
        prefixCls,
        {
          [`${prefixCls}-${sizeCls}`]: sizeCls,
          [`${prefixCls}-rtl`]: direction === 'rtl',
        },
        className,
      );

      return <div {...others} className={classes} />;
    }}
  </ConfigConsumer>
);

export default ButtonGroup;

index.tsx

import Button from './button';
import ButtonGroup from './button-group';

export { ButtonProps, ButtonShape, ButtonType } from './button';
export { ButtonGroupProps } from './button-group';
export { SizeType as ButtonSize } from '../config-provider/SizeContext';

Button.Group = ButtonGroup;
export default Button;

一些使用到的库

classnames

classNames('foo', 'bar'); // => 'foo bar'
classNames('foo', { bar: true }); // => 'foo bar'
classNames({ 'foo-bar': true }); // => 'foo-bar'
classNames({ 'foo-bar': false }); // => ''
classNames({ foo: true }, { bar: true }); // => 'foo bar'
classNames({ foo: true, bar: true }); // => 'foo bar'

classnames结合React

 var Button = React.createClass({
  // ...
  render () {
    var btnClass = 'btn';
    //根据点击的state来控制css
    if (this.state.isPressed) btnClass += ' btn-pressed';
    else if (this.state.isHovered) btnClass += ' btn-over';
    return <button className={btnClass}>{this.props.label}</button>;
  }
});

可以统一返回一个对象

 var classNames = require('classnames');

var Button = React.createClass({
  // ...
  render () {
    var btnClass = classNames({
      'btn': true,
      'btn-pressed': this.state.isPressed,
      'btn-over': !this.state.isPressed && this.state.isHovered
    });
    return <button className={btnClass}>{this.props.label}</button>;
  }
});

如果是name和className进行了映射,可以使用bind方法

var classNames = require('classnames/bind');

var styles = {
  foo: 'abc',
  bar: 'def',
  baz: 'xyz'
};

var cx = classNames.bind(styles);

var className = cx('foo', ['bar'], { baz: true }); // => "abc def xyz"
import { Component } from 'react';
import classNames from 'classnames/bind';
import styles from './submit-button.css';

let cx = classNames.bind(styles);

export default class SubmitButton extends Component {
  render () {
    let text = this.props.store.submissionInProgress ? 'Processing...' : 'Submit';//text根据状态来动态加载
    let className = cx({
      base: true,
      inProgress: this.props.store.submissionInProgress,//样式的动态加载
      error: this.props.store.errorOccurred,
      disabled: this.props.form.valid,
    });
    return <button className={className}>{text}</button>;
  }
};

答案在风中飘着
302 声望6 粉丝

\失去人性,失去许多!失去兽性,失去一切!