近期有个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>;
根据示例尝试了下,发现有以下几个坑:
- 使用location.key当作动画节点的key,由于history的pushState对于同样地址的页面,也会生成不一样的key,所以会导致点击导航过快的话,会产生多个页面节点,因为key不同,无法复用既有节点,所以react会生成新的节点参与动画。解决办法:使用location.pathname当key。(这里还有坑,后面会讲)
- 嵌套路由页面,即某个页面里有使用嵌套路由的话,子路由地址变化也会导致整个根路由页面一起切换,原因依然是上边的根路由页面使用location.key。但是改用location.pathname后也不行,因为还是会跟着子路由变化。所以解决办法是需要使用父级路由定义的path当作CSSTransiiton的key。这样子子路由切换时,父级页面的key不变,所以原地复用,不会重载。
- 对于左右滑入的动画,需要知道页面前进后退,页面前进的话想要从右滑入,页面后退需要从左滑入(当前页面效果相反)。解决办法:自己维护一个浏览记录,即通过sessionStorage记录用户访问过的所有的location.key,当组件更新时,判断当前location.key是否在历史里,在的话判断为后退,并清除这个页面在历史里以后的所以记录;否则则认为是前进(新页面)。
- 知道了前进后退后,页面动画依然不会如预期进行,尤其是前进后退挨着的时刻。因为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即为封装后的路由动画组件。