一次react-router + react-transition-group实现转场动画的探索

原文地址javascript

1. Introduction

在平常开发中,页面切换时的转场动画是比较基础的一个场景。在react项目当中,咱们通常都会选用react-router来管理路由,可是react-router却并无提供相应的转场动画功能,而是很是生硬的直接替换掉组件。必定程度上来讲,体验并非那么友好。css

为了在react中实现动画效果,其实咱们有不少的选择,好比:react-transition-groupreact-motionAnimated等等。可是,因为react-transition-group给元素添加的enter,enter-active,exit,exit-active这一系列勾子,简直就是为咱们的页面入场离场而设计的。基于此,本文选择react-transition-group来实现动画效果。html

接下来,本文就将结合二者提供一个实现路由转场动画的思路,权当抛砖引玉~html5

2. Requirements

咱们先明确要完成的转场动画是什么效果。以下图所示:java

3. react-router

首先,咱们先简要介绍下react-router的基本用法(详细看官网介绍)。react

这里咱们会用到react-router提供的BrowserRouterSwitchRoute三个组件。git

  • BrowserRouter:以html5提供的history api形式实现的路由(还有一种hash形式实现的路由)。
  • Switch:多个Route组件同时匹配时,默认都会显示,可是被Switch包裹起来的Route组件只会显示第一个被匹配上的路由。
  • Route:路由组件,path指定匹配的路由,component指定路由匹配时展现的组件。
// 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

  1. 首页 ↔ 关于页
  2. 首页 ↔ 列表页 ↔ 详情页

来看下目前默认的路由切换效果:web

4. react-transition-group

从上面的效果图中,咱们能够看到react-router在路由切换时彻底没有过渡效果,而是直接替换的,显得很是生硬。api

正所谓工欲善其事,必先利其器,在介绍实现转场动画以前,咱们得先学习如何使用react-transition-group。基于此,接下来就将对其提供的CSSTransition和TransitionGroup这两个组件展开简要介绍。

4.1 CSSTransition

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;
}

来看看效果,是否是和页面的入场离场效果有点类似?

4.2 TransitionGroup

用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样式来实现动画效果就能够了。

5. Page transition animation

基于前文对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匹配了。

6. Page dynamic transition animation

虽然前文用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>
));

再来看下修改以后的动画效果:

7. Optimize

其实,本节的内容算不上优化,转场动画的思路到这里基本上已经结束了,你能够脑洞大开,经过添加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上的仓库。咱们来看下目前的效果:

8. Summarize

本文先简单介绍了react-router和react-transition-group的基本使用方法;其中还分析了利用CSSTransition和TransitionGroup制做动画的工做原理;接着又将react-router和react-transition-group二者结合在一块儿完成一次转场动画的尝试;并利用TransitionGroup的childFactory属性解决了动态转场动画的问题;最后将路由配置化,实现路由的统一管理以及动画的配置化,完成一次react-router + react-transition-group实现转场动画的探索。

9. Reference

  1. A shallow dive into router v4 animated transitions
  2. Dynamic transitions with react router and react transition group
  3. Issue#182 of react-transition-group
  4. StackOverflow: react-transition-group and react clone element do not send updated props

本文全部代码托管在这儿,若是以为不错的,能够给个star

相关文章
相关标签/搜索