Wenzi

React:实现一个带有loading效果的按钮组件

蚊子前端博客
发布于 2019/05/10 21:16
在react中如何实现一个带有loading效果的按钮组件呢?

在业务经常需要点击按钮去请求接口,在接口还未返回结果前,需要给用户一个等待的感觉,同时锁住按钮,防止产生二次点击!

我们实现带有loading效果的按钮组件,主要是实现以下的几个功能:

  • 有菊花或者圈圈的loading图标,且loading颜色与字体颜色相同;
  • loading的过程中,点击无效;

1. 按钮的结构 #

针对第1个功能,我最开始是把接口返回结果之前的loading集成到了按钮中,这个loading是用多个div拼接而成的,而loading的颜色则是渲染div中的after伪元素的背景色:

/** 
 * .lds-spinner div:after {
        content: '';
        display: block;
        position: absolute;
        top: 3px;
        left: 29px;
        width: 5px;
        height: 14px;
        border-radius: 20%;
        background: #f3434a;
    }
*/
render() {
    return (
        <div className="i-loading">
            <div className="lds-spinner">
                {
                    Array(12).fill(0).map((item, index) => <div key={index}></div>)
                }
            </div>
        </div>
    );
}

这种方式如果要loading效果跟按钮字体保持一个颜色,则需要通过props传入一个色值,然后在Button组件中才能修改。实在是不太方便,我既要在CSS中设置按钮的颜色,还要通过props传给Button组件,当按钮的字体颜色需要更新时,则需要修改两个地方。再一个,loading图案的大小也要跟着按钮的大小进行变化,而div整体不太好调整,能想到的方法是使用transform: scale来对loading图案放大或者缩小。

这时,loading效果用SVG实现最好了,配上currentColor,能天然继承其父级元素的color值。再使用em单位适应按钮的大小:

render() {
    return (
        <div>
            <i className="i-icon-loading">
                <svg viewBox="0 0 1024 1024" className="i-icon-loading-spin" data-icon="loading" width="1em" height="1em" fill="currentColor" aria-hidden="true" focusable="false">
                    <path d="M988 548c-19.9 0-36-16.1-36-36 0-59.4-11.6-117-34.6-171.3a440.45 440.45 0 0 0-94.3-139.9 437.71 437.71 0 0 0-139.9-94.3C629 83.6 571.4 72 512 72c-19.9 0-36-16.1-36-36s16.1-36 36-36c69.1 0 136.2 13.5 199.3 40.3C772.3 66 827 103 874 150c47 47 83.9 101.8 109.7 162.7 26.7 63.1 40.2 130.2 40.2 199.3.1 19.9-16 36-35.9 36z"></path>
                </svg>
            </i>
            <span>立即领取</span>
        </div>
    );
}

2. loading效果 #

按钮最少要接收2个参数,一个是表示是否正在loading中,再一个是接收click事件:

/**
.i-button {
    .i-icon-loading {
        margin-right: 8px;
    }
}

.i-button-loading {
    position: relative;

    &:before{
        content: '';
        position: absolute;
        background-color: #ffffff;
        opacity: 0.4;
        top: -1px;
        right: -1px;
        bottom: -1px;
        left: -1px;
        z-index: 1;
        transition: opacity .2s;
        border-radius: inherit;
    }
}
*/

interface ButtonProps {
    loading: boolean;
    onClick: React.MouseEventHandler;
}

export default class Button extends React.Component<ButtonProps> {
    handleClick = (e: any) => {
        const { loading, onClick } = this.props;
        if (!!loading) {
            // 正在loading中时,直接返回
            return;
        }
        if (onClick) {
            (onClick as React.MouseEventHandler)(e);
        }
    }

    render() {
        const { loading } = this.props;

        // 将loading效果提取出来
        const iconNode = loading ? <IconLoading /> : null;

        // loading时添加loading的class
        const loddingClass = loading ? ' i-button-loading' : '';

        return (
            <div onClick={this.handleClick} className={"i-button" + loddingClass }>
                {iconNode}
                <span>{this.props.children}</span>
            </div>
        );
    }
}

loading的icon组件如下:

/**
.i-icon-loading {
    font-size: 36px;
    transition: transform .3s ease-in-out;
    transition: transform .3s ease-in-out;
    will-change: transform;
    display: inline-block;
    color: inherit;
    font-style: normal;
    line-height: 0;
    text-align: center;
    text-transform: none;
    vertical-align: -0.125em;
    text-rendering: optimizeLegibility;
    -webkit-font-smoothing: antialiased;

    .i-icon-loading-spin {
        display: inline-block;
        animation: loadingCircle 1s infinite linear;
    }
    
    @-webkit-keyframes loadingCircle {
        100% {
            -webkit-transform: rotate(360deg);
            transform: rotate(360deg)
        }
    }
    
    @keyframes loadingCircle {
        100% {
            -webkit-transform: rotate(360deg);
            transform: rotate(360deg)
        }
    }
}
 * */
export default class IconLoading extends Component {
    render() {
        return (
            <i className="i-icon-loading">
                <svg viewBox="0 0 1024 1024" className="i-icon-loading-spin" data-icon="loading" width="1em" height="1em" fill="currentColor" aria-hidden="true" focusable="false">
                    <path d="M988 548c-19.9 0-36-16.1-36-36 0-59.4-11.6-117-34.6-171.3a440.45 440.45 0 0 0-94.3-139.9 437.71 437.71 0 0 0-139.9-94.3C629 83.6 571.4 72 512 72c-19.9 0-36-16.1-36-36s16.1-36 36-36c69.1 0 136.2 13.5 199.3 40.3C772.3 66 827 103 874 150c47 47 83.9 101.8 109.7 162.7 26.7 63.1 40.2 130.2 40.2 199.3.1 19.9-16 36-35.9 36z"></path>
                </svg>
            </i>
        );
    }
}

3. 总结 #

第一次开始使用React写项目,之前都是用Vue在写项目。用Vue写了几个项目了,想着换一种技术栈来写项目,React跟Vue有一些相似之处,不过还是有些不同的,感觉React中很多地方需要自己完善,比如react的路由守卫beforeEach,CSS没有scoped属性,老担心不同的组件之前会产生样式冲突,虽然可以使用*.module.css来规避,不过这样在jsx中写className就不太方便了!另一方面来讲,写React特别能锻炼自己的js能力,比如高阶组件、ES6等新特性的使用,同时react和typescript结合的特别好。以后,随着对React更深入的了解,当然,这些也就不是问题了!

参考:

https://www.zhangxinxu.com/wordpress/2014/07/svg-sprites-fill-color-currentcolor/

标签:reactbutton
阅读(8537)
Simple Empty
No data