Ant Design原始碼分析(二):Button元件
年底正式總結的好時機, Button元件的原始碼。
-
Button分析
通過官方API 文章,大家知道<Button /> 元件具備以下幾個功能點
1、多種樣式風格可選: primary、ghost、 danger等,並且每一種風格都對應各自風格的互動
2、接收click事件回撥函式
3、可以指定點選跳轉指定的url
4、可以控制圖示旋轉,模擬請求狀態pending
原始碼如下
import * as React from 'react'; import { findDOMNode } from 'react-dom'; import * as PropTypes from 'prop-types'; import classNames from 'classnames'; /* 引入了一個系的模組Wave,可能是功能函式,可能是元件,先不管它是什麼,用到時再回來看 */ import Wave from '../_util/wave'; import Icon from '../icon'; import Group from './button-group'; //* 元件邏輯的一些輔助常量 */ 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) { // Check the child if is undefined or 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; } /* 聯合型別 Button.props中 type、shape、size、htmlType的取值範圍*/ export type ButtonType = 'default' | 'primary' | 'ghost' | 'dashed' | 'danger'; export type ButtonShape = 'circle' | 'circle-outline'; export type ButtonSize = 'small' | 'default' | 'large'; export type ButtonHTMLType = 'submit' | 'button' | 'reset'; /* 定義介面 相當於props-types */ export interface BaseButtonProps { type?: ButtonType; icon?: string; shape?: ButtonShape; size?: ButtonSize; loading?: boolean | { delay?: number }; prefixCls?: string; className?: string; ghost?: boolean; block?: boolean; children?: React.ReactNode; } export type AnchorButtonProps = { href: string; target?: string; onClick?: React.MouseEventHandler<HTMLAnchorElement>; } & BaseButtonProps & React.AnchorHTMLAttributes<HTMLAnchorElement>; export type NativeButtonProps = { htmlType?: ButtonHTMLType; onClick?: React.MouseEventHandler<HTMLButtonElement>; } & BaseButtonProps & React.ButtonHTMLAttributes<HTMLButtonElement>; export type ButtonProps = AnchorButtonProps | NativeButtonProps; export default class Button extends React.Component<ButtonProps, any> { static Group: typeof Group; static __ANT_BUTTON = true; static defaultProps = { prefixCls: 'ant-btn', loading: false, ghost: false, block: false, }; static propTypes = { type: PropTypes.string, shape: PropTypes.oneOf(['circle', 'circle-outline']), size: PropTypes.oneOf(['large', 'default', 'small']), htmlType: PropTypes.oneOf(['submit', 'button', 'reset']), onClick: PropTypes.func, loading: PropTypes.oneOfType([PropTypes.bool, PropTypes.object]), className: PropTypes.string, icon: PropTypes.string, block: PropTypes.bool, }; private delayTimeout: number; constructor(props: ButtonProps) { super(props); this.state = { /** 控制Button中Icon來旋轉,通常應用在非同步請返回之前的場景中,比如提交表單,請求結束前讓Icon旋轉,可以使得體驗更好,用來實現文章開頭時所描述的功能4 */ loading: props.loading, /** 作為子元素中是否有兩個中文字元的識別符號, 以此作為是否插入空格的識別符號*/ hasTwoCNChar: false, }; } componentDidMount() { this.fixTwoCNChar(); } componentWillReceiveProps(nextProps: ButtonProps) { const currentLoading = this.props.loading; const loading = nextProps.loading; if (currentLoading) { clearTimeout(this.delayTimeout); } if (typeof loading !== 'boolean' && loading && loading.delay) { this.delayTimeout = window.setTimeout(() => this.setState({ loading }), loading.delay); } else { this.setState({ loading }); } } componentDidUpdate() { this.fixTwoCNChar(); } componentWillUnmount() { if (this.delayTimeout) { clearTimeout(this.delayTimeout); } } fixTwoCNChar() { // Fix for HOC usage like <FormatMessage /> const node = (findDOMNode(this) as HTMLElement); const buttonText = node.textContent || node.innerText; if (this.isNeedInserted() && isTwoCNChar(buttonText)) { if (!this.state.hasTwoCNChar) { this.setState({ hasTwoCNChar: true, }); } } else if (this.state.hasTwoCNChar) { this.setState({ hasTwoCNChar: false, }); } } handleClick: React.MouseEventHandler<HTMLButtonElement | HTMLAnchorElement> = e => { const { onClick } = this.props; if (onClick) { (onClick as React.MouseEventHandler<HTMLButtonElement | HTMLAnchorElement>)(e); } } isNeedInserted() { const { icon, children } = this.props; return React.Children.count(children) === 1 && !icon; } render() { /** 通過Props來生成不同的className而達到不同的效果,有興趣的可以研究下樣式 */ const { type, shape, size, className, children, icon, prefixCls, ghost, loading: _loadingProp, block, ...rest } = this.props; const { loading, hasTwoCNChar } = this.state; /* 通過size控制按鈕的大小尺寸*/ let sizeCls = ''; switch (size) { case 'large': sizeCls = 'lg'; break; case 'small': sizeCls = 'sm'; default: break; } /* Antd 聖誕彩蛋事件程式碼,每年12.25搞一次, UI變換思路依舊是通過props.someKey 配合不同的className來實現 */ /* 修復方式 https://github.com/ant-design/ant-design/issues/13848*/ const now = new Date(); const isChristmas = now.getMonth() === 11 && now.getDate() === 25; /** 通過Props來生成不同的className而達到不同的UI效果嗎,有興趣的可以研究下樣式 */ const classes = classNames(prefixCls, className, { [`${prefixCls}-${type}`]: type, [`${prefixCls}-${shape}`]: shape, [`${prefixCls}-${sizeCls}`]: sizeCls, [`${prefixCls}-icon-only`]: !children && icon, [`${prefixCls}-loading`]: loading, [`${prefixCls}-background-ghost`]: ghost, [`${prefixCls}-two-chinese-chars`]: hasTwoCNChar, [`${prefixCls}-block`]: block, christmas: isChristmas, }); const iconType = loading ? 'loading' : icon; const iconNode = iconType ? <Icon type={iconType} /> : null; const kids = (children || children === 0) ? React.Children.map(children, child => insertSpace(child, this.isNeedInserted())) : null; const title= isChristmas ? 'Ho Ho Ho!' : rest.title; /* 可以指定按鈕跳轉地址,實現功能3 */ if ('href' in rest) { return ( <a {...rest} className={classes} onClick={this.handleClick} title={title} > {iconNode}{kids} </a> ); } else { // 這裡的註釋的意思是React不推薦在DOM element上使用 ‘htmlType' 這個屬性,因此在前面的IProps介面中,沒有定義htmlType,但仍可以使用 // 在ES6與React,通常使用(剩餘引數)這種方式可以擴充套件元件的IProps介面 // React does not recognize the `htmlType` prop on a DOM element. Here we pick it out of `rest`. const { htmlType, ...otherProps } = rest; /** *這裡出現了開頭的的外部依賴Wave,看到這種寫法,對React元件設計比較熟悉的應該能猜到這個Wave是做什麼的了,沒錯Wave是容器元件 *React推崇的是元件化開發,經過這些年的發展與沉澱,有兩個關鍵詞越來越活躍:`compose`與`recompose`,看過設計模式的知道,良好的軟體設計因該是組合優於繼承的,這兩個關鍵詞也是這個思路 *為了提高元件的可複用性,社群同時提出了幾種React元件的設計思路HOC、Render Callback、 容器元件與展示元件等 *元件拆分的目的是為了複用,複用什麼呢? 通常是是UI邏輯 *這裡我們先不去關注關注這個Wave是做什麼的,我們只需要知道此時,返回一個<button><button>即可, 我們在下一篇文章中去看下這個Wave元件,這裡我 *這裡我們只需要知道返回了一個button DOM元素,可以接收className、click事件、可以指定點選時跳轉到指定url, * */ return ( <Wave> <button {...otherProps} type={htmlType || 'button'} className={classes} onClick={this.handleClick} title={title} > {iconNode}{kids} </button> </Wave> ); } } }
分析過`<Button />`元件,再結合之前的`<Icon />`元件,我們其實可以發現一些Antd的一點設計模式(經過兩年的React專案踩坑,回過頭來看時,發現React社群中存在著大量的設計模式),將之成為`ControlCSS with Props`,後面簡稱為`CCP`。 在以前JQuery + CSS 橫掃各大瀏覽器的時候,大家寫CSS時已經注意到了複用的便利性,下面的程式碼,前端開發人員肯定寫過,我們來看下面這段css程式碼
// 抽取出一個元件的樣式 .btn { display: inline-block; font-weight: @btn-font-weight; text-align: center; touch-action: manipulation; cursor: pointer; background-image: none; border: @border-width-base @border-style-base transparent; white-space: nowrap; .button-size(@btn-height-base; @btn-padding-base; @font-size-base; @btn-border-radius-base); user-select: none; transition: all .3s @ease-in-out; position: relative; box-shadow: 0 2px 0 rgba(0, 0, 0, .015); } // 在此基礎上變形,與擴充套件 .btn-danger{ color: red; } .btn-primary{ background: blue; }
相信上面這段程式碼對前端開人員來說,如果放到`html + css`中,如喝水吃飯一樣習以為常, 不過是之前的模式在React中經過了變化,此模式將在後面的程式碼中大量出現,所以與大家約定這種`CCP`的名字 css已經有了,怎麼跟Html匹配上呢,看下JSX中的寫法
class SimpleBtn extends React.component { render(){ const {type} = this.this.props; const cls = 'btn'; /**根據props.type 來生成不同DOM節點 */ const btnCls = classNames({ [`${cls}-danger`]: type === 'danger', [`${cls}-primary`]: type === 'type', }, cls); return ( <button className={btnCls}> {this.props.children} </button> ) } }
呼叫方式如下
improt {SimpleBtn} from 'smpePath'; class Clent extends React.Component{ render(){ return ( <div> // 顯示一個紅色的按鈕 <SimpleBtn type="danger"></SimpleBtn> // 顯示一個藍色按鈕 <SimpleBtn type="primary"></SimpleBtn> </div> ) } }
相信看到這裡,大家對這種設計模式已經瞭然於心了。在後面的元件中會大量出現這種元件設計模式。
本篇完