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

时间:2021-1-8 作者:admin

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

  1. <Route render={() => Not Found} />
  2. ;

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

  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讨论

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

  1. import React, { Component } from ‘react’;

  2. import { TransitionGroup, CSSTransition } from ‘react-transition-group’;

  3. import { Switch, withRouter } from ‘react-router’;

  4. import PropTypes from ‘prop-types’;

  5. const HISTORIES_KEY = ‘HISTORIES_KEY’;

  6. const histories = (sessionStorage.getItem(HISTORIES_KEY) || ”).split(‘,’).filter(Boolean);

  7. let timer;

  8. const isHistoryPush = location => {

  9. const index = histories.lastIndexOf(location.key);

  10. clearTimeout(timer);

  11. timer = setTimeout(function() {

  12. if (index > -1) {

  13. histories.splice(index + 1);

  14. } else {

  15. histories.push(location.key);

  16. }

  17. sessionStorage.setItem(HISTORIES_KEY, histories.join(‘,’));

  18. }, 50);

  19. return index < 0;

  20. };

  21. @withRouter

  22. class AnimatedRouter extends Component {

  23. static propTypes = {

  24. className: PropTypes.string,

  25. transitionKey: PropTypes.any

  26. };

  27. render() {

  28. const { className, location, children } = this.props;

  29. const classNames = isHistoryPush(location) ? ‘page-animation-enter’ : ‘page-animation-exit’;

  30. return (

  31. <TransitionGroup

  32. className={‘page-animation-container’ + (className ? ‘ ‘ + className : ”)}

  33. childFactory={child =>

  34. React.cloneElement(child, {

  35. classNames

  36. })

  37. }>

  38. <CSSTransition key={this.props.transitionKey || location.pathname} timeout={300}>

  39. {children}

  40. );

  41. }

  42. }

  43. export default AnimatedRouter;

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

  1. import React, { Component } from ‘react’;

  2. import { render } from ‘react-dom’;

  3. import { Route, Redirect, Switch, BrowserRouter } from ‘react-router-dom’;

  4. import AnimatedRouter from ‘react-animated-router’; //我们的AnimatedRouter组件

  5. import ‘react-animated-router/animate.css’; //引入默认的动画样式定义

  6. import Login from ‘modules/Login’;

  7. import Signup from ‘modules/Signup’;

  8. class App extends Component {

  9. render() {

  10. /** 假如你的代码如此,则可直接使用最下方代码代替,即直接使用 AnimatedRouter 替换掉Switch

    • return (
    • );
  11. **/

  12. return (

  13. );

  14. }

  15. }

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

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

声明:本文内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎进行举报,并提供相关证据,工作人员会在5个工作日内联系你,一经查实,本站将立刻删除涉嫌侵权内容。