過渡動畫使 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 是一款強大的預設 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 增長了如下設置屬性:
目前,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