基於 React 實現一個 Transition 過渡動畫組件

過渡動畫使 UI 更富有表現力而且易於使用。如何使用 React 快速的實現一個 Transition 過渡動畫組件?css

基本實現

實現一個基礎的 CSS 過渡動畫組件,經過切換 CSS 樣式實現簡單的動畫效果,也就是經過添加或移除某個 class 樣式。所以須要給 Transition 組件添加一個 toggleClass 屬性,標識要切換的 class 樣式,再添加一個 action 屬性實現樣式切換,action 爲 true 時添加 toggleClass 到動畫元素上,action 爲 false 時移除 toggleClass。html

安裝 classnames 插件:node

npm install classnames --save-dev

classnames 是一個簡單的JavaScript實用程序,用於有條件地將 className 鏈接在一塊兒。react

在 components 目錄下新建一個 Transition 文件夾,並在該文件夾下新建一個 Transition.jsx 文件:git

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,在 JSX 中,使用 camelCase(小駝峯命名)來定義屬性的名稱,使用大括號「{}」嵌入任何有效的 JavaScript 表達式
如:github

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

等價於:web

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

注意:
由於 JSX 語法上更接近 JavaScript 而不是 HTML,因此 React DOM 使用 camelCase(小駝峯命名)來定義屬性的名稱,而不使用 HTML 屬性名稱的命名約定。
例如,JSX 裏的 class 變成了 className,而 tabindex 則變爲 tabIndex。npm

另外,在 React 中,props.children
包含組件全部的子節點,即組件的開始標籤和結束標籤之間的內容(與 Vue 中 slot 插槽類似)。例如:瀏覽器

<Button>默認按鈕</Button>

在 Button 組件中獲取 props.children,就能夠獲得字符串「默認按鈕」。sass

接下來,在 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-types 實現運行時類型檢查。

注意:
prop-types 是一個運行時類型檢查工具,也是 create-react-app 腳手架默認配置的運行時類型檢查工具,使用時直接引入便可,無需安裝。

完整的 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

如今,可使用咱們的 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 動畫庫。接下來,實如今 Transition 組件中使用 Animate.css 實現強大的 CSS3 動畫。

因爲 Animate.css 動畫在進入動畫和離開動畫一般使用兩個效果相反的 class 樣式,所以,須要給 Transition 組件添加 enterClass 和 leaveClass 兩個屬性,實現動畫切換。

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 樣式的狀況,而 enterClass 和 leaveClass 適用於那些進入動畫與離開動畫切換不一樣的 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 的接口。動畫一般能夠設置延遲時間,播放時長,播放次數等屬性。所以,須要給 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 的功能已經至關豐富,能夠很精細的控制 CSS3 動畫。

優化

這一步,咱們須要針對 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 生命週期的介紹請移步組件生命週期

react-dom 提供了可在 React 應用中使用的 DOM 方法。

獲取兼容性的 animationend 事件和 transitionend 事件。不一樣的瀏覽器要求使用不一樣的前綴,由於火狐和IE都已經支持了這兩個事件,所以,只需針對 webkit 內核瀏覽器進行兼容的 webkitTransitionEnd 事件檢測。檢測函數代碼以下:

/**
 * 瀏覽器兼容事件檢測函數
 *
 * @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 過渡動畫組件

相關文章
相關標籤/搜索