分析antd组件化思路之button按钮的实现

#1

antd是什么?蚂蚁金服技术团队大神们开源的react组件,那么这些组件到底是怎样写出来的呢?
我将通过button按钮的例子来分析源码。
button例子链接:https://github.com/ant-design/ant-design/tree/master/components/button

antd是用typescript写的,不明白为什么不用es6呢?大神的世界我不懂。
在button文件夹下面,有几个主要的文件:

+ style(样式)
+ button-group.tsx(按钮组,也叫作容器)
+ button.tsx(单个按钮组件,内部包含各种逻辑)
+ index.tsx(提供外部接口)

我们从外部调用,深入到组件内部的逻辑去分析。
1、根据antd官网提供的教程来看,调用组件的方式如下

import React, { PropTypes } from 'react'; //导入react
import { Table, Popconfirm, Button } from 'antd'; //导入antd组件,这里就有button的示例

const ProductList = ({ onDelete, products }) => {
  const columns = [{
    title: 'Name',
    dataIndex: 'name',
  }, {
    title: 'Actions',
    render: (text, record) => {
      return (
        <Popconfirm title="Delete?" onConfirm={() => onDelete(record.id)}>
          {*在这里调用了button组件,只是初始化了text为delete*}
          <Button>Delete</Button> 
        </Popconfirm>
      );
    },
  }];
  return (
    <Table
      dataSource={products}
      columns={columns}
    />
  );
};

ProductList.propTypes = {
  onDelete: PropTypes.func.isRequired,
  products: PropTypes.array.isRequired,
};

export default ProductList;

上面的例子很简单,那么接下来我们就看看button是怎样在antd定义好的。

2、index.tsx文件:导出button组件

import Button from './button'; //展示型组件
import ButtonGroup from './button-group'; //组件群(容器)

Button.Group = ButtonGroup; //Group是button组件内部定义的静态成员(static Group: any;)
export default Button; //导出来给各位懒人用咯

3、button-group.tsx文件

import React from 'react';//导入react
import classNames from 'classnames'; //这个插件允许自定义样式对象

export type ButtonSize = 'small' | 'large'//组件有大小,还是很不错的嘛。

//不就是一堆接口吗?
export interface ButtonGroupProps {
  size?: ButtonSize; //按钮的大小,分为small和large
  style?: React.CSSProperties; //样式
  className?: string; //类名
  prefixCls?: string; //前缀
}

//定义一个可默认导出的函数ButtonGroup
export default function ButtonGroup(props: ButtonGroupProps) {
//这些参数通过props传入,而props对应的是上面定义的接口ButtonGroupProps里面的属性
//这写法咋那么像java呢?
  const { prefixCls = 'ant-btn-group', size = '', className, ...others } = props;

  // large => lg
  // small => sm
  const sizeCls = ({
    large: 'lg',
    small: 'sm',
  })[size] || ''; //这句话表示啥意思?

//自定义样式,prefixCls为传入的参数(prefixCls = 'ant-btn-group')
//${prefixCls}-${sizeCls}表示类名
  const classes = classNames(prefixCls, {
    [`${prefixCls}-${sizeCls}`]: sizeCls,//这里不是很了解,据我所知sizeCls应该是bool值true or false
  }, className);

//返回div,上面定义的classes样式传入className
  return <div {...others} className={classes} />;
}

这个文件还是挺容易理解的,他实现了一个div容器,用来包装button按钮组件,然后给这个容器定义了一些属性,比如大小、样式等,就是为了满足其他开发者可以自定义操作。

阅读步骤:入口函数ButtonGroup,然后定义参数props并且赋值为ButtonGroupProps(这个是es6的写法吧,莫非ts也可以。),接着就通过classNames插件定义了样式,传入div里面,最终函数返回这个div。

或许你也发现了others,这个others到底是什么东西呢?这个文件并没有说明,也没有定义。

4、button.tsx文件:有点长,我加上注释,耐心点看,看完再分析。

import React from 'react'; //导入react
import classNames from 'classnames'; //导入classnames
import { findDOMNode } from 'react-dom'; //findDOMNode用来找节点的呀
import Icon from '../icon'; //把其他组件也导进来用了,不用关注

//这正则啥意思?
const rxTwoCNChar = /^[\u4e00-\u9fa5]{2}$/;
const isTwoCNChar = rxTwoCNChar.test.bind(rxTwoCNChar);
function isString(str) {
  return typeof str === 'string';
}

// 自动在两个汉字之间插入一个空格。(为什么要插入空格?好看?)
function insertSpace(child) {
  if (isString(child.type) && isTwoCNChar(child.props.children)) {
    return React.cloneElement(child, {},
                              child.props.children.split('').join(' '));
  }
  if (isString(child)) {
    if (isTwoCNChar(child)) {
      child = child.split('').join(' ');
    }
    return <span>{child}</span>;
  }
  return child;
}

export type ButtonType = 'primary' | 'ghost' | 'dashed'
export type ButtonShape = 'circle' | 'circle-outline'
export type ButtonSize = 'small' | 'large'

export interface ButtonProps {
  type?: ButtonType;
  htmlType?: string;
  icon?: string;
  shape?: ButtonShape;
  size?: ButtonSize;
  onClick?: React.FormEventHandler<any>;
  onMouseUp?: React.FormEventHandler<any>;
  loading?: boolean;
  disabled?: boolean;
  style?: React.CSSProperties;
  prefixCls?: string;
  className?: string;
}

//原来入口跑这里来了,上面定义了一堆函数和接口,好强大啊。
export default class Button extends React.Component<ButtonProps, any> {
  static Group: any; //静态成员,还记得在index的Button.Group = ButtonGroup吧

//默认属性,loading都有,false就是不显示loading。
  static defaultProps = {
    prefixCls: 'ant-btn',
    loading: false,
  };

//静态属性验证,这些属性都是从外部容器穿进来的,需要各位看官自己传值,够灵活了。
  static propTypes = {
    type: React.PropTypes.string,
    shape: React.PropTypes.oneOf(['circle', 'circle-outline']),
    size: React.PropTypes.oneOf(['large', 'default', 'small']),
    htmlType: React.PropTypes.oneOf(['submit', 'button', 'reset']),
    onClick: React.PropTypes.func,
    loading: React.PropTypes.bool,
    className: React.PropTypes.string,
    icon: React.PropTypes.string,
  };

  timeout: any;
  clickedTimeout: any;

  componentWillUnmount() {
  //竟然还有组件卸载触发的事件,看看做了什么,清除定时器。
    if (this.clickedTimeout) {
      clearTimeout(this.clickedTimeout);
    }
    if (this.timeout) {
      clearTimeout(this.timeout);
    }
  }

  clearButton = (button) => {
  //清除按钮样式
    button.className = button.className.replace(` ${this.props.prefixCls}-clicked`, '');
  }

  handleClick = (e) => {
    // 点击激活效果
    const buttonNode = findDOMNode(this);//findDOMNode派上用场了,瞬间绑定当前的元素,e.target一边去。
    //下面几行都是定时改变样式的逻辑,自己仔细品味吧。
    this.clearButton(buttonNode);
    this.clickedTimeout = setTimeout(() => buttonNode.className += ` ${this.props.prefixCls}-clicked`, 10);
    clearTimeout(this.timeout);
    this.timeout = setTimeout(() => this.clearButton(buttonNode), 500);

	//onClick事件有点特殊,onClick来自于button按钮,只是触发了onClick(e),这是递归?原谅我基础不好。
    const onClick = this.props.onClick;
    if (onClick) {
      onClick(e);
    }
  }

  // 在Chrome中点击按钮时处理自动对焦,按钮的onMouseUp事件
  handleMouseUp = (e) => {
    (findDOMNode(this) as HTMLElement).blur();
    if (this.props.onMouseUp) {
      this.props.onMouseUp(e);
    }
  }

	//render终于出现了,这才是主体啊,上面几十行代码都是组件内部的逻辑函数,render才是渲染虚拟dom的正宗。
  render() {
  //为什么要单独把props提取出来呢。。我个人习惯于直接用this.props表示。
    const props = this.props;
    //这么多参数,如果不是自己写的代码,可能真的会晕了。大部分还是能看懂的,英语单词也不难。
    const { type, shape, size = '', className, htmlType, children, icon, loading, prefixCls, ...others } = props;

    // large => lg
    // small => sm
    //老代码,不解释了。
    const sizeCls = ({
      large: 'lg',
      small: 'sm',
    })[size] || '';
	
	//又是定义样式的代码。
    const classes = classNames(prefixCls, {
      [`${prefixCls}-${type}`]: type,
      [`${prefixCls}-${shape}`]: shape,
      [`${prefixCls}-${sizeCls}`]: sizeCls,
      [`${prefixCls}-icon-only`]: !children && icon,
      [`${prefixCls}-loading`]: loading,
    }, className);

	//判断显示loading还是icon
    const iconType = loading ? 'loading' : icon;
	
	//看了这句代码,我才发现自对react的children还不够了解。
    const kids = React.Children.map(children, insertSpace);
	//下面就只剩下jsx模板了,到现在还没看懂others是干嘛的。
    return (
      <button
        {...others}
        type={htmlType || 'button'}
        className={classes}
        onMouseUp={this.handleMouseUp}
        onClick={this.handleClick}
      >
        {iconType ? <Icon type={iconType} /> : null}{kids}
      </button>
    );
  }
}

代码好长。。138行呢。。看来阿里宝宝们已经考虑到了组件的多种适用场景了,比我厉害很多,我自己写的react没有那么强大。

给一些代码加了注释,看官们可以结合自己的理解去看。

那么,我来总结一下button组件的写法逻辑。

a、首先定义一个组件类Button(没错,用到了ES6的class语法)

b、类里面需要什么?一个react组件的生命周期componentWillunmount(),还有就是静态属性和事件方法,最重要的就是render函数,其实就是一个常用的react组件的写法,如果你写过react的项目,那么或多或少也这样写过。

c、button组件导入了一个叫做classnames的插件,好吧,我承认这个插件我也经常用到,只是没有阿里的大神们用的这么得心应手。还有一堆属性表示什么意思就不详细说了,想必看一下antd文档也就大致明白了。

5、最后,你是否发现了,button组件没有用到state!!这是不是说明各位在写react组件的时候不需要state了呢?我认为不是的,很多童鞋用state来保存组件内部的当前状态,包括我自己也会这样用,但无论用不用state,都需要将点击选中的value或者其他属性暴露给外部容器,这样才能获取需要的值。

6、我最近也在研究input组件的封装,目前还没有去看antd是怎样实现input的,毕竟自己脑海里有思路,不想被antd的思路给打乱。在input封装的时候,我就用到了内部state,用来存储当前输入的值,再通过接口(可以是一个函数return this.state.value)暴露给外部,让外部可以读取到输入框的值。还有一种办法是通过ref来读取子组件的值,就不做介绍了。

分析的不一定很准确,可能我个人写过很多react组件,对于antd的组件写法一看就似曾相识的感觉,但是对于新手来说,不一定能马上看懂,建议一次看不懂的同学,多看几遍,然后尝试自己去写一个简单的,没有任何交互的组件,再一步步去增加事件交互,完善好它。

1 Like
#2

others那里是解构赋值 意思应该是取除了指定props以外的其他props,透传给react的button

#3

有点不理解是 里面使用了typescript 约束 ButtonProps,后面用了 propTypes,这个不是做重复了吗

#4

others就是除了列出的属性之外的属性,也就是用户随意。。。

#5

大佬说的对,antdesign官方的开发者写的代码也不是绝对的完美,只要能理解主要思路就够了。