react-transition-group實現路由切換過渡效果

基本介紹

效果演示

  • 簡單介紹:本人實現了一個常見問題列表,點擊列表項進入詳情頁,目標是在頁面切換的時候加過渡效果,提高用戶體驗。
  • 沒加過渡效果的,進入詳情頁時很突兀:
    無過渡效果演示
  • 加了過渡效果的,看起來好一點了吧:
    過渡效果演示
  • 下面來看具體怎麼實現吧。

react-transition-group 基本知識

官方文檔 👉react-transition-group 文檔react

  • react 項目中可以使用官網提供的動畫過渡庫 react-transition-group 來實現切換頁面(路由切換)時的過渡效果。git

  • react-transition-group 中,暴露了三個組件,分別是:github

    • Transition
    • CSSTransition
    • TransitionGroup
  • 其中最重要的是 CSSTransition,而 TransitionGroup 用於列表項的過渡動畫。項目中我也是使用了這兩個組件。typescript

  • TransitionGroup 不提供任何形式的動畫,具體的動畫取決與咱們包裹的 Transition || CSSTransition 的動畫,因此咱們能夠在列表裏面作出不一樣類型的動畫。編程

  • CSSTransition 組件中較爲重要的 api 有:api

    • in:boolean,控制組件顯示與隱藏,true 顯示,false 隱藏。瀏覽器

    • timeout:number,延遲,涉及到動畫狀態的持續時間。也可傳入一個對象,如{ exit:300, enter:500 } 來分別設置進入和離開的延時。bash

    • classNames:string,動畫進行時給元素添加的類名。通常利用這個屬性來設計動畫。這裏要特別注意是 classNames 而不是className。react-router

    • unmountOnExit:boolean,爲 true 時組件將移除處於隱藏狀態的元素,爲 false 時組件保持動畫結束時的狀態而不移除元素。通常要設成 trueapp

    • appear:boolean,爲 false 時當 CSSTransition 控件加載完畢後不執行動畫,爲 true 時控件加載完畢則當即執行動畫。若是要組件初次渲染就有動畫,則須要設成 true

    • key:string,這個屬性是配合 TransitionGroup 組件來使用的,能夠經過key來判斷是否須要觸發動畫。這個屬性十分重要!

  • classNames屬性的做用是:當組件被應用動畫時,不一樣的動畫狀態(enter,exits,done)將做爲className屬性的後綴來拼接爲新的className,如爲 CSSTransition組件設置瞭如下屬性:

<CSSTransition
          classNames={'fade'}
          appear={true}
          key={location.pathname}
          timeout={300}
          unmountOnExit={true}
        >
          /* 省略... */
        </CSSTransition>
複製代碼
  • 將會生成 fade-enterfade-enter-activefade-enter-donefade-exitfade-exite-activefade-exit-donefade-appear 以及 fade-appear-active多個className。每個獨立的className都對應着單獨的狀態。

react-router 冷知識

  • 關於 react-router 的基本知識可具體查看官方文檔 👉react-router文檔,這裏就再也不重複進行介紹。

  • 這裏介紹你們平時沒注意的關於 Switch 組件的冷知識,也是實現路由切換動畫的關鍵

  • Switch 有一個很重要的屬性:location。通常咱們不會給該組件設置 location 屬性。有無該屬性的區別:

    • 不設置location屬性: Switch 組件的子組件(通常是 Route 或 Redirect)會根據當前瀏覽器的 location 做爲匹配依據來進行路由匹配。
    • 設置location屬性:Switch 組件的子組件會根據定義的 location 做爲匹配依據。
  • 看完基本介紹,下面就看看如何在項目中使用 react-transition-group 實現頁面切換過渡效果吧。

完整流程

  • 首先,根據以前博客已經介紹的 👉 react + typescript 項目的定製化過程 搭建好項目後,在進行組件開發以前,便可在項目中引入 react-transition-group。同時,因爲項目中我還使用了typescript,因此還要安裝 @types/react-transition-group。安裝命令以下:
yarn add react-transition-group
yarn add @types/react-transition-group --dev
複製代碼
  • 在入口文件 App.tsx 中使用:
import { createHashHistory } from 'history';
import React from 'react';
import { Router } from 'react-router';
import { Route, Switch, withRouter } from 'react-router-dom';
import { TransitionGroup, CSSTransition } from 'react-transition-group';
import routeData from './common/route'; // 路由配置
import NotFound from './views/Exception';

const history = createHashHistory();

const Routes = withRouter(({ location }) => (
  <TransitionGroup className={'router-wrapper'}>
    <CSSTransition timeout={300} classNames={'fade'} key={location.pathname} unmountOnExit={true}>
      <Switch>
        {routeData.map(({ path, component, exact }: IRouterItem) => (
          <Route key={path} path={path} component={component} exact={exact} />
        ))}
        <Route component={NotFound} />
      </Switch>
    </CSSTransition>
  </TransitionGroup>
));

const App: React.FC = () => {
  return (
    <Router history={history}>
      <Routes />
    </Router>
  );
};

export default App;
複製代碼
  • withRouter的做用:能夠包裝任何自定義組件,能夠把不是經過路由切換過來的組件中,將react-router 的 historylocationmatch 三個方法傳入 props 對象上。

默認狀況下必須是通過路由匹配渲染的組件才存在 this.props,才擁有路由參數,才能使用編程式導航的寫法。然而不是全部組件都直接與路由相連(經過路由跳轉到此組件)的,當這些組件須要路由參數時,使用 withRouter 就能夠給此組件傳入路由參數,此時就可使用 this.props

  • 好比 App.js 這個組件,通常是首頁,不是經過路由跳轉過來的,而是直接從瀏覽器中輸入地址打開的,若是不使用 withRouter,此組件的 this.props 爲空,無法執行 props 中的 historylocationmatch 等方法。

  • 爲了讓入口文件 App.tsx 看起來更加簡潔,我將使用了 react-transition-group 的路由切換相關代碼封裝成Routes組件。

  • 修改後的入口文件 App.tsx 內容以下:

import { createHashHistory } from 'history';
import React from 'react';
import { Router } from 'react-router';
import Routes from './components/Routes';

const history = createHashHistory();

const App: React.FC = () => {
  return (
    <Router history={history}>
      <Routes />
    </Router>
  );
};

export default App;
複製代碼
  • Routes組件內容以下:
import React from 'react';
import { Route, Switch, withRouter } from 'react-router-dom';
import { TransitionGroup, CSSTransition } from 'react-transition-group';
import routeData from '../../common/route';
import NotFound from '../../views/Exception';

interface IRouterItem {
  component?: React.ComponentType;
  path?: string;
  exact?: boolean;
}

class Routes extends React.Component<any> {
  render () {
    const { location } = this.props;
    return (
      <TransitionGroup className={'router-wrapper'}>
        <CSSTransition
          classNames={'fade'}
          appear={true}
          key={location.pathname}
          timeout={300}
          unmountOnExit={true}
        >
          <Switch location={location}>
            {routeData.map(({ path, component, exact }: IRouterItem) => (
              <Route key={path} path={path} component={component} exact={exact} />
            ))}
            <Route component={NotFound} />
          </Switch>
        </CSSTransition>
      </TransitionGroup>
    );
  }
}

export default withRouter(Routes);
複製代碼
  • 決定是否有動畫效果的關鍵步驟來了,就是完成動畫的相關樣式 !因爲動畫效果是做用的全局,因此應該寫在全局樣式裏面。
  • 關於模塊化的 less 全局的 less 的相關介紹可具體查看前一篇博客 👉 react + typescript 項目的定製化過程進行了解。
  • 咱們要作的就是在src目錄下新建一個index.less文件,內容以下:
/* 動畫相關樣式 */
.fade-enter, .fade-appear {
  opacity: 0;
}

.fade-enter.fade-enter-active, .fade-appear.fade-appear-active {
  opacity: 1;
  transition: opacity 300ms ease-in;
}

.fade-exit {
  opacity: 1;
}

.fade-exit.fade-exit-active {
  opacity: 0;
}
複製代碼
  • 而後在入口文件index.tsx進行引入便可:
import './index.less';
複製代碼
  • 以上則能夠實如今切換路由時呈現頁面過渡效果,且無bug(首次加載無動畫、接口請求兩次)。

⚠ ️若是隻是想實現過渡效果,按照上面介紹的內容便可實現

⚠ ️若是想了解出現以上兩種bug的緣由,則能夠繼續看下面的內容。

踩坑實踐

  • 說沒踩坑是不可能的,剛開始的代碼也不是這樣的。下面來解釋爲什麼會出現首次加載無動畫、接口請求兩次這兩種bug。

首次加載無動畫

  • 以前寫過的文章提到過,我在項目中使用了一個叫 react-loadable 的第三方庫來進行代碼拆分,實現組件按需加載。(相關介紹可具體查看前一篇博客 👉 react + typescript 項目的定製化過程進行了解)
  • 然而,發現當使用 react-loadable首次加載時頁面切換沒有過渡效果,具體看下面的效果:
    首次加載無動畫
  • 能夠看到,當刷新頁面時,列表頁(第一個頁面)並無過渡效果,進入詳情頁也是沒有過渡效果,很突兀,點擊後退到列表頁(有動畫),以後進入詳情頁和切出來都有過渡效果,這就是我遇到的首次加載時頁面切換沒有過渡效果問題。
  • 因爲本次實現的功能比較簡單,爲了解決這個問題,只能暫時在項目中捨棄使用 react-loadable 進行組件按需加載了😭。
  • 修改後的路由配置文件 route.tsx 內容大體以下:
// path:src/common/route.tsx
import * as React from 'react';
import DetailPage from '../views/DetailPage';
import Exception from '../views/Exception';
import HomePage from '../views/HomePage';

const routeConfig: any = [
  {
    path: '/',
    component: HomePage,
  },
  {
    path: '/detail/:id',
    component: DetailPage,
  },
  /**
   * Exception 頁面
   */
  {
    path: '/exception/404',
    component: Exception,
  },
];

function generateRouteConfig (route: IRouteConfig[]) {
  return route.map(item => {
    return {
      key: item.path,
      exact: typeof item.exact === 'undefined' ? true : item.exact,
      ...item,
      component: item.component,
    };
  });
}

export default generateRouteConfig(routeConfig);
複製代碼
  • 至於二者結合會出現這樣的bug緣由還在觀察中,若是有大佬知道能夠留言告知,或者以後知道緣由了再進行更新。

接口請求兩次

  • 咱們都知道,通常是在 react 生命週期的 componentDidMount 方法中調接口(請求相關數據)。componentDidMount 方法會在render()以後當即執行,拉取數據後使用setState() 方法觸發從新渲染(re-render)。
  • 一開始路由切換相關代碼封裝的Routes組件內容以下:
import React from 'react';
import { Route, Switch, withRouter } from 'react-router-dom';
import { TransitionGroup, CSSTransition } from 'react-transition-group';
import routeData from '../../common/route';
import NotFound from '../../views/Exception';

interface IRouterItem {
  component?: React.ComponentType;
  path?: string;
  exact?: boolean;
}

class Routes extends React.Component<any> {
  render () {
    const { location } = this.props;
    return (
      <TransitionGroup className={'router-wrapper'}>
        <CSSTransition
          classNames={'fade'}
          appear={true}
          key={location.pathname}
          timeout={300}
          unmountOnExit={true}
        >
          <Switch>
            {routeData.map(({ path, component, exact }: IRouterItem) => (
              <Route key={path} path={path} component={component} exact={exact} />
            ))}
            <Route component={NotFound} />
          </Switch>
        </CSSTransition>
      </TransitionGroup>
    );
  }
}

export default withRouter(Routes);
複製代碼
  • 與沒bug的代碼差異就是,以前沒有給 Switch 組件設置location屬性。致使首次加載後進入詳情頁,切出來都會請求兩次接口,具體看看下面的演示:

接口請求兩次演示

  • 爲何?前面提到:

Switch 有一個很重要的屬性:location。通常咱們不會給該組件設置 location 屬性。有無該屬性的區別:

  • 不設置location屬性: Switch 組件的子組件(通常是 Route 或 Redirect)會根據當前瀏覽器的 location 做爲匹配依據來進行路由匹配。
  • 設置location屬性:Switch 組件的子組件會根據定義的 location 做爲匹配依據。
  • 關鍵代碼塊截圖:
    代碼
  • 結合代碼👆看解釋👇:
    • CSSTransition 這個組件中的 key 屬性是配合 TransitionGroup 組件使用的,能夠經過 key 來判斷是否須要觸發動畫。
    • 在切換路由時,舊的路由內容會在必定時間內過渡消失,新的路由內容過渡顯示,過渡期間會同時存在兩個節點,舊節點顯示舊的路由內容,新的節點則顯示新的路由內容。
    • CSSTransition 組件中的 key 屬性決定該節點是否顯示,而 Router 組件中的 location 屬性會在路由發生變化時進行更新,剛好 locationpathname 能夠做爲 CSSTransition 組件中的 key 屬性。當路由切換的時候, location 對象就會發生改變,新的 key key會使得頁面從新渲染時出現兩個 CSSTransition
    • 若是隻是給 CSSTransition 組件配置 key 屬性,會發現舊節點會去匹配新的路由內容,這是由於 Route 組件默認根據當前瀏覽器的 location 進行匹配,爲了讓舊節點根據舊的 location 進行匹配,則須要設置 Switch 組件的 location 屬性。
  • 組件重複渲染就會致使接口的重複請求,趕忙給 Switch 組件加個 location 屬性吧。
  • 本文的內容就介紹到這裏啦,歡迎留言,喜歡的麻煩點個贊👍,謝謝❤️。
相關文章
相關標籤/搜索