
一、前言
最近這兩個星期,已經(jīng)從jQuery的泥潭里抽出身來,開始學(xué)習(xí)React框架。React其實是一個UI框架,其功能性比較單一,需要依賴各種插件進(jìn)行開發(fā)。這些React插件集合起來,就是鼎鼎大名的React全家桶。
目前我了解并使用過的有如下幾個插件:
- react-router-dom React路由跳轉(zhuǎn)插件
- react-loadable 一個動態(tài)導(dǎo)入加載UI的高階組件
- redux, react-redux-dom react函數(shù)響應(yīng)式編程框架,類似于iOS中的ReactiveCocoa數(shù)據(jù)驅(qū)動視圖的思想。剛開始學(xué)的時候,配上ES6、7、8的語法糖,能繞的你不要不要的
- prop-types 為弱語法的JS提供強類型操作
- axios React界可以與ajax相抗衡的網(wǎng)絡(luò)庫
- react-motion React中的彈性動畫庫
- react-transition-group React中個人目前感覺最好用的動畫庫
待了解的插件及知識點:- react-saga、react-thunk、immutable、webpack打包
二、需求產(chǎn)出
本篇文章是我在React學(xué)習(xí)過程中,打算將用過的知識點串起來時遇到的一個需求問題。因為還是iOS出身,所以梳理知識點的時候,還是想按照移動端那種形式去整理,如下圖所示。很屌絲,終歸還是一時難忘iOS。

此Demo現(xiàn)在還在編寫完善當(dāng)中,暫不公開。
Demo中的路由跳轉(zhuǎn)肯定就是用的react-router-dom這個插件了。但是這個插件進(jìn)行路由切換時,效果很僵硬,沒有過渡效果。這對于追求用戶體驗的iOS開發(fā)者來說肯定是不能接受的?。?!所以我要做的,就是對react路由切換加上仿原生的轉(zhuǎn)場動畫。
三、react-router-dom
簡單實現(xiàn)一下純router插件的路由跳轉(zhuǎn),效果及代碼如下:
要說的都在代碼注釋中!??!先不要去管CSS樣式
import React from 'react';
import {
//以html5提供的history api形式實現(xiàn)的路由
//一般其作為項目的原始組件進(jìn)行包裹
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>
);
}
}
默認(rèn)的路由切換效果如下:

四、react-transition-group
上面也略有介紹,react-transition-group是react中的一個很不錯的動畫庫。為什么我會想到用它去實現(xiàn)轉(zhuǎn)場動畫?因為我了解的react動畫庫就兩個,還有一個react-motion是彈性動畫庫,顯然不合適。
眾所周知,JS實現(xiàn)動畫最方便的還要屬jQuery。其提供了一系列動畫函數(shù),好用且方便。但是操控的都是真實dom,這顯然與React的虛擬dom思想相違背,所以沒有考慮jQuery去實現(xiàn)需求。
1、 CSSTransition實現(xiàn)單一組件動畫
CSSTransition單獨使用時,有兩個比較重要的屬性。in和classNames。
- classNames屬性: CSSTransition子組件動態(tài)類選擇器名前綴。
-
in屬性:
當(dāng)in為true時,CSSTransition的子組件會先添加${classNames}-enter類,下一個tick會添加${classNames}-enter-active類。
當(dāng)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`的類
* 當(dāng)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);
/*這個移動過程我們做個動畫,動畫持續(xù)時長500毫秒*/
transition: all 500ms ease;
}
.app1-exit {
opacity: 1;
transform: translateX(0);
}
.app1-exit-active {
opacity: 0;
transform: translateX(-100%);
transition: all 500ms ease;
}
效果符合預(yù)期:

2、TransitionGroup實現(xiàn)多組件協(xié)調(diào)動畫
我們的路由跳轉(zhuǎn),實際上是有兩個組件同時動畫的。即第一個頁面組件和第二個頁面組件。所以單純的CSSTransition組件不能滿足需求。
以代碼為例進(jìn)行講解:
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用法進(jìn)行比較,發(fā)現(xiàn)有以下幾點不同:
- CSSTransition組件上層嵌了一層TransitionGroup組件
- 沒有使用in屬性作為控制組件添加enter和exit類的手段,而是使用了key屬性。
我們先來看一下效果:
TransitionGroup組件使用效果
將動畫速度調(diào)低,來看一下子節(jié)點類選擇器的變化:
子節(jié)點類選擇器變化過程
現(xiàn)象:我們可以看到在點擊切換節(jié)點內(nèi)容的時候,會新增了一個新的dom。此時新老dom并存。老dom新增了-exit和-exit-active兩個選擇器。新dom新增了-enter和-enter-active兩個選擇器。這樣的情況確實會出現(xiàn)我們看到的效果。
原因:剛開始學(xué)習(xí)react的時候,我們就聽過了react的虛擬dom渲染優(yōu)化機制。它是有一個diff算法,比較出存在變化的地方,然后針對性地進(jìn)行重新渲染。diff機制其實用到的就是key,我們兩次key不一樣,react就會卸載舊key對應(yīng)的節(jié)點,裝載新key對應(yīng)的節(jié)點。但是為什么會有動畫效果,而不是立馬卸載裝載呢?這就是TransitionGroup組件的特別之處,它會保存住即將被卸載的children,并在動畫執(zhí)行完畢將其進(jìn)行移除。
五、路由轉(zhuǎn)場動畫
針對上面對TransitionGroup和CSSTransition組件的運用,想象一下,其實我們把案例中的子組件div換成對應(yīng)的Route路由組件,講道理就能實現(xiàn)轉(zhuǎn)場動畫了。
但是diff算法需要的key,用什么來表示呢?你應(yīng)該一下子就能想起來了,每個Route路由的pathname路徑都不一樣,用它來簡直完美。
注意: 新舊兩個節(jié)點,一定要在同一位置才符合我們enter、exit選擇器規(guī)定的transform屬性,并作出X軸方向上平移動畫。
所以我將父節(jié)點TransitionGroup的position設(shè)為releative,子節(jié)點HomePage和SecondPage設(shè)為絕對定位,且top和left都為0
代碼如下:
const RouteModule = function (props) {
return (
<TransitionGroup style={{position: 'releative'}}>
<CSSTransition
//key為路由路徑,因為使用高階組件withRouter
//所以會有l(wèi)ocation和history屬性
key={props.location.pathname}
timeout={1000}
classNames={'app3'}
>
//這里注意一點,Switch組件是根據(jù)location屬性中的url進(jìn)行匹配子組件的
//如果這個地方不對應(yīng)設(shè)置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>
);
}
}
來看一下效果:

應(yīng)該算是符合預(yù)期了,但是push與pop時的效果是一樣的,因為我們并沒有進(jìn)行區(qū)分。
六、實現(xiàn)Push、Pop效果分離
上面我們其實已經(jīng)做出了Push效果,但是Pop的效果其實是沒有處理的,因為選擇器都是同一個。
Pop的時候,enter與exit選擇器的效果應(yīng)該和push時的正好相反才對,所以我們先對CSS樣式進(jìn)行處理。
/*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的類選擇器完成后,下一步就是在適當(dāng)?shù)臅r機,對TransitionGroup子組件添加這些對應(yīng)的選擇器。
一開始我的做法是通過路由中的location.action去判斷是否為PUSH還是POP操作,并對應(yīng)設(shè)置CSSTransition組件的選擇器前綴classNames屬性。
<CSSTransition
key={props.location.pathname}
timeout={500}
classNames={props.history.action === 'PUSH'?'app4-push':'app4-pop'}
>
但是效果貌似出了些問題~~~

確定CSS中的邏輯是沒有問題的情況下。將動畫速度調(diào)低,我們看一下子組件類選擇器的變化情況:

我們來分析一下:
當(dāng)點擊Push的時候。按照我們的思路,secondPage組件應(yīng)該是添加push-enter選擇器,home組件應(yīng)該添加push-exit選擇器。點擊Pop的時候,home組件應(yīng)該添加pop-enter選擇器,secondPage組件應(yīng)該添加pop-exit選擇器。但是現(xiàn)象卻是點擊push時,home組件添加了pop-exit選擇器。
點擊pop時,second組件添加了push-exit選擇器。
為什么會這樣???
靜下心來繼續(xù)分析:debugger調(diào)試發(fā)現(xiàn),其實組件的location.action值默認(rèn)是pop。當(dāng)?shù)谝淮蝡ush操作時,新組件的action變?yōu)镻USH,而舊組件action默認(rèn)為POP,所以自然會添加pop-exit選擇器。第二次pop操作時,因為舊組件此時的action為PUSH,所以添加了push.exit。
action:pop push操作 action:push
舊組件 --------------------> 新組件
新組件 <-------------------- 舊組件
pop操作
那么如何讓push操作的時候,新舊子組件分別添加push-enter、enter-exit。pop操作的時候,新舊子組件分別添加pop-enter、pop-exit選擇器呢?
經(jīng)過查找資料,發(fā)現(xiàn)TransitionGroup組件有個childFactory屬性可以強行覆蓋子組件的類選擇器名稱。
<TransitionGroup
style={{position: 'releative'}}
//childFactory屬性為一個function
//這個function的第一個參數(shù)child實際上就是TransitionGroup子組件
//通過React.cloneElement方法重新克隆子組件,并根據(jù)當(dāng)前的操作類型去設(shè)置類選擇器前綴
//這樣,當(dāng)操作為push時,子組件的類選擇器前綴并不是根據(jù)其本身的location.action去分別命名。
//而是根據(jù)最新的action類型設(shè)置
childFactory={child => React.cloneElement(
child,
{classNames: props.history.action === 'PUSH'?'app4-push':'app4-pop'}
)}
>
最終效果如下:

七、總結(jié)
本篇文章是針對React知識點的第一篇文章。圍繞這個需求,可以提升對React開發(fā)時遇到問題如何適當(dāng)調(diào)試的技能,加深對react-router-dom和react-transition-group兩個插件的理解和運用。文中所涉及的代碼全部可在這里下載查看,如果對您有所幫助,希望給個Star。
本人是初入前端,水平有限。如若您發(fā)現(xiàn)問題,望及時指出,謝謝~??

