此前,我使用了react-router库来完成单页应用的路由,从而实现组件之间的切换能力。然而,默认页面的切换是非常生硬的,为了让页面切换更加缓和与舒适,通常的方案就是过渡动画。
这里我调研了2种实现方案,它们都能够为react-router实现路由切换时的过渡效果,第1种是react官方自带的ReactCSSTransitionGroup(官方,推荐),第2种则是react-router-transition(非官方)。
下面,我会基于ReactCSSTransitionGroup来分析页面过渡的简单原理以及编程细节,而react-router-transition则大同小异,因此不做赘述。
ReactCSSTransitionGroup
安装
这个库是react官方自带的,它实现于react/lib/ReactCSSTransitionGroup.js。
你可以通过import直接导入这个文件,或者通过命令来安装一个便捷的别名包(仅仅是指向react/lib/ReactCSSTransitionGroup.js):
- npm install –save react-addons-css-transition-group
原理
ReactCSSTransitionGroup也是一个react组件,我们将在react-router的路由容器组件中引用它,让它替我们在路由切换的时候实现页面间的过渡动画。
首先看一下我的路由配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
ReactDOM.render( ( <Provider store={store}> <Router history={history}> <Route path="/" component={Container}> <IndexRoute component={MsgListPage} /> <Route path="msg-list-page" component={MsgListPage}/> <Route path="msg-detail-page/:msgId" component={MsgDetailPage}/> <Route path="msg-create-page" component={MsgCreatePage}/> <Route path="menu-page" component={MenuPage}/> </Route> </Router> </Provider> ), document.getElementById(‘reactRoot‘) ); |
一个很简单的路由配置,所有子路由的父容器都是Container组件,路由切换时react-router会将代表子路由的组件(例如MsgListPage)填充到Container的props.children孩子属性中。
既然Container组件是容纳子路由组件的容器,那么可以想到当子路由切换时:Conainter的props.children经历了从老的组件变为了新的组件的过程,如果可以在这个过程中稍作手脚是有机会实现新老组件的平滑过渡的。
先来看一下当前Container当前实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
import React from "react"; export default class Container extends React.Component { constructor(props, context) { super(props, context); } componentWillMount() { document.body.style.margin = "0px"; // 这是防止页面被拖拽 document.body.addEventListener(‘touchmove‘, (ev) => { ev.preventDefault(); }); } render() { return ( <div id="reactContainer"> { this.props.children } </div> ); } } |
它将子路由组件(也就是this.props.children)直接填充了进来,这样实现虽然能够完成路由切换,但是它没有任何的过渡效果。
下面利用ReactCSSTransitionGroup实现过渡效果,代码变成了这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
import React from "react"; import ReactCSSTransitionGroup from "react-addons-css-transition-group"; import style from "./Container.css"; export default class Container extends React.Component { constructor(props, context) { super(props, context); } componentWillMount() { document.body.style.margin = "0px"; // 这是防止页面被拖拽 document.body.addEventListener(‘touchmove‘, (ev) => { ev.preventDefault(); }); } render() { return ( <ReactCSSTransitionGroup transitionName="transitionWrapper" component="div" className={style.transitionWrapper} transitionEnterTimeout={300} transitionLeaveTimeout={300}> <div key={this.props.location.pathname} style={{position:"absolute", width: "100%"}}> { this.props.children } </div> </ReactCSSTransitionGroup> ); } } |
我们直接套用了ReactCSSTransitionGroup组件,并将子路由组件(this.props.children)包裹在其内部,这样做的目的是:当子路由组件切换时,ReactCSSTransitionGroup可以拦截其内部新老组件的交替过程,从而实现老组件消逝,新组件出现的过渡视觉。
说了那么多,不如看一下切换路由的瞬间DOM树的样子,更加便于理解:
外层div是ReactCSSTransitionGroup引入的父<div>,它内部是有2个子<div>是这段代码引入的:
1 2 |
<div key={this.props.location.pathname} style={{position:"absolute", width: "100%"}}> |
默认同一时刻应该只有1个路由组件,那么<div>为什么会出现2个呢?因为ReactCSSTransitionGroup拦截了子路由切换的过程,它在组件替换前将前1个子组件备份了起来,在替换后将新老2个子组件一起填充到父<div>中并开始执行过渡动画,当动画结束后它将老组件移除只保留下新组件:
为什么子<div>要有一个key属性呢?因为ReactCSSTransitionGroup在过渡期间同时维护新老组件需要一个唯一标识加以区分,因为location.pathname代表当前访问的完整路径(包括_k=…),所以用它最合适不过。
CSS动画
至于动画是怎么实现的?第一张图片里你应该可以看到,它为2个子<div>添加了对应的class,一个是enter进入的意思,另外一个是leave离开的意思,我们只需要定义对应的css实现transition动画既可(注意<ReactCSSTransitionGroup>的transitionName属性定义了下述class的前缀):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
:global(.transitionWrapper-enter) { opacity: 0.01; transition: opacity 30000ms ease-in; } :global(.transitionWrapper-enter.transitionWrapper-enter-active) { opacity: 1; } :global(.transitionWrapper-leave) { opacity: 1; transition: opacity 30000ms ease-in; } :global(.transitionWrapper-leave.transitionWrapper-leave-active) { opacity: 0; } .transitionWrapper { position: relative; } |
这里,:global(classname)的用法是css-loader插件提供的,默认所有css都是通过css-loader局部编译的,从而保证跨组件css名字不冲突。
然而ReactCSSTransitionGroup组件不支持我们控制这些动画class的命名规则,因此我们只能使用全局css,通过:global修饰的class或者id都不会被编码,而是在整个app全局生效,这一块知识可以在这里补充学习。
这里我基于transition实现透明度opacity的动画,新组件逐渐显现而老组件逐渐淡化,动画方面可以自行学习。这里重点提一下:
1 |
.transitionWrapper-enter.transitionWrapper-enter-active |
我们通常见过2种css表达:
- .class1 .class2,中间是一个空格,表示class1孩子里的class2元素都应用某css规则。
- .class1,.class2,中间是一个逗号,表示class1和class2都应用某css规则。
这里.class1.class2是连续写的,表示同时满足class1和class2的元素应用css规则。
为什么不好用?
很多朋友用ReactCSSTransitionGroup发现路由切换动画异常,不符合预期的效果,怎么调试都不行,其实本质都是对原理不够了解。
问题关键在于CSS控制有问题,如果你理解了上述ReactCSSTransitionGroup实现的原理,那么你应该知道新老组件同时出现的时候属于过渡阶段,它们顺序堆积在父<div>中(默认<div>是从上而下堆砌的)。
为了实现过渡效果,理所应当让2个组件重叠在屏幕中央,然后一个淡入一个淡出。因此这就要求子组件要绝对定位(position:absolute),因此你可以看到我给transitionWrapper应用了position:relative,并给<div key=…>应用了position:absolute,width:100%,就是这个道理。
为什么报错?
如果你发现console里有这样的报错:
Warning: setState(…): Cannot update during an existing state transition (such as within
render
or another component’s constructor). Render methods should be a pure function of props and state; constructor side-effects are an anti-pattern, but can be moved tocomponentWillMount
.
那么说明你在组件的render或者constructor里调用了setState方法,这些应该移到componentWillMount中执行。
我用的是react-redux,之前的某些组件在构造函数里调用了action触发了state修改也被警告了,因此我将初始化组件用的action调用挪到了componentWillMount中,问题迎刃而解。
体验
代码:https://github.com/owenliang/react
扫码访问: