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

3,330 阅读3分钟
原文链接: www.qiqiboy.com

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

  1. <CSSTransition key={location.key} classNames="fade" timeout={300}>
  2. <Switch location={location}>
  3. <Route exact path="/hsl/:h/:s/:l" component={HSL} />
  4. <Route exact path="/rgb/:r/:g/:b" component={RGB} />
  5. <Route render={() => <div>Not Found</div>} />
  6. </Switch>
  7. </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讨论

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

  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. <Switch location={location}>{children}</Switch>
  40. </CSSTransition>
  41. </TransitionGroup>
  42. );
  43. }
  44. }
  45. 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
  11. * return (
  12. * <Switch>
  13. * <Route path="/login" component={Login} />
  14. * <Route path="/signup" component={Signup} />
  15. * <Redirect to="/login" />
  16. * </Switch>
  17. * );
  18. **/
  19. return (
  20. <AnimatedRouter>
  21. <Route path="/login" component={Login} />
  22. <Route path="/signup" component={Signup} />
  23. <Redirect to="/login" />
  24. </AnimatedRouter>
  25. );
  26. }
  27. }

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

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