原文地址javascript
在平常開發中,頁面切換時的轉場動畫是比較基礎的一個場景。在react項目當中,咱們通常都會選用react-router
來管理路由,可是react-router卻並無提供相應的轉場動畫功能,而是很是生硬的直接替換掉組件。必定程度上來講,體驗並非那麼友好。css
爲了在react中實現動畫效果,其實咱們有不少的選擇,好比:react-transition-group
,react-motion
,Animated
等等。可是,因爲react-transition-group給元素添加的enter,enter-active,exit,exit-active這一系列勾子,簡直就是爲咱們的頁面入場離場而設計的。基於此,本文選擇react-transition-group來實現動畫效果。html
接下來,本文就將結合二者提供一個實現路由轉場動畫的思路,權當拋磚引玉~html5
咱們先明確要完成的轉場動畫是什麼效果。以下圖所示:java
首先,咱們先簡要介紹下react-router的基本用法(詳細看官網介紹)。react
這裏咱們會用到react-router提供的BrowserRouter
,Switch
,Route
三個組件。git
// src/App1/index.js export default class App1 extends React.PureComponent { render() { return ( <BrowserRouter> <Switch> <Route exact path={'/'} component={HomePage}/> <Route exact path={'/about'} component={AboutPage}/> <Route exact path={'/list'} component={ListPage}/> <Route exact path={'/detail'} component={DetailPage}/> </Switch> </BrowserRouter> ); } }
如上所示,這是路由關鍵的實現部分。咱們一共建立了首頁
,關於頁
,列表頁
,詳情頁
這四個頁面。跳轉關係爲:github
來看下目前默認的路由切換效果:web
從上面的效果圖中,咱們能夠看到react-router在路由切換時徹底沒有過渡效果,而是直接替換的,顯得很是生硬。api
正所謂工欲善其事,必先利其器,在介紹實現轉場動畫以前,咱們得先學習如何使用react-transition-group。基於此,接下來就將對其提供的CSSTransition和TransitionGroup這兩個組件展開簡要介紹。
CSSTransition是react-transition-group提供的一個組件,這裏簡單介紹下其工做原理。
When the in prop is set to true, the child component will first receive the class example-enter, then the example-enter-active will be added in the next tick. CSSTransition forces a reflow between before adding the example-enter-active. This is an important trick because it allows us to transition between example-enter and example-enter-active even though they were added immediately one after another. Most notably, this is what makes it possible for us to animate appearance.
這是來自官網上的一段描述,意思是當CSSTransition的in屬性置爲true時,CSSTransition首先會給其子組件加上xxx-enter的class,而後在下個tick時立刻加上xxx-enter-active的class。因此咱們能夠利用這一點,經過css的transition屬性,讓元素在兩個狀態之間平滑過渡,從而獲得相應的動畫效果。
相反地,當in屬性置爲false時,CSSTransition會給子組件加上xxx-exit和xxx-exit-active的class。(更多詳細介紹能夠戳官網查看)
基於以上兩點,咱們是否是隻要事先寫好class對應的css樣式便可?能夠作個小demo試試,以下代碼所示:
// src/App2/index.js export default class App2 extends React.PureComponent { state = {show: true}; onToggle = () => this.setState({show: !this.state.show}); render() { const {show} = this.state; return ( <div className={'container'}> <div className={'square-wrapper'}> <CSSTransition in={show} timeout={500} classNames={'fade'} unmountOnExit={true} > <div className={'square'} /> </CSSTransition> </div> <Button onClick={this.onToggle}>toggle</Button> </div> ); } }
/* src/App2/index.css */ .fade-enter { opacity: 0; transform: translateX(100%); } .fade-enter-active { opacity: 1; transform: translateX(0); transition: all 500ms; } .fade-exit { opacity: 1; transform: translateX(0); } .fade-exit-active { opacity: 0; transform: translateX(-100%); transition: all 500ms; }
來看看效果,是否是和頁面的入場離場效果有點類似?
用CSSTransition來處理動畫當然很方便,可是直接用來管理多個頁面的動畫仍是略顯單薄。爲此咱們再來介紹react-transition-group提供的TransitionGroup這個組件。
The <TransitionGroup> component manages a set of transition components (<Transition> and <CSSTransition>) in a list. Like with the transition components, <TransitionGroup> is a state machine for managing the mounting and unmounting of components over time.
如官網介紹,TransitionGroup組件就是用來管理一堆節點mounting和unmounting過程的組件,很是適合處理咱們這裏多個頁面的狀況。這麼介紹彷佛有點難懂,那就讓咱們來看段代碼,解釋下TransitionGroup的工做原理。
// src/App3/index.js export default class App3 extends React.PureComponent { state = {num: 0}; onToggle = () => this.setState({num: (this.state.num + 1) % 2}); render() { const {num} = this.state; return ( <div className={'container'}> <TransitionGroup className={'square-wrapper'}> <CSSTransition key={num} timeout={500} classNames={'fade'} > <div className={'square'}>{num}</div> </CSSTransition> </TransitionGroup> <Button onClick={this.onToggle}>toggle</Button> </div> ); } }
咱們先來看效果,而後再作解釋:
對比App3和App2的代碼,咱們能夠發現此次CSSTransition沒有in屬性了,而是用到了key屬性。可是爲何仍然能夠正常工做呢?
在回答這個問題以前,咱們先來思考一個問題:
因爲react的dom diff機制用到了key屬性,若是先後兩次key不一樣,react會卸載舊節點,掛載新節點。那麼在上面的代碼中,因爲key變了,舊節點難道不是應該立馬消失,可是爲何咱們還能看到它淡出的動畫過程呢?
關鍵就出在TransitionGroup身上,由於它在感知到其children變化時,會先保存住即將要被移除的節點,而在其動畫結束時纔會真正移除該節點。
因此在上面的例子中,當咱們按下toggle按鈕時,變化的過程能夠這樣理解:
<TransitionGroup> <div>0</div> </TransitionGroup> ⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️ <TransitionGroup> <div>0</div> <div>1</div> </TransitionGroup> ⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️ <TransitionGroup> <div>1</div> </TransitionGroup>
如上所解釋,咱們徹底能夠巧妙地借用key值的變化來讓TransitionGroup來接管咱們在過渡時的頁面建立和銷燬工做,而僅僅須要關注如何選擇合適的key值和須要什麼樣css樣式來實現動畫效果就能夠了。
基於前文對react-router和react-transition-group的介紹,咱們已經掌握了基礎,接下來就能夠將二者結合起來作頁面切換的轉場動畫了。
在上一小節的末尾有提到,用了TransitionGroup以後咱們的問題變成如何選擇合適的key值。那麼在路由系統中,什麼做爲key值比較合適呢?
既然咱們是在頁面切換的時候觸發轉場動畫,天然是跟路由相關的值做爲key值合適了。而react-router中的location
對象就有一個key屬性,它會隨着瀏覽器中的地址發生變化而變化。然而,在實際場景中彷佛並不適合,由於query參數或者hash變化也會致使location.key發生變化,但每每這些場景下並不須要觸發轉場動畫。
所以,我的以爲key值的選取仍是得根據不一樣的項目而視。大部分狀況下,仍是推薦用location.pathname做爲key值比較合適,由於它恰是咱們不一樣頁面的路由。
說了這麼多,仍是看看具體的代碼是如何將react-transition-group應用到react-router上的吧:
// src/App4/index.js const Routes = withRouter(({location}) => ( <TransitionGroup className={'router-wrapper'}> <CSSTransition timeout={5000} classNames={'fade'} key={location.pathname} > <Switch location={location}> <Route exact path={'/'} component={HomePage} /> <Route exact path={'/about'} component={AboutPage} /> <Route exact path={'/list'} component={ListPage} /> <Route exact path={'/detail'} component={DetailPage} /> </Switch> </CSSTransition> </TransitionGroup> )); export default class App4 extends React.PureComponent { render() { return ( <BrowserRouter> <Routes/> </BrowserRouter> ); } }
這是效果:
App4的代碼思路跟App3大體相同,只是將原來的div換成了Switch組件,並且還用到了withRouter。
withRouter是react-router提供的一個高階組件,能夠爲你的組件提供location,history等對象。由於咱們這裏要用location.pathname做爲CSSTransition的key值,因此用到了它。
另外,這裏有一個坑,就是Switch的location屬性。
A location object to be used for matching children elements instead of the current history location (usually the current browser URL).
這是官網中的描述,意思就是Switch組件會用這個對象來匹配其children中的路由,並且默認用的就是當前瀏覽器的url。若是在上面的例子中咱們不給它指定,那麼在轉場動畫中會發生很奇怪的現象,就是同時有兩個相同的節點在移動。。。就像下面這樣:
這是由於TransitionGroup組件雖然會保留即將被remove的Switch節點,可是當location變化時,舊的Switch節點會用變化後的location去匹配其children中的路由。因爲location都是最新的,因此兩個Switch匹配出來的頁面是相同的。好在咱們能夠改變Switch的location屬性,如上述代碼所示,這樣它就不會老是用當前的location匹配了。
雖然前文用react-transition-group和react-router實現了一個簡單的轉場動畫,可是卻存在一個嚴重的問題。仔細觀察上一小節的示意圖,不難發現咱們的進入下個頁面的動畫效果是符合預期的,可是後退的動畫效果是什麼鬼。。。明明應該是上個頁面從左側淡入,當前頁面從右側淡出。可是爲何卻變成當前頁面從左側淡出,下個頁面從右側淡入,跟進入下個頁面的效果是同樣的。其實錯誤的緣由很簡單:
首先,咱們把路由改變分紅forward和back兩種操做。在forward操做時,當前頁面的exit效果是向左淡出;在back操做時,當前頁面的exit效果是向右淡出。因此咱們只用fade-exit和fade-exit-active這兩個class,很顯然,獲得的動畫效果確定是一致的。
所以,解決方案也很簡單,咱們用兩套class來分別管理forward和back操做時的動畫效果就能夠了。
/* src/App5/index.css */ /* 路由前進時的入場/離場動畫 */ .forward-enter { opacity: 0; transform: translateX(100%); } .forward-enter-active { opacity: 1; transform: translateX(0); transition: all 500ms; } .forward-exit { opacity: 1; transform: translateX(0); } .forward-exit-active { opacity: 0; transform: translateX(-100%); transition: all 500ms; } /* 路由後退時的入場/離場動畫 */ .back-enter { opacity: 0; transform: translateX(-100%); } .back-enter-active { opacity: 1; transform: translateX(0); transition: all 500ms; } .back-exit { opacity: 1; transform: translateX(0); } .back-exit-active { opacity: 0; transform: translate(100%); transition: all 500ms; }
不過光有css的支持還不行,咱們還得在不一樣的路由操做時加上合適的class才行。那麼問題又來了,在TransitionGroup的管理下,一旦某個組件掛載後,其exit動畫其實就已經肯定了,能夠看官網上的這個issue。也就是說,就算咱們動態地給CSSTransition添加不一樣的ClassNames屬性來指定動畫效果,但實際上是無效的。
解決方案其實在那個issue的下面就給出了,咱們能夠藉助TransitionGroup的ChildFactory屬性以及React.cloneElement方法來強行覆蓋其className。好比:
<TransitionGroup childFactory={child => React.cloneElement(child, { classNames: 'your-animation-class-name' })}> <CSSTransition> ... </CSSTransition> </TransitionGroup>
上述幾個問題都解決以後,剩下的問題就是如何選擇合適的動畫class了。而這個問題的實質在於如何判斷當前路由的改變是forward仍是back操做了。好在react-router已經貼心地給咱們準備好了,其提供的history對象有一個action屬性,表明當前路由改變的類型,其值是'PUSH' | 'POP' | 'REPLACE'
。因此,咱們再調整下代碼:
// src/App5/index.js const ANIMATION_MAP = { PUSH: 'forward', POP: 'back' } const Routes = withRouter(({location, history}) => ( <TransitionGroup className={'router-wrapper'} childFactory={child => React.cloneElement( child, {classNames: ANIMATION_MAP[history.action]} )} > <CSSTransition timeout={500} key={location.pathname} > <Switch location={location}> <Route exact path={'/'} component={HomePage} /> <Route exact path={'/about'} component={AboutPage} /> <Route exact path={'/list'} component={ListPage} /> <Route exact path={'/detail'} component={DetailPage} /> </Switch> </CSSTransition> </TransitionGroup> ));
再來看下修改以後的動畫效果:
其實,本節的內容算不上優化,轉場動畫的思路到這裏基本上已經結束了,你能夠腦洞大開,經過添加css來實現更炫酷的轉場動畫。不過,這裏仍是想再講下如何將咱們的路由寫得更配置化(我的喜愛,不喜勿噴)。
咱們知道,react-router在升級v4的時候,作了一次大改版。更加推崇動態路由,而非靜態路由。不過具體問題具體分析,在一些項目中我的仍是喜歡將路由集中化管理,就上面的例子而言但願能有一個RouteConfig,就像下面這樣:
// src/App6/RouteConfig.js export const RouterConfig = [ { path: '/', component: HomePage }, { path: '/about', component: AboutPage, sceneConfig: { enter: 'from-bottom', exit: 'to-bottom' } }, { path: '/list', component: ListPage, sceneConfig: { enter: 'from-right', exit: 'to-right' } }, { path: '/detail', component: DetailPage, sceneConfig: { enter: 'from-right', exit: 'to-right' } } ];
透過上面的RouterConfig,咱們能夠清晰的知道每一個頁面所對應的組件是哪一個,並且還能夠知道其轉場動畫效果是什麼,好比關於頁面
是從底部進入頁面的,列表頁
和詳情頁
都是從右側進入頁面的。總而言之,咱們經過這個靜態路由配置表能夠直接獲取到不少有用的信息,而不須要深刻到代碼中去獲取信息。
那麼,對於上面的這個需求,咱們對應的路由代碼須要如何調整呢?請看下面:
// src/App6/index.js const DEFAULT_SCENE_CONFIG = { enter: 'from-right', exit: 'to-exit' }; const getSceneConfig = location => { const matchedRoute = RouterConfig.find(config => new RegExp(`^${config.path}$`).test(location.pathname)); return (matchedRoute && matchedRoute.sceneConfig) || DEFAULT_SCENE_CONFIG; }; let oldLocation = null; const Routes = withRouter(({location, history}) => { // 轉場動畫應該都是採用當前頁面的sceneConfig,因此: // push操做時,用新location匹配的路由sceneConfig // pop操做時,用舊location匹配的路由sceneConfig let classNames = ''; if(history.action === 'PUSH') { classNames = 'forward-' + getSceneConfig(location).enter; } else if(history.action === 'POP' && oldLocation) { classNames = 'back-' + getSceneConfig(oldLocation).exit; } // 更新舊location oldLocation = location; return ( <TransitionGroup className={'router-wrapper'} childFactory={child => React.cloneElement(child, {classNames})} > <CSSTransition timeout={500} key={location.pathname}> <Switch location={location}> {RouterConfig.map((config, index) => ( <Route exact key={index} {...config}/> ))} </Switch> </CSSTransition> </TransitionGroup> ); });
因爲css代碼有點多,這裏就不貼了,不過無非就是相應的轉場動畫配置,完整的代碼能夠看github上的倉庫。咱們來看下目前的效果:
本文先簡單介紹了react-router和react-transition-group的基本使用方法;其中還分析了利用CSSTransition和TransitionGroup製做動畫的工做原理;接着又將react-router和react-transition-group二者結合在一塊兒完成一次轉場動畫的嘗試;並利用TransitionGroup的childFactory屬性解決了動態轉場動畫的問題;最後將路由配置化,實現路由的統一管理以及動畫的配置化,完成一次react-router + react-transition-group實現轉場動畫的探索。