1. 程式人生 > >基於 React 實現一個 Transition 過渡動畫元件

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

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

基本實現

實現一個基礎的 CSS 過渡動畫元件,通過切換 CSS 樣式實現簡單的動畫效果,也就是通過新增或移除某個 class 樣式。因此需要給 Transition 元件新增一個 toggleClass 屬性,標識要切換的 class 樣式,再新增一個 action 屬性實現樣式切換,action 為 true 時新增 toggleClass 到動畫元素上,action 為 false 時移除 toggleClass。

安裝 classnames 外掛:

npm install classnames --save-dev

classnames 是一個簡單的JavaScript實用程式,用於有條件地將 className 連線在一起。

在 components 目錄下新建一個 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,在 JSX 中,使用 camelCase(小駝峰命名)來定義屬性的名稱,使用大括號“{}”嵌入任何有效的 JavaScript 表示式。
如:

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

等價於:

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

注意:
因為 JSX 語法上更接近 JavaScript 而不是 HTML,所以 React DOM 使用 camelCase(小駝峰命名)來定義屬性的名稱,而不使用 HTML 屬性名稱的命名約定。
例如,JSX 裡的 class 變成了 className,而 tabindex 則變為 tabIndex。

另外,在 React 中,props.children
包含元件所有的子節點,即元件的開始標籤和結束標籤之間的內容(與 Vue 中 slot 插槽相似)。例如:

<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-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 過渡動畫組