React-實現(xiàn)仿原生App的轉(zhuǎn)場動畫

React大法好

一、前言

最近這兩個星期,已經(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。

React學(xué)習(xí)Demo調(diào)試圖

此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-router-dom路由切換效果.gif

四、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)intrue時,CSSTransition的子組件會先添加${classNames}-enter類,下一個tick會添加${classNames}-enter-active類。
    當(dāng)infalse時,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)有以下幾點不同:

  1. CSSTransition組件上層嵌了一層TransitionGroup組件
  2. 沒有使用in屬性作為控制組件添加enterexit類的手段,而是使用了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)場動畫

針對上面對TransitionGroupCSSTransition組件的運用,想象一下,其實我們把案例中的子組件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>
        );
    }
}

來看一下效果:


轉(zhuǎn)場效果圖

應(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'}
            >

但是效果貌似出了些問題~~~

667.gif

確定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'}
            )}
>

最終效果如下:


react轉(zhuǎn)場實戰(zhàn)的最終效果

七、總結(jié)

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容