学习React的过渡管理 React Transition Group(一)

478 阅读6分钟

React Transition Group官方文档

React Transition Group能做什么?

就官方文档所言,其实react-transition-group并非动画库,并不会提供动画样式(如React-Motion)。react-transition-group提供了能让你更方便实现 转换过程 中过渡效果的组件,通过这些组件可以管理过渡时机、操纵DOM。

安装

# npm
npm install react-transition-group --save

# yarn
yarn add react-transition-group

基础组件 Transition

Transition 用于实现并控制单个组件的过渡效果,而且预留了多个钩子。

1. Transition 用法

下面用弹窗组件举例:

<Transition in={visible} timeout={500} mountOnEnter unmountOnExit>
  {state => (
    <div className={styles.popup}>
      {/* 遮罩层 */}
      <div
        className={styles.mask}
        onClick={() => onCancel()}
        style={{ ...maskDefaultStyle, ...maskTransitionStyles[state] }}
      />
      {/* 弹窗内容 */}
      <div
        className={styles.modal}
        style={{
          width,
          ...modalDefaultStyle,
          ...modalTransitionStyles[state],
        }}
      >
      </div>
    </div>
  )}
</Transition>

Transition组件接受一个Fuction类型的children,传参为state(类型为TransitionStatus),我们主要使用其中四个状态:entering、entered、exiting 和 exited 分别代表进入和离开的过渡效果。

我们需要为实现过渡效果的元素的样式,添加一个transition值,也要为其过渡过程中的四个状态定义样式(没错,就是传统html实现过渡效果的方式😀),如下:

const duration = 400;

const maskDefaultStyle = {
  transition: `opacity ${duration}ms ease-in-out`,
  opacity: 0.5,
};

const maskTransitionStyles: Partial<Record<TransitionStatus, any>> = {
  entering: { opacity: 0.5, transform: 'scale(1)' },
  entered: { opacity: 0.5, transform: 'scale(1)' },
  exiting: { opacity: 0, transform: 'scale(0)' },
  exited: { opacity: 0, transform: 'scale(0)' },
};

然后就是将样式添加到对应元素的style属性值中。 整体代码如下

const duration = 400;

const maskDefaultStyle = {
  transition: `opacity ${duration}ms ease-in-out`,
  opacity: 0,
};

const maskTransitionStyles: Partial<Record<TransitionStatus, any>> = {
  entering: { opacity: 0.5, transform: 'scale(1)' },
  entered: { opacity: 0.5, transform: 'scale(1)' },
  exiting: { opacity: 0, transform: 'scale(0)' },
  exited: { opacity: 0, transform: 'scale(0)' },
};

const modalDefaultStyle = {
  // transition 
  transition: `all ${duration}ms ease-in-out`,
  opacity: 0,
  transform: 'scale(0)',
};

const modalTransitionStyles: Partial<Record<TransitionStatus, any>> = {
  entering: { opacity: 1, transform: 'scale(1)' },
  entered: { opacity: 1, transform: 'scale(1)' },
  exiting: { opacity: 0, transform: 'scale(0)' },
  exited: { opacity: 0, transform: 'scale(0)' },
};

<Transition in={visible} timeout={500} mountOnEnter unmountOnExit>
  {state => (
    <div className={styles.popup}>
      {/* 遮罩层 */}
      <div
        className={styles.mask}
        onClick={() => onCancel()}
        style={{ ...maskDefaultStyle, ...maskTransitionStyles[state] }}
      />
      {/* 弹窗内容 */}
      <div
        className={styles.modal}
        style={{
          width,
          ...modalDefaultStyle,
          ...modalTransitionStyles[state],
        }}
      >
      </div>
    </div>
  )}
</Transition>

2. Transition的属性

Transition 所接受的props如下表所示(粗略的写一下,详细可去看官方文档):

props是否可选解释
nodeRef可选用于获取过渡组件的DOM元素ref。如果定义了nodeRef值,诸如onEnter等钩子的回调函数,将不会传递node。如果没有定义nodeRef,Transition组件会使用ReactDom.findDomNode来获取DOM元素
children必要使用时传递一个函数类型的children,传参为state(过渡状态),返回相应样式的过渡组件
in可选用于控制过渡组件的进入或离开,默认值为false
mountOnEnter可选是否在首次过渡组件进入时执行才挂载,也就是第一次in属性为true时挂载,默认值为false
unmountOnExit可选是否在过渡组件离开时执行卸载,默认值为false
appear可选一般情况下,Transition并不会在首次挂载时展现进入效果,即使in初始值为true。如果想要实现这种效果,需要同时将appear属性和in属性置为true
enter可选控制是否展示进入效果,默认值为true
exit可选控制是否展示离开效果,默认值为true
timeout如果没有定义addEndListener,则为必要可为单一的number类型值,或者为对象类型{ enter?: number, exit?: number, appear?: number },为对象时三个属性默认值都为0。单位为毫秒。注意:这里的值应与样式中的transition-duration值保持一致
onEnter/onEntering可选Transition组件过渡状态为entering之前/之后的钩子, 类型Function(node: HtmlElement, isAppearing: bool) -> void, isAppearing指是否首次挂载时的进入过渡,注意:当提供了nodeRef属性值时,这里的函数并不会传递node
onEntered可选Transition组件过渡状态为entered之后的钩子, 类型Function(node: HtmlElement, isAppearing: bool) -> void, isAppearing指是否首次挂载时的进入过渡,注意:当提供了nodeRef属性值时,这里的函数并不会传递node
onExit/onExiting可选Transition组件过渡状态为exiting之前/之后的钩子, 类型Function(node: HtmlElement) -> void,注意:当提供了nodeRef属性值时,这里的函数并不会传递node
onExited可选Transition组件过渡状态为exited之后的钩子, 类型Function(node: HtmlElement) -> void,注意:当提供了nodeRef属性值时,这里的函数并不会传递node
addEndListenertimeout属性与addEndListener属性必须有一个,同时有则以前者为主其实timeout属性与addEndListener属性都是提供给Transition组件使用的,Transition组件需要在过渡效果结束时执行钩子。
// 注意:当提供了nodeRef属性值时,这里的回调函数并不会传递node
addEndListener={(node, done) => {
  // use the css transitionend event to mark the finish of a transition
  node.addEventListener('transitionend', done, false);
}}

注意,Transition组件的初始过渡状态是这么决定的。如下面代码(解释某些情况下mountOnEnter为false不生效的原因)

construct() {
    // ...
    
    if (props.in) {
     if (appear) {
       initialStatus = EXITED;
       this.appearStatus = ENTERING;
     } else {
       initialStatus = ENTERED;
     }
   } else {
     if (props.unmountOnExit || props.mountOnEnter) {
       initialStatus = UNMOUNTED;
     } else {
       initialStatus = EXITED;
     }
   }
   this.state = { status: initialStatus };
   
   // ...
}

CSSTransition

Transition组件中,我们将过渡组件包装成Function类型的children,并且通过style属性来定义各个过渡状态的样式。如果想通过css来定义过渡的样式和动画,我们可以选用React Transition Group提供的另一个组件CSSTransition

1. CSSTransition用法

现在改造一下上文的弹窗组件

<CSSTransition
  classNames={{
    enter: styles.enter,
    enterActive: styles.enterActive,
    exit: styles.exit,
    exitActive: styles.exitActive,
  }}
  in={visible}
  timeout={400}
  mountOnEnter
  unmountOnExit
>
    <div className={styles.popup}>
      {/* 遮罩层 */}
      <div
        className={styles.mask}
        onClick={() => onCancel()}
      />
      {/* 弹窗内容 */}
      <div
        className={styles.modal}
        style={{width}}
      >
      </div>
    </div>
</CSSTransition>

可以看到,CSSTransition中的过渡组件不再需要包装成Fuction类型,而是可以直接作为children。同时使用CSSTranstion组件相比Transition组件多了classNames属性,这个属性可以用来定义各个过渡状态的css类。注意:笔者这里使用了css module的方式处理组件的样式隔离,如代码中的styles.enter就是一个css类名。

css部分如下所示:

.enter .mask {
  opacity: 0;
  transform: scale(0);
}
.enterActive .mask {
  opacity: 0.5;
  transform: scale(1);
  transition: opacity 0.4s ease-in-out;
}

.exit .mask {
  opacity: 0.5;
  transform: scale(1);
}

.exitActive .mask {
  opacity: 0;
  transform: scale(0);
  transition: opacity 0.4s ease-in-out;
}

.enter .modal {
  opacity: 0;
  transform: scale(0);
}

.enterActive .modal {
  opacity: 1;
  transform: scale(1);
  transition: all 0.4s ease-in-out;
}

.exit .modal {
  opacity: 1;
  transform: scale(1);
}

.exitActive .modal {
  opacity: 0;
  transform: scale(0);
  transition: all 0.4s ease-in-out;
}

值得一说,CSSTransition的类名将会挂载在nodeRef关联的DOM实例上(没有定义nodeRef的话,就是通过findDOMNode获取到的DOM实例)。

2. CSSTransition的属性

CSSTransition组件直接复用了Transition组件,CSSTransition组件可以使用全部的Transition的属性。

区别:

  1. CSSTransition的钩子时机都改成了相应的 css类名添加完成 时机,如onEnter 对应 enterappear类名, onEntering 对应 enter-activeappear-active类名, onEntered 对应 enter-doneappear-done类名;
  2. CSSTransition有一个独有的属性classNames
  3. CSSTransition中我们可以为appear过渡状态添加独立样式(appear指过度组件初次进入的时候)。appear过渡样式只能有*-appear*-appear-active*-appear-done等类样式决定。
props是否可选解释
classNames可选(默认值为空字串)classNames属性值为字符串时,用作css类名的前缀。当classNames属性值为对象时,可以单独定义每个状态下的样式 { appear?: string, appearActive?: string, appearDone?: string, enter?: string, enterActive?: string, enterDone?: string, exit?: string, exitActive?: string, exitDone?: string, }

值得注意:像*-appear*-appear-activecss类名会在in属性变更后,先后添加进过渡组件的DOM实例的class中,然后在过渡效果消失时删除,并添加*-appear-done类名。

在CSSTransition中还可以使用Animate.css里面的动画,就直接添加到*-active类中即可

.alert-enter-active {

    animation: bounceInLeft; /* referring directly to the animation's @keyframe declaration */

    animation-duration: 0.3s; /* don't forget to set a duration! */
}

.alert-exit-active {

    animation: bounceOutLeft; /* 进入和离开的离开的动画还是配套的 */

    animation-duration: 0.3s;
}

最后,看一看本文所示弹窗组件代码的演示效果😀 弹窗动画演示.gif