使用react-router v4和react-transition-group实现页面路由切换动画效果

近期有个react移动端项目,想要在页面切换中实现动画,设想是页面左右滑入滑出。路由是使用react-router v4版本,所以第一时间去官网上找示例:animated-transitions

<CSSTransition key={location.key} classNames="fade" timeout={300}>
    <Switch location={location}>
        <Route exact path="/hsl/:h/:s/:l" component={HSL} />
        <Route exact path="/rgb/:r/:g/:b" component={RGB} />
        <Route render={() => <div>Not Found</div>} />
    </Switch>
</CSSTransition>;

根据示例尝试了下,发现有以下几个坑:

  1. 使用location.key当作动画节点的key,由于history的pushState对于同样地址的页面,也会生成不一样的key,所以会导致点击导航过快的话,会产生多个页面节点,因为key不同,无法复用既有节点,所以react会生成新的节点参与动画。解决办法:使用location.pathname当key。(这里还有坑,后面会讲)
  2. 嵌套路由页面,即某个页面里有使用嵌套路由的话,子路由地址变化也会导致整个根路由页面一起切换,原因依然是上边的根路由页面使用location.key。但是改用location.pathname后也不行,因为还是会跟着子路由变化。所以解决办法是需要使用父级路由定义的path当作CSSTransiiton的key。这样子子路由切换时,父级页面的key不变,所以原地复用,不会重载。
  3. 对于左右滑入的动画,需要知道页面前进后退,页面前进的话想要从右滑入,页面后退需要从左滑入(当前页面效果相反)。解决办法:自己维护一个浏览记录,即通过sessionStorage记录用户访问过的所有的location.key,当组件更新时,判断当前location.key是否在历史里,在的话判断为后退,并清除这个页面在历史里以后的所以记录;否则则认为是前进(新页面)。
  4. 知道了前进后退后,页面动画依然不会如预期进行,尤其是前进后退挨着的时刻。因为TransitionGroup中exiting状态的节点,我们是无法访问并自由控制修改其属性的,所以正好与前一刻相反的动画,会存在exited的节点上的classNames依然是旧的。解决办法:通过cloneElement强制覆盖其上面绑定的classNames。关于这一点有一些讨论,这里有篇文章,还有一篇issue讨论

解决了以上所有问题后,封装了一个父级组件,对于需要路由动画的地方,直接调用就可以。

import React, { Component } from 'react';
import { TransitionGroup, CSSTransition } from 'react-transition-group';
import { Switch, withRouter } from 'react-router';
import PropTypes from 'prop-types';

const HISTORIES_KEY = 'HISTORIES_KEY';
const histories = (sessionStorage.getItem(HISTORIES_KEY) || '').split(',').filter(Boolean);
let timer;
const isHistoryPush = location => {
    const index = histories.lastIndexOf(location.key);

    clearTimeout(timer);

    timer = setTimeout(function() {
        if (index > -1) {
            histories.splice(index + 1);
        } else {
            histories.push(location.key);
        }

        sessionStorage.setItem(HISTORIES_KEY, histories.join(','));
    }, 50);

    return index < 0;
};

@withRouter
class AnimatedRouter extends Component {
    static propTypes = {
        className: PropTypes.string,
        transitionKey: PropTypes.any
    };

    render() {
        const { className, location, children } = this.props;
        const classNames = isHistoryPush(location) ? 'page-animation-enter' : 'page-animation-exit';

        return (
            <TransitionGroup
                className={'page-animation-container' + (className ? ' ' + className : '')}
                childFactory={child =>
                    React.cloneElement(child, {
                        classNames
                    })
                }>
                <CSSTransition key={this.props.transitionKey || location.pathname} timeout={300}>
                    <Switch location={location}>{children}</Switch>
                </CSSTransition>
            </TransitionGroup>
        );
    }
}

export default AnimatedRouter;

上面的主要的代码逻辑。我已经将其封装成npm包并发布成react-animated-router,使用方式如下:

import React, { Component } from 'react';
import { render } from 'react-dom';
import { Route, Redirect, Switch, BrowserRouter } from 'react-router-dom';
import AnimatedRouter from 'react-animated-router'; //我们的AnimatedRouter组件
import 'react-animated-router/animate.css'; //引入默认的动画样式定义

import Login from 'modules/Login';
import Signup from 'modules/Signup';

class App extends Component {
    render() {
        /** 假如你的代码如此,则可直接使用最下方代码代替,即直接使用 AnimatedRouter 替换掉Switch
         * return (
         *  <Switch>
         *       <Route path="/login" component={Login} />
         *       <Route path="/signup" component={Signup} />
         *       <Redirect to="/login" />
         *   </Switch>
         * );
         **/

        return (
            <AnimatedRouter>
                <Route path="/login" component={Login} />
                <Route path="/signup" component={Signup} />
                <Redirect to="/login" />
            </AnimatedRouter>
        );
    }
}

注:AnimatedRouter即为封装后的路由动画组件。

完整的组件还包括css部分,有兴趣的可以移步我的github查看。效果可以看微博视频

本文已经有 3 条评论,继续盖楼啦!

  1. 但是在IOS上就不行了
    因为iphone上有一个左滑前进右滑后退的操作。
    右滑后退到上一页后,又会触发一次动画,就很怪….
    唉,怪之怪IOS太奇葩

发表评论 取消回复