React怎么实现一个Transition过渡动画组件

2023-05-22,

这篇文章将为大家详细讲解有关React怎么实现一个Transition过渡动画组件,小编觉得挺实用的,因此分享给大家做个参考,希望大家阅读完这篇文章后可以有所收获。

一、基本实现

我们在实现基础的过度动画组件,需要通过切换CSS样式实现简单的动画效果。

首先我们安装 classnames 插件:

npm install classnames --save-dev

而且 classnaems 是一个简单的JavaScript实用程序,用于有条件地将 classnames 连接一起。那么我们将 component 目录新建一个 Transition 文件夹,并在文件夹中新建一个 Transition.jsx文件,代码如下:

import React from 'react'
import classnames from 'classnames'

/**
 * css过渡动画组件
 *
 * @visibleName Transition 过渡动画
 */
class Transition extends React.Component {
  render() {
    const { children } = this.props
    const transition = (
      <div
        className={
          classnames({
            transition: true
          })
        }
        style={
          {
            position: 'relative',
            overflow: 'hidden'
          }
        }
      >
        <div
          className={
            classnames({
              'transition-wrapper': true
            })
          }
        >
          { children }
        </div>
      </div>
    )
    return transition
  }
}
export default Transition

在文件中我们通过使用小驼峰来定义属性定义名称;而且我们在文件中使用 JSX 语法,我们来看看如下案例代码:

const name = 'Josh Perez';
const element = <h2>Hello, {name}</h2>;

这个代码等价如下这串代码:

const element = <h2>Hello, Josh Perez</h2>;

当然在使用 JSX 语法的时候我们还是要注意的,因为在 JSX 语法中会更接近 JavaScript 而不是 html ,所以在 React DOM 中我们使用 小驼峰来进行命名,而不使用 html 属性名称约定。

除此之外在 React 中的 props.children 包含组件所有的子节点,即组件开始标签和结束标签之间的内容,如下案例所示:

<Button>默认按钮</Button>

Button 组件中获取 props.children,就可以得到字符串“默认按钮”。

那么接下来我们在 Transition 文件夹下新建一个 index.js ,导出 Transition 组件,代码如下所示:

import Transition from './Transition.jsx'
export { Transition }
export default Transition

完成之后,在 Transition.jsx 文件中为组件添加 props 检查并设置 action 默认值,代码如下所示:

import PropTypes from 'prop-types'
const propTypes = {
  /** 执行动画 */
  action: PropTypes.bool,
  /** 切换的css动画的class名称 */
  toggleClass: PropTypes.string
}
const defaultProps = {
  action: false
}

这时候我们使用 prop-ty.pes实现运行时类型检查。但是需要注意的是 prop-ty.pes是一个运行时;类型检查工具,我们来看看完整的 Transition 组件代码如下所示:

import React from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
const propTypes = {
  /** 执行动画 */
  action: PropTypes.bool,
  /** 切换的css动画的class名称 */
  toggleClass: PropTypes.string
}
const defaultProps = {
  action: false
}
/**
 * css过渡动画组件
 *
 * @visibleName Transition 过渡动画
 */
class Transition extends React.Component {
  static propTypes = propTypes
  static defaultProps = defaultProps
  render() {
    const {
      className,
      action,
      toggleClass,
      children
    } = this.props
    const transition = (
      <div
        className={
          classnames({
            transition: true
          })
        }
        style={
          {
            position: 'relative',
            overflow: 'hidden'
          }
        }
      >
        <div
          className={
            classnames({
              'transition-wrapper': true,
              [className]: className,
              [toggleClass]: action && toggleClass
            })
          }
        >
          { children }
        </div>
      </div>
    )
    return transition
  }
}
export default Transition

CSS代码如下所示:

.fade {
  transition: opacity 0.15s linear;
}
.fade:not(.show) {
  opacity: 0;
}

JS代码如下所示:

import React from 'react';
import Transition from './Transition';

class Anime extends React.Component {
  constructor (props) {
    super(props)
    this.state = {
      action: true
    }
  }
  
  render () {
    const btnText = this.state.action ? '淡出' : '淡入'
    return (
      <div>
        <Transition
          className="fade"
          toggleClass="show"
          action={ this.state.action }
        >
          淡入淡出
        </Transition>
        <button
          style={{ marginTop: '20px' }}
          onClick={() => this.setState({ action: !this.state.action })}
        >
          { btnText }
        </button>
      </div>
    )
  }
}

这样子我们就只需要在需要使用动画的地方来进行使用 Anime 组件就可以了。


二、实现Animate.css兼容

我们都知道 Animate.css 是一款强大的预设 CSS3 动画库。由于在进入动画和离开动画通常在使用这两个效果相反的 class 样式,所以我们需要给我们的 Transition 组件添加 enterClassleaveClass 两个属性来实现动画的切换,代码如下所示:

import React from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'

const propTypes = {
  /** 执行动画 */
  action: PropTypes.bool,
  /** 切换的css动画的class名称 */
  toggleClass: PropTypes.string,
  /** 进入动画的class名称,存在 toggleClass 时无效 */
  enterClass: PropTypes.string,
  /** 离开动画的class名称,存在 toggleClass 时无效 */
  leaveClass: PropTypes.string
}

const defaultProps = {
  action: false
}

/**
 * css过渡动画组件
 *
 * @visibleName Transition 过渡动画
 */
class Transition extends React.Component {

  static propTypes = propTypes

  static defaultProps = defaultProps

  render() {
    const {
      className,
      action,
      toggleClass,
      enterClass,
      leaveClass,
      children
    } = this.props
    return (
      <div
        className={
          classnames({
            transition: true
          })
        }
        style={
          {
            position: 'relative',
            overflow: 'hidden'
          }
        }
      >
        <div
          className={
            classnames({
              'transition-wrapper': true,
              [className]: className,
              [toggleClass]: action && toggleClass,
              [enterClass]: !toggleClass && action && enterClass,
              [leaveClass]: !toggleClass && !action && leaveClass,
            })
          }
        >
          { children }
        </div>
      </div>
    )
  }
}

export default Transition

当然我们还是要注意一下,由于 toggleClass 适用于那些进入动画与离开动画切换相同 class 样式的情况,而且 enterClassleaveClass 使用那些进入动画和离开动画切换不同的 class 样式的情况,所以,他们和 toggleClass 不能共存。

那么我们接下来就尝试下加入Animate.css 后的 Transition 组件,代码如下所示:

import React from 'react';
import 'animate.css';

class Anime extends React.Component {
  constructor (props) {
    super(props)
    this.state = {
      action: true
    }
  }
  
  render () {
    return (
      <div>
        <Transition
          className="animated"
          enterClass="bounceInLeft"
          leaveClass="bounceOutLeft"
          action={ this.state.action }
        >
          弹入弹出
        </Transition>
        <utton
          style={{ marginTop: '20px' }}
          onClick={() => this.setState({ action: !this.state.action })}
        >
          { this.state.action ? '弹出' : '弹入' }
        </utton>
      </div>
    )
  }
}

三、功能扩展

通过上面的方法实现之后我们知道 Transition 组件是可以适用在很多的场景中的,但是功能不是很丰富,所以就需要扩展 Transition 的接口。首先我们来添加 props 属性,并设置默认值,代码如下所示:

const propTypes = {
  ...,
  /** 动画延迟执行时间 */
  delay: PropTypes.string,
  /** 动画执行时间长度 */
  duration: PropTypes.string,
  /** 动画执行次数,只在执行 CSS3 动画时有效 */
  count: PropTypes.number,
  /** 动画缓动函数 */
  easing: PropTypes.oneOf([
    'linear',
    'ease',
    'ease-in',
    'ease-out',
    'ease-in-out'
  ]),
  /** 是否强制轮流反向播放动画,count 为 1 时无效 */
  reverse: PropTypes.bool
}

const defaultProps = {
  count: 1,
  reverse: false
}

根据 props 设置样式,代码如下所示:

// 动画样式
const styleText = (() => {
  let style = {}
  // 设置延迟时长
  if (delay) {
    style.transitionDelay = delay
    style.animationDelay = delay
  }
  // 设置播放时长
  if (duration) {
    style.transitionDuration = duration
    style.animationDuration = duration
  }
  // 设置播放次数
  if (count) {
    style.animationIterationCount = count
  }
  // 设置缓动函数
  if (easing) {
    style.transitionTimingFunction = easing
    style.animationTimingFunction = easing
  }
  // 设置动画方向
  if (reverse) {
    style.animationDirection = 'alternate'
  }
  return style
})()

完整代码如下所示:

import React from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'

const propTypes = {
  /** 执行动画 */
  action: PropTypes.bool,
  /** 切换的css动画的class名称 */
  toggleClass: PropTypes.string,
  /** 进入动画的class名称,存在 toggleClass 时无效 */
  enterClass: PropTypes.string,
  /** 离开动画的class名称,存在 toggleClass 时无效 */
  leaveClass: PropTypes.string,
  /** 动画延迟执行时间 */
  delay: PropTypes.string,
  /** 动画执行时间长度 */
  duration: PropTypes.string,
  /** 动画执行次数,只在执行 CSS3 动画时有效 */
  count: PropTypes.number,
  /** 动画缓动函数 */
  easing: PropTypes.oneOf([
    'linear',
    'ease',
    'ease-in',
    'ease-out',
    'ease-in-out'
  ]),
  /** 是否强制轮流反向播放动画,count 为 1 时无效 */
  reverse: PropTypes.bool
}

const defaultProps = {
  action: false,
  count: 1,
  reverse: false
}

/**
 * css过渡动画组件
 *
 * @visibleName Transition 过渡动画
 */
class Transition extends React.Component {

  static propTypes = propTypes

  static defaultProps = defaultProps

  render() {
    const {
      className,
      action,
      toggleClass,
      enterClass,
      leaveClass,
      delay,
      duration,
      count,
      easing,
      reverse,
      children
    } = this.props

    // 动画样式
    const styleText = (() => {
      let style = {}
      // 设置延迟时长
      if (delay) {
        style.transitionDelay = delay
        style.animationDelay = delay
      }
      // 设置播放时长
      if (duration) {
        style.transitionDuration = duration
        style.animationDuration = duration
      }
      // 设置播放次数
      if (count) {
        style.animationIterationCount = count
      }
      // 设置缓动函数
      if (easing) {
        style.transitionTimingFunction = easing
        style.animationTimingFunction = easing
      }
      // 设置动画方向
      if (reverse) {
        style.animationDirection = 'alternate'
      }
      return style
    })()

    return (
      <div
        className={
          classnames({
            transition: true
          })
        }
        style={
          {
            position: 'relative',
            overflow: 'hidden'
          }
        }
      >
        <div
          className={
            classnames({
              'transition-wrapper': true,
              [className]: className,
              [toggleClass]: action && toggleClass,
              [enterClass]: !toggleClass && action && enterClass,
              [leaveClass]: !toggleClass && !action && leaveClass,
            })
          }
          style={ styleText }
        >
          { children }
        </div>
      </div>
    )
  }
}

export default Transition

在这里我们来看下相关的 Transition 增加的属性:

  • delay:规定在动画开始之前的延迟。

  • duration:规定完成动画所花费的时间,以秒或毫秒计。

  • count:规定动画应该播放的次数。

  • easing:规定动画的速度曲线。

  • reverse:规定是否应该轮流反向播放动画。


四、优化

那么接下来我们来对 Transition 来进行一个优化,我们主要的是动画监听、卸载组件以及其他的相关兼容问题。那么我们在代码中添加 props 属性,并且设置默认值。代码如下所示:

const propTypes = {
  ...,
  /** 动画结束的回调 */
  onEnd: PropTypes.func,
  /** 离开动画结束时卸载元素 */
  exist: PropTypes.bool
}

const defaultProps = {
  ...,
  reverse: false,
  exist: false
}

接下来进行动画结束监听的事件代码:

/**
 * css过渡动画组件
 *
 * @visibleName Transition 过渡动画
 */
class Transition extends React.Component {

  ...

  onEnd = e => {
    const { onEnd, action, exist } = this.props
    if (onEnd) {
      onEnd(e)
    }
    // 卸载 DOM 元素
    if (!action && exist) {
      const node = e.target.parentNode
      node.parentNode.removeChild(node)
    }
  }

  /**
   * 对动画结束事件 onEnd 回调的处理函数
   *
   * @param {string} type - 事件解绑定类型: add - 绑定事件,remove - 移除事件绑定
   */
  handleEndListener (type = 'add') {
    const el = ReactDOM.findDOMNode(this).querySelector('.transition-wrapper')
    const events = ['animationend', 'transitionend']
    events.forEach(ev => {
      el[`${type}EventListener`](ev, this.onEnd, false)
    })
  }

  componentDidMount () {
    this.handleEndListener()
  }

  componentWillUnmount () {
    const { action, exist } = this.props
    if (!action && exist) {
      this.handleEndListener('remove')
    }
  }

  render () {
    ...
  }
}

在代码中我们可以知道使用到 componentDidMount 和 componentWillUnmount 这两个生命周期函数。

react-dom 中还为我们提供了可以在 React 应用中使用的 DOM 方法。我们通过获取兼容性 animationend 和transitionend 事件。检验函数方法的代码如下所示:

/**
 * 浏览器兼容事件检测函数
 *
 * @param {node} el - 触发事件的 DOM 元素
 * @param {array} events - 可能的事件类型
 * @returns {*}
 */
const whichEvent = (el, events) => {
  const len = events.length
  for (var i = 0; i < len; i++) {
    if (el.style[i]) {
      return events[i];
    }
  }
}

修改 handleEndListener 函数代码如下所示:

/**
 * css过渡动画组件
 *
 * @visibleName Transition 过渡动画
 */
class Transition extends React.Component {

  ...

  /**
   * 对动画结束事件 onEnd 回调的处理函数
   *
   * @param {string} type - 事件解绑定类型: add - 绑定事件,remove - 移除事件绑定
   */
  handleEndListener (type = 'add') {
    const el = ReactDOM.findDOMNode(this).querySelector('.transition-wrapper')
    const events = ['AnimationEnd', 'TransitionEnd']
    events.forEach(ev => {
      const eventType = whichEvent(el, [ev.toLowerCase(), `webkit${ev}`])
      el[`${type}EventListener`](eventType, this.onEnd, false)
    })
  }

  ...

}

那么到这里之后我们就完成了整个 Transition 组件的开发,相关完整代码如下所示:

import React from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
import ReactDOM from 'react-dom'

const propTypes = {
  /** 执行动画 */
  action: PropTypes.bool,
  /** 切换的css动画的class名称 */
  toggleClass: PropTypes.string,
  /** 进入动画的class名称,存在 toggleClass 时无效 */
  enterClass: PropTypes.string,
  /** 离开动画的class名称,存在 toggleClass 时无效 */
  leaveClass: PropTypes.string,
  /** 动画延迟执行时间 */
  delay: PropTypes.string,
  /** 动画执行时间长度 */
  duration: PropTypes.string,
  /** 动画执行次数,只在执行 CSS3 动画时有效 */
  count: PropTypes.number,
  /** 动画缓动函数 */
  easing: PropTypes.oneOf([
    'linear',
    'ease',
    'ease-in',
    'ease-out',
    'ease-in-out'
  ]),
  /** 是否强制轮流反向播放动画,count 为 1 时无效 */
  reverse: PropTypes.bool,
  /** 动画结束的回调 */
  onEnd: PropTypes.func,
  /** 离开动画结束时卸载元素 */
  exist: PropTypes.bool
}

const defaultProps = {
  action: false,
  count: 1,
  reverse: false,
  exist: false
}

/**
 * 浏览器兼容事件检测函数
 *
 * @param {node} el - 触发事件的 DOM 元素
 * @param {array} events - 可能的事件类型
 * @returns {*}
 */
const whichEvent = (el, events) => {
  const len = events.length
  for (var i = 0; i < len; i++) {
    if (el.style[i]) {
      return events[i];
    }
  }
}

/**
 * css过渡动画组件
 *
 * @visibleName Transition 过渡动画
 */
class Transition extends React.Component {

  static propTypes = propTypes

  static defaultProps = defaultProps

  onEnd = e => {
    const { onEnd, action, exist } = this.props
    if (onEnd) {
      onEnd(e)
    }
    // 卸载 DOM 元素
    if (!action && exist) {
      const node = e.target.parentNode
      node.parentNode.removeChild(node)
    }
  }

  /**
   * 对动画结束事件 onEnd 回调的处理函数
   *
   * @param {string} type - 事件解绑定类型: add - 绑定事件,remove - 移除事件绑定
   */
  handleEndListener (type = 'add') {
    const el = ReactDOM.findDOMNode(this).querySelector('.transition-wrapper')
    const events = ['AnimationEnd', 'TransitionEnd']
    events.forEach(ev => {
      const eventType = whichEvent(el, [ev.toLowerCase(), `webkit${ev}`])
      el[`${type}EventListener`](eventType, this.onEnd, false)
    })
  }

  componentDidMount () {
    this.handleEndListener()
  }

  componentWillUnmount() {
    const { action, exist } = this.props
    if (!action && exist) {
      this.handleEndListener('remove')
    }
  }

  render () {
    const {
      className,
      action,
      toggleClass,
      enterClass,
      leaveClass,
      delay,
      duration,
      count,
      easing,
      reverse,
      children
    } = this.props

    // 动画样式
    const styleText = (() => {
      let style = {}
      // 设置延迟时长
      if (delay) {
        style.transitionDelay = delay
        style.animationDelay = delay
      }
      // 设置播放时长
      if (duration) {
        style.transitionDuration = duration
        style.animationDuration = duration
      }
      // 设置播放次数
      if (count) {
        style.animationIterationCount = count
      }
      // 设置缓动函数
      if (easing) {
        style.transitionTimingFunction = easing
        style.animationTimingFunction = easing
      }
      // 设置动画方向
      if (reverse) {
        style.animationDirection = 'alternate'
      }
      return style
    })()

    const transition = (
      <div
        className={
          classnames({
            transition: true
          })
        }
        style={
          {
            position: 'relative',
            overflow: 'hidden'
          }
        }
      >
        <div
          className={
            classnames({
              'transition-wrapper': true,
              [className]: className,
              [toggleClass]: action && toggleClass,
              [enterClass]: !toggleClass && action && enterClass,
              [leaveClass]: !toggleClass && !action && leaveClass,
            })
          }
          style={ styleText }
        >
          { children }
        </div>
      </div>
    )

    return transition
  }
}

export default Transition

关于“React怎么实现一个Transition过渡动画组件”这篇文章就分享到这里了,希望以上内容可以对大家有一定的帮助,使各位可以学到更多知识,如果觉得文章不错,请把它分享出去让更多的人看到。

《React怎么实现一个Transition过渡动画组件.doc》

下载本文的Word格式文档,以方便收藏与打印。