首页 > 行业动态 > 正文

利用React完成天猫轮播效果
2016-12-23 10:30:15   来源:   评论:0 点击:

天猫购物网站最显眼的就是轮播图了。我在学习一样新js库,一个新框架或新的编程思想的时候,总是感叹入门必做选项卡,进阶须撸轮播图。作为
天猫购物网站最显眼的就是轮播图了。我在学习一样新js库,一个新框架或新的编程思想的时候,总是感叹“入门必做选项卡,进阶须撸轮播图。”作为一个React组件,它是状态操控行为的典型,拿来练手是个不错的选择。
为了复习,这次就尝试用原生的javascript+React来完成。


轮播图原生实现

所谓轮播图其实是扩展版的选项卡。

先布局

主干架构



样式如下


大概效果

纯javascript实现

事件

一个简单的轮播图包括多个事件。

  • 鼠标移入移出:当鼠标移出,或者是鼠标不在轮播图上面,执行自动播放
  • 当鼠标移入:不再自动播放,而且点击按钮会执行跳转到相应的页面

渐变

因为6张图不是很多。所以考虑六张图全部做绝对定位,按照顺序叠加在一起。然再通过一个透明度的运动框架,实现之。
在此我选用这个运动框架:



这个框架可以指定样式值进行渐变。

不得不说,这确实是一个很棒的运动框架。可以把它单独放在为一个名为move.js的文件中再引入。

根据这个思路写出原生的代码:




效果如下:



不得不说,原生的代码写起来好长好长。

很长吗?后面的更长。
 

React思路

以上原生代码已经经过了初步的封装——比如INowListener。但是在React的价值观来说,显然还需要进一步的封装。甚至重新拆分。最理想的情况是:顶层组件作为主干架构和状态机。下层组件接收状态并运行方法。
 

多少个组件?

在这个轮播图中,就三个组件。

这样写就把样式写出来了。

哪个是状态?


iNOW是状态。而且是最重要的状态!既然这样,就考虑把状态iNow放顶层。

鼠标悬停看起来也是状态,但悬停按钮上,触发iNow改变——因此还是iNow。

鼠标移入移出事件,应该是状态。但是这个移入移出的状态依赖于iNow。所以不能单独用。
 

需要哪些props?


构造组件时,为了灵活性,一般都不考虑把组件框架写死。比如图片张数,id名,等等都应该是props。但是这些暂时来说,都是次要的。

状态肯定是一个核心props,此外,底层设置状态的回调也是核心的props之一。

空谈太多无意义,接下来尝试实现!


自动按钮

现在先不考虑其它,单看按钮。
在插入文档之后,开启一个定时器,每隔2000ms执行一次状态更新。

setState的写法

那涉及到了iNow状态根据前一个状态更新,官方文档不建议这种写法:

this.setState({
    return {
        iNow:this.state.iNow+1
    }
})

因为状态更新可能是异步的。这样写很容易出问题。
事实上,官网提供了这样的写法:

this.setState(function(prev,props){
    return {
        iNow:prev.iNow+1
    }
})

在这里只用第一个参数就够了。

想当然的按钮

定时器应该是一个状态计算器。

所以按钮可以这么写:

按钮就实现了。

看起来不错,但是这样写可能在未来造成极大的不便。

悬停交互

再强调一次价值观这个概念,按照React的价值观,状态应该从顶层传下去,况且在这个案例中,顶层Tabs组件做一件事就够了:状态机,在Btn组件插入到文档之后,打开这个定时器。底层组件比如Btns根据状态每隔2000ms通过props刷新变化。

同时,我还要实现一个简单的交互功能:当鼠标悬停在Tabs上时,不再允许iNow自动更新。——可以做一个bCheck开关,当Tabs组件鼠标移入/移出时,触发bCheck的来回变化。

此处可能有个小问题,就是鼠标一道按钮组上时,会造成bCheck抖动。但是最后又变回false。所以认为不影响。

很自然想到,bCheck为false时,关闭定时器。但是这样做又等于浪费了定时器的功能,回调方法中一旦关掉定时器,再重新定时器就不是一般的麻烦了,为什么不直接在定时器做判断呢?所以我认为不应该让定时器停下来。只需要改变定时器计算iNow的行为就行了。

图片动画

一件事三个步骤

图片组件虽说只是做一件事情(根据iNow渲染效果),但是也得分三步来做。

  • 首先,渲染前应该保证索引值非iNow的所有图片display为none。索引值为iNow的图片透明度为0。(初始化)

  • 其次,在首次插入文档完毕之后(componentDidMount),对第0张图执行startMove函数。

  • 第三,需要一个监听顶层iNow的方法。定时器已经给Btns组件用了,再用就会出错。

留意到Imgs组件实际上只接受一个会变化的props那就是iNow。因此采用componentWillReceiveProps

生命周期方法

componentWillReceiveProps

组件接收到新的props时调用,并将其作为参数nextProps使用,此时可以更改组件propsstate
这里采用的两个组件周期方法都是组件真实存在时的方法。所以可以直接使用真实的DOM命令。

 

实现

var Tabs=React.createClass({
        getInitialState:function(){
            return {
                iNow:0,
                bCheck:true
            };
        },
        setInow:function(){
            var _this=this;
            var timer=setInterval(function(){
                if(_this.state.bCheck){
                    //console.log(_this.state.bCheck)
                    _this.setState(function(prev){
                        if(prev.iNow==5){
                            return {
                                iNow:0
                            };
                        }else{
                            return {
                                iNow:prev.iNow+1
                            };
                        }
                    });
                }else{
                    console.log('该停了!')
                    return false;
                }

            },2000);

        },
        checkSwitch:function(){
            console.log(this.state.bCheck)
            this.setState(function(prev){
                return {
                    bCheck:!prev.bCheck
                };
            });
        },
        render:function(){
            return (
                <div id="tabs"
                  onMouseOver={this.checkSwitch}
                  onMouseOut={this.checkSwitch}>
                    <Btns iNow={this.state.iNow}
                      setInow={this.setInow} />
                    <Imgs iNow={this.state.iNow}/>
                </div>
            );
        }
    });

    var Btns=React.createClass({
        componentDidMount:function(){
            this.props.setInow();
        },

        render:function(){
            var arr=[];
            for(var i=0;i<6;i++){
                var btnsContent=null;
                if(i==this.props.iNow){
                    btnsContent=
                        <li key={i.toString()}>
                            <a style={{background:'rgba(255,255,255,0.5)'}} href="javascript:;">a>
                        </li>
                }else{
                    btnsContent=
                        <li key={i.toString()}>
                            <a href="javascript:;">a>
                        </li>
                }
                arr.push(btnsContent);
            }

            return (
                <ul id="btns">{arr}ul>
            );
        }
    });

    var Imgs=React.createClass({
        componentDidMount:function(){//刚开始加载时,就执行动画函数
            var iNow=this.props.iNow;
            var obj=document.getElementById('imgs').getElementsByTagName('li')[iNow].childNodes[0];
            startMove(obj,{'opacity':100});
        },
        componentWillReceiveProps:function(nextProps){
            var obj=document.getElementById('imgs').getElementsByTagName('li')[nextProps.iNow].childNodes[0];
            //console.log(obj)
            startMove(obj,{'opacity':100});
        },
        // this.startMove:startMove(),
        render:function(){
            var arr=[];
            for(var i=0;i<6;i++){
                var imgsContent=null
                if(i==this.props.iNow){
                    imgsContent=
                        <li key={i.toString()}>
                            <img style={{opacity:'0'}} src={'images/banner'+(i+1)+'.jpg'} />
                        li>
                    arr.push(imgsContent);
                }else{
                    imgsContent=
                        <li key={i.toString()}>
                            <img style={{display:'none'}} src={'images/banner'+(i+1)+'.jpg'} />
                        li>
                    arr.push(imgsContent);
                }

            }

            return (
                <ul id="imgs">{arr}</ul>
            )
        }
    })

    ReactDOM.render(
        <Tabs/>,
        document.getElementById('example')
    );

看起来Imgs组件已经很完备了。——就它的功能来说已经没有什么需要添加了。

 

鼠标悬停改变iNow

这个事件只能在底层组件Btns上实现。所以要拿到悬停的索引值。

然后通过回调,把该按钮的索引值设置为整个组件Tabs的状态iNow。

为了干这两件事,还是用一个changeInow(e)函数来包装它们。
 

给谁绑定?加什么事件?

为了忠实原来的代码。我给a标签加onMouseOver事件。

加了事件直接,秉承这React的核心价值观(一个组件只干一件事),我把get到的index值通过this.props.setInow传递回去。只要顶层的iNow变了,下面的组件不管什么状态,都会乖乖听话了。

如何获取当前悬停的索引值?

在Jquery很容易使用index方法来获取索引值。但是在原生方法中,还得费一番周章。

给所有a绑定一个onMouseOver事件,假设该事件方法的参数为e,那么e.target就是该参数的方法。

这需要写一个getIndex方法

...
getIndex:function(e){
  var list=e.target.parentNode.parentNode.childNodes;
  for(var i=0;i<list.length;i++){
    if(list[i]===e.target.parentNode){
      return  i;
    }
  }
},
...

拿到索引值之后

——就把它设置为顶层的iNow。

既然决定通过this.props.setInow回调,那么还得传一个索引值参数,回到顶层稍微修改下方法,就实现了。

全部代码:

var Tabs=React.createClass({//顶层组件
        getInitialState:function(){
            return {
                iNow:0,
                bCheck:true
            };
        },
        setInow:function(index){//核心状态计算工具:依赖定时器进行实时刷新
            if(index!==undefined){//如果参数有内容。
                this.setState({
                    iNow:index
                });
            }else{
                var _this=this;
                this.timer=setInterval(function(){
                    if(_this.state.bCheck){
                        //console.log(_this.state.bCheck)
                        _this.setState(function(prev){
                            if(prev.iNow==5){
                                return {
                                    iNow:0
                                };
                            }else{
                                return {
                                    iNow:prev.iNow+1
                                };
                            }
                        });
                    }else{
                        //console.log('该停了!')
                        return false;
                    }
                },2000);
            }
        },
        checkSwitch:function(){
            //console.log(this.state.bCheck)
            this.setState(function(prev){
                return {
                    bCheck:!prev.bCheck
                };
            });
        },
        render:function(){
            return (
                <div id="tabs"
                  onMouseOver={this.checkSwitch}
                  onMouseOut={this.checkSwitch}>
                    <Btns iNow={this.state.iNow}
                      setInow={this.setInow} />
                    <Imgs iNow={this.state.iNow}/>
                </div>
            );
        }
    });

    var Btns=React.createClass({
        componentDidMount:function(){
            this.props.setInow();
        },
        getIndex:function(e){//获取a的父级索引值
            var list=e.target.parentNode.parentNode.childNodes;
            for(var i=0;i++){
                if(list[i]===e.target.parentNode){
                    return i;
                }
            }
        },
        changeInow:function(e){//回调方法
            //console.log($(e.target).parent().index());
            //console.log(this.getIndex(e));
            var index=this.getIndex(e);
            this.props.setInow(index)
        },

        render:function(){
            var arr=[];
            for(var i=0;i<6;i++){
                var btnsContent=null;
                var index=i;
                if(i==this.props.iNow){
                    btnsContent=
                        <li key={i.toString()}>
                            <a onMouseOver={this.changeInow} style={{background:'rgba(255,255,255,0.5)'}} href="javascript:;">a>
                        </li>
                }else{
                    btnsContent=
                        <li key={i.toString()}>
                            <a  onMouseOver={this.changeInow} href="javascript:;">a>
                        </li>
                }
                arr.push(btnsContent);
            }

            return (
                <ul id="btns">{arr}ul>
            );
        }
    });

    var Imgs=React.createClass({
        componentDidMount:function(){//刚开始加载时,就执行动画函数
            var iNow=this.props.iNow;
            var obj=document.getElementById('imgs').getElementsByTagName('li')[iNow].childNodes[0];
            startMove(obj,{'opacity':100});
        },
        componentWillReceiveProps:function(nextProps){
            var obj=document.getElementById('imgs').getElementsByTagName('li')[nextProps.iNow].childNodes[0];
            //console.log(obj)
            startMove(obj,{'opacity':100});
        },
        // this.startMove:startMove(),
        render:function(){
            var arr=[];
            for(var i=0;i<6;i++){
                var imgsContent=null
                if(i==this.props.iNow){
                    imgsContent=
                        <li key={i.toString()}>
                            <img style={{opacity:'0'}} src={'images/banner'+(i+1)+'.jpg'} />
                        li>
                    arr.push(imgsContent);
                }else{
                    imgsContent=
                        <li key={i.toString()}>
                            <img style={{display:'none'}} src={'images/banner'+(i+1)+'.jpg'} />
                        li>
                    arr.push(imgsContent);
                }

            }

            return (
                <ul id="imgs">{arr}</ul>
            )
        }
    })

    ReactDOM.render(
        <Tabs/>,
        document.getElementById('example')
    );

完善

我们要让这个组件可复用,换言之就是把之前写死的东西比如图片数量,样式,id名都变成Tabs的props属性。

这下工作量够了吧。原来50多行的东西改写完之后大概150多行。

var Tabs=React.createClass({//顶层组件
        getInitialState:function(){
            return {
                iNow:0,
                bCheck:true
            };
        },
        setInow:function(index){//核心状态计算工具:依赖定时器进行实时刷新
            if(index!==undefined){//如果参数有内容。
                this.setState({
                    iNow:index
                });
            }else{
                var _this=this;
                this.timer=setInterval(function(){
                    if(_this.state.bCheck){
                        //console.log(_this.state.bCheck)
                        _this.setState(function(prev){
                            if(prev.iNow==this.props.nums-1){
                                return {
                                    iNow:0
                                };
                            }else{
                                return {
                                    iNow:prev.iNow+1
                                };
                            }
                        });
                    }else{
                        //console.log('该停了!')
                        return false;
                    }
                },this.props.timer);
            }
        },
        checkSwitch:function(){
            //console.log(this.state.bCheck)
            this.setState(function(prev){
                return {
                    bCheck:!prev.bCheck
                };
            });
        },
        render:function(){
            return (
                <div id={this.props.idNames.main}
                  onMouseOver={this.checkSwitch}
                  onMouseOut={this.checkSwitch}>

                    <Btns iNow={this.state.iNow}
                      setInow={this.setInow}
                      nums={this.props.nums}
                      idNames={this.props.idNames} />

                    <Imgs iNow={this.state.iNow}
                    nums={this.props.nums}
                    idNames={this.props.idNames}
                    imgType={this.props.imgType} />

                </div>
            );
        }
    });

    var Btns=React.createClass({
        componentDidMount:function(){
            this.props.setInow();
        },
        getIndex:function(e){//获取a的父级索引值
            var list=e.target.parentNode.parentNode.childNodes;
            for(var i=0;i++){
                if(list[i]===e.target.parentNode){
                    return i;
                }
            }
        },
        changeInow:function(e){//回调方法
            //console.log($(e.target).parent().index());
            //console.log(this.getIndex(e));
            var index=this.getIndex(e);
            this.props.setInow(index)
        },

        render:function(){
            var arr=[];
            for(var i=0;i<this.props.nums;i++){
                var btnsContent=null;
                var index=i;
                if(i==this.props.iNow){
                    btnsContent=
                        <li key={i.toString()}>
                            <a onMouseOver={this.changeInow} id={this.props.idNames.active} href="javascript:;">a>
                        </li>
                }else{
                    btnsContent=
                        <li key={i.toString()}>
                            <a  onMouseOver={this.changeInow} href="javascript:;">a>
                        </li>
                }
                arr.push(btnsContent);
            }

            return (
                <ul id={this.props.idNames.btns}>{arr}ul>
            );
        }
    });

    var Imgs=React.createClass({
        componentDidMount:function(){//刚开始加载时,就执行动画函数
            var iNow=this.props.iNow;
            var obj=document.getElementById(this.props.idNames.imgs).getElementsByTagName('li')[iNow].childNodes[0];
            startMove(obj,{'opacity':100});
        },
        componentWillReceiveProps:function(nextProps){//每当收到新的props就执行动画
            var obj=document.getElementById(this.props.idNames.imgs).getElementsByTagName('li')[nextProps.iNow].childNodes[0];
            //console.log(obj)
            startMove(obj,{'opacity':100});
        },

        render:function(){
            var arr=[];
            for(var i=0;i<this.props.nums;i++){
                var imgsContent=null;
                var src=this.props.imgType.url+this.props.imgType.name+(i+1)+'.'+this.props.imgType.type;
                if(i==this.props.iNow){
                    imgsContent=
                        <li key={i.toString()}>
                            <img style={{opacity:'0'}} src={src} />
                        li>
                    arr.push(imgsContent);
                }else{
                    imgsContent=
                        <li key={i.toString()}>
                            <img style={{display:'none'}} src={src} />
                        li>
                    arr.push(imgsContent);
                }
            }

            return (
                <ul id={this.props.idNames.imgs}>{arr}</ul>
            )
        }
    })

    ReactDOM.render(
        <Tabs
          nums={6}
          timer={2000}
          idNames={
              {
                  main:"tabs",
                  btns:"btns",
                  imgs:"imgs",
                  active:"btn-active"
              }
          }
          imgType={
              {
                  type:"jpg",
                  url:"images/",
                  name:"banner"
              }
          }
           />,
        document.getElementById('example')
    );

其中多设置了一个#btn-active样式。

#btn-active{
    background:rgba(255,255,255,0.5)!important;
}

是不是好长好长呢?

demo地址:

http://djtao.top/tabs/

但是这个确实是一个可复用的,而且还是原生js写成的组件。

不得不说,作为一个初学几天的人,写这东西时的时候遭遇好多的坑。但是最后“蓦然回首,那人却在灯火阑珊处”,也有种人生三境界的感悟了!

本文章版权归爱创课堂所有,转载请注明出处。

更多详细内容请访问爱创课堂官网首页

http://www.icketang.com/


相关热词搜索:天猫 效果

上一篇:2016年学前端是一种什么样的体验
下一篇:解决ie兼容渲染的小技巧

分享到: 收藏