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的组件写法一看就似曾相识的感觉,但是对于新手来说,不一定能马上看懂,建议一次看不懂的同学,多看几遍,然后尝试自己去写一个简单的,没有任何交互的组件,再一步步去增加事件交互,完善好它。