React-实现仿原生App的转场动画

作者: CoderXLL | 来源:发表于2019-05-24 21:02 被阅读64次
    React大法好

    一、前言

    最近这两个星期,已经从jQuery的泥潭里抽出身来,开始学习React框架。React其实是一个UI框架,其功能性比较单一,需要依赖各种插件进行开发。这些React插件集合起来,就是鼎鼎大名的React全家桶。

    目前我了解并使用过的有如下几个插件:

    • react-router-dom React路由跳转插件
    • react-loadable 一个动态导入加载UI的高阶组件
    • redux, react-redux-dom react函数响应式编程框架,类似于iOS中的ReactiveCocoa数据驱动视图的思想。刚开始学的时候,配上ES6、7、8的语法糖,能绕的你不要不要的
    • prop-types 为弱语法的JS提供强类型操作
    • axios React界可以与ajax相抗衡的网络库
    • react-motion React中的弹性动画库
    • react-transition-group React中个人目前感觉最好用的动画库
      待了解的插件及知识点:
    • react-saga、react-thunk、immutable、webpack打包

    二、需求产出

    本篇文章是我在React学习过程中,打算将用过的知识点串起来时遇到的一个需求问题。因为还是iOS出身,所以梳理知识点的时候,还是想按照移动端那种形式去整理,如下图所示。很屌丝,终归还是一时难忘iOS。

    React学习Demo调试图
    此Demo现在还在编写完善当中,暂不公开。
    Demo中的路由跳转肯定就是用的react-router-dom这个插件了。但是这个插件进行路由切换时,效果很僵硬,没有过渡效果。这对于追求用户体验的iOS开发者来说肯定是不能接受的!!!所以我要做的,就是对react路由切换加上仿原生的转场动画。

    三、react-router-dom

    简单实现一下纯router插件的路由跳转,效果及代码如下:
    要说的都在代码注释中!!!先不要去管CSS样式

    import React from 'react';
    import {
        //以html5提供的history api形式实现的路由
        //一般其作为项目的原始组件进行包裹
        BrowserRouter as Router,
        //路由组件,path指定匹配的路由,component指定路由匹配时展示的组件
        Route,
        //Route组件包裹器
        Switch,
        //一个高阶组件,为组件提供location,history等对象
        withRouter
    } from 'react-router-dom';
    //自定义HomePage组件
    import HomePage from '../page/home';
    //自定义SecondPage组件
    import SecondPage from '../page/second';
    
    const RouteModule = function (props) {
        return (
            <Switch history={props.history}>
                <Route exact path={'/'} component={HomePage} name={'首页'} />
                <Route path={'/second'} component={SecondPage} name={'第二页'} />
            </Switch>
        );
    };
    
    export default class DemoApp5 extends React.Component {
        render() {
            const Routes = withRouter(RouteModule);
            return (
                <Router>
                    <Routes />
                </Router>
            );
        }
    }
    

    默认的路由切换效果如下:


    僵硬的react-router-dom路由切换效果.gif

    四、react-transition-group

    上面也略有介绍,react-transition-group是react中的一个很不错的动画库。为什么我会想到用它去实现转场动画?因为我了解的react动画库就两个,还有一个react-motion是弹性动画库,显然不合适。
    众所周知,JS实现动画最方便的还要属jQuery。其提供了一系列动画函数,好用且方便。但是操控的都是真实dom,这显然与React的虚拟dom思想相违背,所以没有考虑jQuery去实现需求。

    1、 CSSTransition实现单一组件动画

    CSSTransition单独使用时,有两个比较重要的属性。in和classNames。

    • classNames属性: CSSTransition子组件动态类选择器名前缀。
    • in属性:
      intrue时,CSSTransition的子组件会先添加${classNames}-enter类,下一个tick会添加${classNames}-enter-active类。
      infalse时,CSSTransition的子组件会先添加${classNames}-exit类,下一个tick会添加${classNames}-exit-active类。

    基于上面的知识点,我们先出一个react-transition-group的简单小Demo。

    import React from 'react';
    import {CSSTransition} from 'react-transition-group';
    import './style.css';
    /*
     * 知识点
     * CSSTransition属性in为true时,其子组件会加上`${classNames}-enter`的类
     * 然后再下一个tick时,马上加上`${classNames}-enter-active`的类
     * 当in为false时,其组件会加上`${classNames}-exit`和`${classNames}-exit-active`类
     *
     * unmountOnExit为false的时候,其子组件exit后不会卸载,并添加`${classNames}-exit-done`类
     * 为true,子组件exit后会卸载
     * 这里我们直接卸载,省的其写done样式
     * */
    export default class DemoApp1 extends React.Component {
        constructor(props) {
            super(props);
            this.state = {
               show: true
            };
            this.handleSwitch = this.handleSwitch.bind(this);
        }
        handleSwitch() {
            this.setState({
                show: !this.state.show
            });
        }
        render() {
            return (
                <div className='app1-container'>
                    <CSSTransition
                        in={this.state.show}
                        classNames='app1'
                        timeout={500}
                        unmountOnExit={true}
                    >
                        <div className='app1-square' />
                    </CSSTransition>
                    <button
                        onClick={this.handleSwitch}
                        className='app1-btn'
                    >切换
                    </button>
                </div>
            );
        }
    }
    

    CSS样式如下:

    .app1-enter {
        /*初始透明度为0*/
        opacity: 0;
        /*初始偏移量为100%*/
        transform: translateX(100%);
    }
    
    .app1-enter-active {
        /*随后透明度为1*/
        opacity: 1;
        /*随后偏移量回归原位*/
        transform: translateX(0);
        /*这个移动过程我们做个动画,动画持续时长500毫秒*/
        transition: all 500ms ease;
    }
    
    .app1-exit {
        opacity: 1;
        transform: translateX(0);
    }
    
    .app1-exit-active {
        opacity: 0;
        transform: translateX(-100%);
        transition: all 500ms ease;
    }
    

    效果符合预期:


    单一组件动画效果
    2、TransitionGroup实现多组件协调动画

    我们的路由跳转,实际上是有两个组件同时动画的。即第一个页面组件和第二个页面组件。所以单纯的CSSTransition组件不能满足需求。
    以代码为例进行讲解:

    export default class DemoApp2 extends React.Component {
        constructor(props) {
            super(props);
            this.state = {
                number: 0
            };
            this.handleSwitch = this.handleSwitch.bind(this);
        }
        handleSwitch(event) {
            this.setState({
                number: this.state.number === 0?1:0
            });
        }
        render() {
            return (
                <div className='app2-container'>
                    <TransitionGroup>
                        <CSSTransition
                            key={this.state.number}
                            timeout={500}
                            classNames='app2'
                            unmountOnExit={true}
                        >
                            <div className='app2-square'>{this.state.number}</div>
                        </CSSTransition>
                    </TransitionGroup>
                    <button className='app2-btn' onClick={this.handleSwitch}>切换</button>
                </div>
            );
        }
    }
    

    CSS代码同上
    我们和纯CSSTransition用法进行比较,发现有以下几点不同:

    1. CSSTransition组件上层嵌了一层TransitionGroup组件
    2. 没有使用in属性作为控制组件添加enterexit类的手段,而是使用了key属性。
      我们先来看一下效果:
      TransitionGroup组件使用效果
      将动画速度调低,来看一下子节点类选择器的变化:
      子节点类选择器变化过程
      现象:我们可以看到在点击切换节点内容的时候,会新增了一个新的dom。此时新老dom并存。老dom新增了-exit和-exit-active两个选择器。新dom新增了-enter和-enter-active两个选择器。这样的情况确实会出现我们看到的效果。
      原因:刚开始学习react的时候,我们就听过了react的虚拟dom渲染优化机制。它是有一个diff算法,比较出存在变化的地方,然后针对性地进行重新渲染。diff机制其实用到的就是key,我们两次key不一样,react就会卸载旧key对应的节点,装载新key对应的节点。但是为什么会有动画效果,而不是立马卸载装载呢?这就是TransitionGroup组件的特别之处,它会保存住即将被卸载的children,并在动画执行完毕将其进行移除。

    五、路由转场动画

    针对上面对TransitionGroupCSSTransition组件的运用,想象一下,其实我们把案例中的子组件div换成对应的Route路由组件,讲道理就能实现转场动画了。
    但是diff算法需要的key,用什么来表示呢?你应该一下子就能想起来了,每个Route路由的pathname路径都不一样,用它来简直完美。

    注意: 新旧两个节点,一定要在同一位置才符合我们enter、exit选择器规定的transform属性,并作出X轴方向上平移动画。
    所以我将父节点TransitionGroup的position设为releative,子节点HomePage和SecondPage设为绝对定位,且top和left都为0

    代码如下:

    const RouteModule = function (props) {
        return (
            <TransitionGroup style={{position: 'releative'}}>
                <CSSTransition
                    //key为路由路径,因为使用高阶组件withRouter
                    //所以会有location和history属性
                    key={props.location.pathname}
                    timeout={1000}
                    classNames={'app3'}
                >
                    //这里注意一点,Switch组件是根据location属性中的url进行匹配子组件的
                    //如果这个地方不对应设置location,那么旧的Switch组件
                    //就会使用新的location去匹配子组件。这样会造成新旧组件为同一个的bug
                    <Switch location={props.location}>
                        <Route exact path={'/'} component={HomePage} name={'首页'} />
                        <Route path={'/second'} component={SecondPage} name={'第二页'} />
                    </Switch>
                </CSSTransition>
            </TransitionGroup>
        );
    };
    
    export default class DemoApp3 extends React.Component {
        render() {
            const Routes = withRouter(RouteModule);
            return (
                <Router>
                    <Routes />
                </Router>
            );
        }
    }
    

    来看一下效果:


    转场效果图

    应该算是符合预期了,但是push与pop时的效果是一样的,因为我们并没有进行区分。

    六、实现Push、Pop效果分离

    上面我们其实已经做出了Push效果,但是Pop的效果其实是没有处理的,因为选择器都是同一个。
    Pop的时候,enter与exit选择器的效果应该和push时的正好相反才对,所以我们先对CSS样式进行处理。

    /*push*/
    .app4-push-enter {
        opacity: 0;
        transform: translateX(100%);
    }
    
    .app4-push-enter-active {
        opacity: 1;
        transform: translateX(0);
        transition: all 500ms ease;
    }
    
    .app4-push-exit {
        opacity: 1;
        transform: translateX(0);
    }
    
    .app4-push-exit-active {
        opacity: 0;
        transform: translateX(-100%);
        transition: all 500ms ease;
    }
    
    /*pop*/
    .app4-pop-enter {
        opacity: 0;
        transform: translateX(-100%);
    }
    
    .app4-pop-enter-active {
        opacity: 1;
        transform: translateX(0);
        transition: all 500ms ease;
    }
    
    .app4-pop-exit {
        opacity: 1;
        transform: translateX(0);
    }
    
    .app4-pop-exit-active {
        opacity: 0;
        transform: translateX(100%);
        transition: all 500ms ease;
    }
    

    CSS的类选择器完成后,下一步就是在适当的时机,对TransitionGroup子组件添加这些对应的选择器。
    一开始我的做法是通过路由中的location.action去判断是否为PUSH还是POP操作,并对应设置CSSTransition组件的选择器前缀classNames属性。

    <CSSTransition
                    key={props.location.pathname}
                    timeout={500}
                    classNames={props.history.action === 'PUSH'?'app4-push':'app4-pop'}
                >
    

    但是效果貌似出了些问题~~~

    667.gif
    确定CSS中的逻辑是没有问题的情况下。将动画速度调低,我们看一下子组件类选择器的变化情况:
    子组件类选择器添加过程
    我们来分析一下:
    当点击Push的时候。按照我们的思路,secondPage组件应该是添加push-enter选择器,home组件应该添加push-exit选择器。
    点击Pop的时候,home组件应该添加pop-enter选择器,secondPage组件应该添加pop-exit选择器。
    但是现象却是点击push时,home组件添加了pop-exit选择器。
    点击pop时,second组件添加了push-exit选择器。
    为什么会这样???
    静下心来继续分析:debugger调试发现,其实组件的location.action值默认是pop。当第一次push操作时,新组件的action变为PUSH,而旧组件action默认为POP,所以自然会添加pop-exit选择器。第二次pop操作时,因为旧组件此时的action为PUSH,所以添加了push.exit。
    action:pop     push操作         action:push
    旧组件   -------------------->   新组件
    新组件  <--------------------    旧组件
                   pop操作
    

    那么如何让push操作的时候,新旧子组件分别添加push-enter、enter-exit。pop操作的时候,新旧子组件分别添加pop-enter、pop-exit选择器呢?

    经过查找资料,发现TransitionGroup组件有个childFactory属性可以强行覆盖子组件的类选择器名称。

    <TransitionGroup
                style={{position: 'releative'}}
                //childFactory属性为一个function
                //这个function的第一个参数child实际上就是TransitionGroup子组件
                //通过React.cloneElement方法重新克隆子组件,并根据当前的操作类型去设置类选择器前缀
                //这样,当操作为push时,子组件的类选择器前缀并不是根据其本身的location.action去分别命名。
                //而是根据最新的action类型设置
                childFactory={child => React.cloneElement(
                      child,
                      {classNames: props.history.action === 'PUSH'?'app4-push':'app4-pop'}
                )}
    >
    

    最终效果如下:


    react转场实战的最终效果

    七、总结

    本篇文章是针对React知识点的第一篇文章。围绕这个需求,可以提升对React开发时遇到问题如何适当调试的技能,加深对react-router-domreact-transition-group两个插件的理解和运用。文中所涉及的代码全部可在这里下载查看,如果对您有所帮助,希望给个Star
    本人是初入前端,水平有限。如若您发现问题,望及时指出,谢谢~🙂

    相关文章

      网友评论

        本文标题:React-实现仿原生App的转场动画

        本文链接:https://www.haomeiwen.com/subject/fingzqtx.html