React-实现仿原生App的转场动画
一、前言
最近这两个星期,已经从jQuery的泥潭里抽出身来,开始学习React框架。React其实是一个UI框架,其功能性比较单一,需要依赖各种插件进行开发。这些React插件集合起来,就是鼎鼎大名的React全家桶。
目前我了解并使用过的有如下几个插件:
- react-router-dom React路由跳转插件
- react-loadable 一个动态导入加载UI的高阶组件
- redux, react-redux-dom react函数响应式编程框架,类似于iOS中的ReactiveCocoa数据驱动视图的思想。刚开始学的时候,配上ES6、7、8的语法糖,能绕的你不要不要的
- prop-types 为弱语法的JS提供强类型操作
- axios React界可以与ajax相抗衡的网络库
- react-motion React中的弹性动画库
- react-transition-group React中个人目前感觉最好用的动画库
待了解的插件及知识点:- react-saga、react-thunk、immutable、webpack打包
二、需求产出
本篇文章是我在React学习过程中,打算将用过的知识点串起来时遇到的一个需求问题。因为还是iOS出身,所以梳理知识点的时候,还是想按照移动端那种形式去整理,如下图所示。很屌丝,终归还是一时难忘iOS。
此Demo现在还在编写完善当中,暂不公开。
Demo中的路由跳转肯定就是用的react-router-dom这个插件了。但是这个插件进行路由切换时,效果很僵硬,没有过渡效果。这对于追求用户体验的iOS开发者来说肯定是不能接受的!!!所以我要做的,就是对react路由切换加上仿原生的转场动画。
三、react-router-dom
简单实现一下纯router插件的路由跳转,效果及代码如下:
要说的都在代码注释中!!!先不要去管CSS样式
import React from 'react';
import {
//以html5提供的history api形式实现的路由
//一般其作为项目的原始组件进行包裹
BrowserRouter as Router,
//路由组件,path指定匹配的路由,component指定路由匹配时展示的组件
Route,
//Route组件包裹器
Switch,
//一个高阶组件,为组件提供location,history等对象
withRouter
} from 'react-router-dom';
//自定义HomePage组件
import HomePage from '../page/home';
//自定义SecondPage组件
import SecondPage from '../page/second';
const RouteModule = function (props) {
return (
<Switch history={props.history}>
<Route exact path={'/'} component={HomePage} name={'首页'} />
<Route path={'/second'} component={SecondPage} name={'第二页'} />
</Switch>
);
};
export default class DemoApp5 extends React.Component {
render() {
const Routes = withRouter(RouteModule);
return (
<Router>
<Routes />
</Router>
);
}
}
默认的路由切换效果如下:
僵硬的react-router-dom路由切换效果.gif
四、react-transition-group
上面也略有介绍,react-transition-group是react中的一个很不错的动画库。为什么我会想到用它去实现转场动画?因为我了解的react动画库就两个,还有一个react-motion是弹性动画库,显然不合适。
众所周知,JS实现动画最方便的还要属jQuery。其提供了一系列动画函数,好用且方便。但是操控的都是真实dom,这显然与React的虚拟dom思想相违背,所以没有考虑jQuery去实现需求。
1、 CSSTransition实现单一组件动画
CSSTransition单独使用时,有两个比较重要的属性。in和classNames。
- classNames属性: CSSTransition子组件动态类选择器名前缀。
-
in属性:
当in为true时,CSSTransition的子组件会先添加${classNames}-enter
类,下一个tick会添加${classNames}-enter-active
类。
当in为false时,CSSTransition的子组件会先添加${classNames}-exit
类,下一个tick会添加${classNames}-exit-active
类。
基于上面的知识点,我们先出一个react-transition-group的简单小Demo。
import React from 'react';
import {CSSTransition} from 'react-transition-group';
import './style.css';
/*
* 知识点
* CSSTransition属性in为true时,其子组件会加上`${classNames}-enter`的类
* 然后再下一个tick时,马上加上`${classNames}-enter-active`的类
* 当in为false时,其组件会加上`${classNames}-exit`和`${classNames}-exit-active`类
*
* unmountOnExit为false的时候,其子组件exit后不会卸载,并添加`${classNames}-exit-done`类
* 为true,子组件exit后会卸载
* 这里我们直接卸载,省的其写done样式
* */
export default class DemoApp1 extends React.Component {
constructor(props) {
super(props);
this.state = {
show: true
};
this.handleSwitch = this.handleSwitch.bind(this);
}
handleSwitch() {
this.setState({
show: !this.state.show
});
}
render() {
return (
<div className='app1-container'>
<CSSTransition
in={this.state.show}
classNames='app1'
timeout={500}
unmountOnExit={true}
>
<div className='app1-square' />
</CSSTransition>
<button
onClick={this.handleSwitch}
className='app1-btn'
>切换
</button>
</div>
);
}
}
CSS样式如下:
.app1-enter {
/*初始透明度为0*/
opacity: 0;
/*初始偏移量为100%*/
transform: translateX(100%);
}
.app1-enter-active {
/*随后透明度为1*/
opacity: 1;
/*随后偏移量回归原位*/
transform: translateX(0);
/*这个移动过程我们做个动画,动画持续时长500毫秒*/
transition: all 500ms ease;
}
.app1-exit {
opacity: 1;
transform: translateX(0);
}
.app1-exit-active {
opacity: 0;
transform: translateX(-100%);
transition: all 500ms ease;
}
效果符合预期:
单一组件动画效果
2、TransitionGroup实现多组件协调动画
我们的路由跳转,实际上是有两个组件同时动画的。即第一个页面组件和第二个页面组件。所以单纯的CSSTransition组件不能满足需求。
以代码为例进行讲解:
export default class DemoApp2 extends React.Component {
constructor(props) {
super(props);
this.state = {
number: 0
};
this.handleSwitch = this.handleSwitch.bind(this);
}
handleSwitch(event) {
this.setState({
number: this.state.number === 0?1:0
});
}
render() {
return (
<div className='app2-container'>
<TransitionGroup>
<CSSTransition
key={this.state.number}
timeout={500}
classNames='app2'
unmountOnExit={true}
>
<div className='app2-square'>{this.state.number}</div>
</CSSTransition>
</TransitionGroup>
<button className='app2-btn' onClick={this.handleSwitch}>切换</button>
</div>
);
}
}
CSS代码同上
我们和纯CSSTransition用法进行比较,发现有以下几点不同:
- CSSTransition组件上层嵌了一层TransitionGroup组件
- 没有使用in属性作为控制组件添加enter和exit类的手段,而是使用了key属性。
我们先来看一下效果:
TransitionGroup组件使用效果
将动画速度调低,来看一下子节点类选择器的变化:
子节点类选择器变化过程
现象:我们可以看到在点击切换节点内容的时候,会新增了一个新的dom。此时新老dom并存。老dom新增了-exit和-exit-active两个选择器。新dom新增了-enter和-enter-active两个选择器。这样的情况确实会出现我们看到的效果。
原因:刚开始学习react的时候,我们就听过了react的虚拟dom渲染优化机制。它是有一个diff算法,比较出存在变化的地方,然后针对性地进行重新渲染。diff机制其实用到的就是key,我们两次key不一样,react就会卸载旧key对应的节点,装载新key对应的节点。但是为什么会有动画效果,而不是立马卸载装载呢?这就是TransitionGroup组件的特别之处,它会保存住即将被卸载的children,并在动画执行完毕将其进行移除。
五、路由转场动画
针对上面对TransitionGroup和CSSTransition组件的运用,想象一下,其实我们把案例中的子组件div换成对应的Route路由组件,讲道理就能实现转场动画了。
但是diff算法需要的key,用什么来表示呢?你应该一下子就能想起来了,每个Route路由的pathname路径都不一样,用它来简直完美。
注意: 新旧两个节点,一定要在同一位置才符合我们enter、exit选择器规定的transform属性,并作出X轴方向上平移动画。
所以我将父节点TransitionGroup的position设为releative,子节点HomePage和SecondPage设为绝对定位,且top和left都为0
代码如下:
const RouteModule = function (props) {
return (
<TransitionGroup style={{position: 'releative'}}>
<CSSTransition
//key为路由路径,因为使用高阶组件withRouter
//所以会有location和history属性
key={props.location.pathname}
timeout={1000}
classNames={'app3'}
>
//这里注意一点,Switch组件是根据location属性中的url进行匹配子组件的
//如果这个地方不对应设置location,那么旧的Switch组件
//就会使用新的location去匹配子组件。这样会造成新旧组件为同一个的bug
<Switch location={props.location}>
<Route exact path={'/'} component={HomePage} name={'首页'} />
<Route path={'/second'} component={SecondPage} name={'第二页'} />
</Switch>
</CSSTransition>
</TransitionGroup>
);
};
export default class DemoApp3 extends React.Component {
render() {
const Routes = withRouter(RouteModule);
return (
<Router>
<Routes />
</Router>
);
}
}
来看一下效果:
转场效果图
应该算是符合预期了,但是push与pop时的效果是一样的,因为我们并没有进行区分。
六、实现Push、Pop效果分离
上面我们其实已经做出了Push效果,但是Pop的效果其实是没有处理的,因为选择器都是同一个。
Pop的时候,enter与exit选择器的效果应该和push时的正好相反才对,所以我们先对CSS样式进行处理。
/*push*/
.app4-push-enter {
opacity: 0;
transform: translateX(100%);
}
.app4-push-enter-active {
opacity: 1;
transform: translateX(0);
transition: all 500ms ease;
}
.app4-push-exit {
opacity: 1;
transform: translateX(0);
}
.app4-push-exit-active {
opacity: 0;
transform: translateX(-100%);
transition: all 500ms ease;
}
/*pop*/
.app4-pop-enter {
opacity: 0;
transform: translateX(-100%);
}
.app4-pop-enter-active {
opacity: 1;
transform: translateX(0);
transition: all 500ms ease;
}
.app4-pop-exit {
opacity: 1;
transform: translateX(0);
}
.app4-pop-exit-active {
opacity: 0;
transform: translateX(100%);
transition: all 500ms ease;
}
CSS的类选择器完成后,下一步就是在适当的时机,对TransitionGroup子组件添加这些对应的选择器。
一开始我的做法是通过路由中的location.action去判断是否为PUSH还是POP操作,并对应设置CSSTransition组件的选择器前缀classNames属性。
<CSSTransition
key={props.location.pathname}
timeout={500}
classNames={props.history.action === 'PUSH'?'app4-push':'app4-pop'}
>
但是效果貌似出了些问题~~~
确定CSS中的逻辑是没有问题的情况下。将动画速度调低,我们看一下子组件类选择器的变化情况:
子组件类选择器添加过程
我们来分析一下:
当点击Push的时候。按照我们的思路,secondPage组件应该是添加push-enter选择器,home组件应该添加push-exit选择器。
点击Pop的时候,home组件应该添加pop-enter选择器,secondPage组件应该添加pop-exit选择器。
但是现象却是点击push时,home组件添加了pop-exit选择器。
点击pop时,second组件添加了push-exit选择器。
为什么会这样???
静下心来继续分析:debugger调试发现,其实组件的location.action值默认是pop。当第一次push操作时,新组件的action变为PUSH,而旧组件action默认为POP,所以自然会添加pop-exit选择器。第二次pop操作时,因为旧组件此时的action为PUSH,所以添加了push.exit。
action:pop push操作 action:push
旧组件 --------------------> 新组件
新组件 <-------------------- 旧组件
pop操作
那么如何让push操作的时候,新旧子组件分别添加push-enter、enter-exit。pop操作的时候,新旧子组件分别添加pop-enter、pop-exit选择器呢?
经过查找资料,发现TransitionGroup组件有个childFactory属性可以强行覆盖子组件的类选择器名称。
<TransitionGroup
style={{position: 'releative'}}
//childFactory属性为一个function
//这个function的第一个参数child实际上就是TransitionGroup子组件
//通过React.cloneElement方法重新克隆子组件,并根据当前的操作类型去设置类选择器前缀
//这样,当操作为push时,子组件的类选择器前缀并不是根据其本身的location.action去分别命名。
//而是根据最新的action类型设置
childFactory={child => React.cloneElement(
child,
{classNames: props.history.action === 'PUSH'?'app4-push':'app4-pop'}
)}
>
最终效果如下:
react转场实战的最终效果
七、总结
本篇文章是针对React知识点的第一篇文章。围绕这个需求,可以提升对React开发时遇到问题如何适当调试的技能,加深对react-router-dom和react-transition-group两个插件的理解和运用。文中所涉及的代码全部可在这里下载查看,如果对您有所帮助,希望给个Star。
本人是初入前端,水平有限。如若您发现问题,望及时指出,谢谢~🙂