此前,我使用了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
renderor 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
掃碼訪問:

