一步一步帶你封裝基於react的modal元件
中秋放假,一個人有點無聊,於是寫點博文暖暖心,同時祝大家中秋快樂~
接下來將一步步帶領大家實現一個基本的modal彈窗元件,封裝一個簡單的動畫元件,其中涉及到的一些知識點也會在程式碼中予以註釋講解。
一. modal元件的實現;
1. 環境搭建
我們使用create-react-app指令,快速搭建開發環境:
create-react-app modal 複製程式碼
安裝完成後,按照提示啟動專案,接著在 src
目錄下新建 modal
目錄,同時建立 modal.jsx modal.css
兩個檔案
modal.jsx
內容如下:
import React, { Component } from 'react'; import './modal.css'; class Modal extends Component { render() { return <div className="modal"> 這是一個modal元件 </div> } } export default Modal; 複製程式碼
回到根目錄,開啟App.js,將其中內容替換成如下:
import Modal from './modal/modal'; import React, { Component } from 'react'; import './App.css'; class App extends Component { render() { return <div className="app"> <Modal></Modal> </div> } } export default App; 複製程式碼
完成以上步驟後,我們瀏覽器中就會如下圖顯示了:

2. modal樣式完善
寫之前,我們先回想一下,我們平時使用的modal元件都有哪些元素,一個標題區,內容區,還有控制區,一個mask;
modal.jsx
內容修改如下:
import React, { Component } from 'react'; import './modal.css'; class Modal extends Component { render() { return <div className="modal-wrapper"> <div className="modal"> <div className="modal-title">這是modal標題</div> <div className="modal-content">這是modal內容</div> <div className="modal-operator"> <button className="modal-operator-close">取消</button> <button className="modal-operator-confirm">確認</button> </div> </div> <div className="mask"></div> </div> } } export default Modal; 複製程式碼
modal.css
內容修改如下:
.modal { position: fixed; width: 300px; height: 200px; top: 0; left: 0; right: 0; bottom: 0; margin: auto; border-radius: 5px; background: #fff; overflow: hidden; z-index: 9999; box-shadow: inset 0 0 1px 0 #000; } .modal-title { width: 100%; height: 50px; line-height: 50px; padding: 0 10px; } .modal-content { width: 100%; height: 100px; padding: 0 10px; } .modal-operator { width: 100%; height: 50px; } .modal-operator-close, .modal-operator-confirm { width: 50%; border: none; outline: none; height: 50px; line-height: 50px; opacity: 1; color: #fff; background: rgb(247, 32, 32); cursor: pointer; } .modal-operator-close:active, .modal-operator-confirm:active { opacity: .6; transition: opacity .3s; } .mask { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: #000; opacity: .6; z-index: 9998; } 複製程式碼
修改完成後,我們瀏覽器中就會如下圖顯示:

3. modal功能開發
到這裡我們的準備工作已經完成,接下就具體實現modal功能,再次回想,我們使用modal元件的時候,會有哪些基本的功能呢?
- 可以通過
visible
控制modal
的顯隱; -
title
,content
可以自定義顯示內容; - 點選取消關閉
modal
,同時會呼叫名為onClose
的回撥,點選確認會呼叫名為confirm
的回撥,並關閉modal
,點選蒙層mask
關閉modal
; - animate欄位可以開啟/關閉動畫;
3.1. 新增 visible
欄位控制顯隱
modal.jsx
修改如下:
import React, { Component } from 'react'; import './modal.css'; class Modal extends Component { constructor(props) { super(props) } render() { // 通過父元件傳遞的visile控制顯隱 const { visible } = this.props; return visible && <div className="modal-wrapper"> <div className="modal"> <div className="modal-title">這是modal標題</div> <div className="modal-content">這是modal內容</div> <div className="modal-operator"> <button className="modal-operator-close">取消</button> <button className="modal-operator-confirm">確認</button> </div> </div> <div className="mask"></div> </div> } } export default Modal; 複製程式碼
App.js
修改如下:
import Modal from './modal/modal'; import React, { Component } from 'react'; import './App.css'; class App extends Component { constructor(props) { super(props) // 這裡繫結this因為類中的方法不會自動繫結指向當前示例,我們需要手動繫結,不然方法中的this將是undefined,這是其中一種繫結的方法, // 第二種方法是使用箭頭函式的方法,如:showModal = () => {} // 第三種方法是呼叫的時候繫結,如:this.showModal.bind(this) this.showModal = this.showModal.bind(this) this.state = { visible: false } } showModal() { this.setState({ visible: true }) } render() { const { visible } = this.state return <div className="app"> <button onClick={this.showModal}>click here</button> <Modal visible={visible}></Modal> </div> } } export default App; 複製程式碼
以上我們通過父元件 App.js
中的visible狀態,傳遞給 modal
元件,再通過 button
的點選事件來控制visible的值以達到控制 modal
元件顯隱的效果
未點選按鈕效果如下圖:

點選按鈕後效果如下圖:

3.2. title
與 content
內容自定義
modal.jsx
修改如下:
import React, { Component } from 'react'; import './modal.css'; class Modal extends Component { constructor(props) { super(props) } render() { const { visible, title, children } = this.props; return visible && <div className="modal-wrapper"> <div className="modal"> {/* 這裡使用父元件的title*/} <div className="modal-title">{title}</div> {/* 這裡的content使用父元件的children*/} <div className="modal-content">{children}</div> <div className="modal-operator"> <button className="modal-operator-close">取消</button> <button className="modal-operator-confirm">確認</button> </div> </div> <div className="mask"></div> </div> } } export default Modal; 複製程式碼
App.js
修改如下:
import Modal from './modal/modal'; import React, { Component } from 'react'; import './App.css'; class App extends Component { constructor(props) { super(props) this.showModal = this.showModal.bind(this) this.state = { visible: false } } showModal() { this.setState({ visible: true }) } render() { const { visible } = this.state return <div className="app"> <button onClick={this.showModal}>click here</button> <Modal visible={visible} title="這是自定義title" > 這是自定義content </Modal> </div> } } export default App; 複製程式碼
接著我們點選頁面中的按鈕,結果顯示如下:

3.3. 取消與確認按鈕以及蒙層點選功能新增
寫前思考:我們需要點選取消按鈕關閉 modal
,那麼我們就需要在 modal
中維護一個狀態,然後用這個狀態來控制 modal
的顯隱,好像可行,但是我們再一想,我們前面是通過父元件的 visible
控制 modal
的顯隱,這樣不就矛盾了嗎?這樣不行,那我們作一下改變,如果父元件的狀態改變,那麼我們只更新這個狀態, modal
中點選取消我們也只更新這個狀態,最後用這個狀態值來控制 modal
的顯隱;至於 onClose
鉤子函式我們可以再更新狀態之前進行呼叫,確認按鈕的點選同取消。
modal.jsx
修改如下:
import React, { Component } from 'react'; import './modal.css'; class Modal extends Component { constructor(props) { super(props) this.confirm = this.confirm.bind(this) this.maskClick = this.maskClick.bind(this) this.closeModal = this.closeModal.bind(this) this.state = { visible: false } } // 首次渲染使用父元件的狀態更新modal中的visible狀態,只調用一次 componentDidMount() { this.setState({ visible: this.props.visible }) } // 每次接收props就根據父元件的狀態更新modal中的visible狀態,首次渲染不會呼叫 componentWillReceiveProps(props) { this.setState({ visible: props.visible }) } // 點選取消更新modal中的visible狀態 closeModal() { console.log('大家好,我叫取消,聽說你們想點我?傲嬌臉:princess:') const { onClose } = this.props onClose && onClose() this.setState({ visible: false }) } confirm() { console.log('大家好,我叫確認,樓上的取消是我兒子,腦子有點那個~') const { confirm } = this.props confirm && confirm() this.setState({ visible: false }) } maskClick() { console.log('大家好,我是蒙層,我被點選了') this.setState({ visible: false}) } render() { // 使用modal中維護的visible狀態來控制顯隱 const { visible } = this.state; const { title, children } = this.props; return visible && <div className="modal-wrapper"> <div className="modal"> <div className="modal-title">{title}</div> <div className="modal-content">{children}</div> <div className="modal-operator"> <button onClick={this.closeModal} className="modal-operator-close" >取消</button> <button onClick={this.confirm} className="modal-operator-confirm" >確認</button> </div> </div> <div className="mask" onClick={this.maskClick} ></div> </div> } } export default Modal; 複製程式碼
App.js
修改如下:
import Modal from './modal/modal'; import React, { Component } from 'react'; import './App.css'; class App extends Component { constructor(props) { super(props) this.confirm = this.confirm.bind(this) this.showModal = this.showModal.bind(this) this.closeModal = this.closeModal.bind(this) this.state = { visible: false } } showModal() { this.setState({ visible: true }) } closeModal() { console.log('我是onClose回撥') } confirm() { console.log('我是confirm回撥') } render() { const { visible } = this.state return <div className="app"> <button onClick={this.showModal}>click here</button> <Modal visible={visible} title="這是自定義title" confirm={this.confirm} onClose={this.closeModal} > 這是自定義content </Modal> </div> } } export default App; 複製程式碼
儲存後,我們再瀏覽器中分別點選取消和確認,控制檯中將會出現如下圖所示:

4. modal優化
以上就完成了一個基本的 modal
元件,但是我們還有一個疑問,就是現在引入的 modal
是在類名為 App
的元素之中,而一些被廣泛使用的UI框架中的 modal
元件確實在 body
層,無論你在哪裡引入,這樣就可以防止 modal
元件受到父元件的樣式的干擾。
而想要實現這種效果,我們必須得先了解React自帶的特性: Portals
(傳送門)。這個特性是在16版本之後新增的,而在16版本之前,都是通過使用 ReactDOM
的 unstable_renderSubtreeIntoContainer
方法處理,這個方法可以將元素渲染到指定元素中,與 ReactDOM.render
方法的區別就是,可以保留當前元件的上下文 context
, react-redux
就是基於 context
進行跨元件之間的通訊,所以若是使用 ReactDOM.render
進行渲染就會導致丟失上下文,從而導致所有基於 context
實現跨元件通訊的框架失效。
4.1. ReactDOM.unstable_renderSubtreeIntoContainer
的使用
ReactDOM.unstable_renderSubtreeIntoContainer( parentComponent, // 用來指定上下文 element,// 要渲染的元素 containerNode,// 渲染到指定的dom中 callback// 回撥 ); 複製程式碼
接下來在我們的專案中使用它, src
目錄下新建 oldPortal
目錄,並在其中新建 oldPortal.jsx
, oldPortal.jsx
中的內容如下:
import React from 'react'; import ReactDOM from 'react-dom'; class OldPortal extends React.Component { constructor(props) { super(props) } // 初始化時根據visible屬性來判斷是否渲染 componentDidMount() { const { visible } = this.props if (visible) { this.renderPortal(this.props); } } // 每次接受到props進行渲染與解除安裝操作 componentWillReceiveProps(props) { if (props.visible) { this.renderPortal(props) } else { this.closePortal() } } // 渲染 renderPortal(props) { if (!this.node) { // 防止多次建立node this.node = document.createElement('div'); } // 將當前node新增到body中 document.body.appendChild(this.node); ReactDOM.unstable_renderSubtreeIntoContainer( this,// 上下文指定當前的例項 props.children, // 渲染的元素為當前的children this.node,// 將元素渲染到我們新建的node中,這裡我們不使用第四個引數回撥. ); } // 解除安裝 closePortal() { if (this.node) { // 解除安裝元素中的元件 ReactDOM.unmountComponentAtNode(this.node) // 移除元素 document.body.removeChild(this.node) } } render() { return null; } } export default OldPortal 複製程式碼
儲存後,我們在 modal.jsx
中使用它:
import React, { Component } from 'react'; import OldPortal from '../oldPortal/oldPortal'; import './modal.css'; class Modal extends Component { constructor(props) { super(props) this.confirm = this.confirm.bind(this) this.maskClick = this.maskClick.bind(this) this.closeModal = this.closeModal.bind(this) this.state = { visible: false } } componentDidMount() { this.setState({ visible: this.props.visible }) } componentWillReceiveProps(props) { this.setState({ visible: props.visible }) } closeModal() { console.log('大家好,我叫取消,聽說你們想點我?傲嬌臉:princess:') const { onClose } = this.props onClose && onClose() this.setState({ visible: false }) } confirm() { console.log('大家好,我叫確認,樓上的取消是我兒子,腦子有點那個~') const { confirm } = this.props confirm && confirm() this.setState({ visible: false }) } maskClick() { console.log('大家好,我是蒙層,我被點選了') this.setState({ visible: false }) } render() { const { visible } = this.state; const { title, children } = this.props; return <OldPortal visible={visible}> <div className="modal-wrapper"> <div className="modal"> <div className="modal-title">{title}</div> <div className="modal-content">{children}</div> <div className="modal-operator"> <button onClick={this.closeModal} className="modal-operator-close" >取消</button> <button onClick={this.confirm} className="modal-operator-confirm" >確認</button> </div> </div> <div className="mask" onClick={this.maskClick} ></div> </div> </OldPortal> } } export default Modal; 複製程式碼
可以看到,我們僅僅是在 modal
中 return
的內容外層包裹一層 OldPortal
元件,然後將控制顯隱的狀態 visible
傳遞給了 OldPortal
元件,由 OldPortal
來實際控制 modal
的顯隱;然後我們點選頁面中的按鈕,同時開啟控制檯,發現 modal
如我們所想,床送到了 body
層:

4.2. 16版本 Portal
使用
在16版本中, react-dom
原生提供了一個方法 ReactDOM.createPortal()
,用來實現傳送門的功能:
ReactDOM.createPortal( child,// 要渲染的元素 container // 指定渲染的父元素 ) 複製程式碼
引數比之 unstable_renderSubtreeIntoContainer
減少了兩個,接著我們在專案中使用它.
在 src
目錄下新建 newPortal
目錄,在其中新建 newPortal.jsx
, newPortal.jsx
內容如下:
import React from 'react'; import ReactDOM from 'react-dom'; class NewPortal extends React.Component { constructor(props) { super(props) // 初始化建立渲染的父元素並新增到body下 this.node = document.createElement('div'); document.body.appendChild(this.node); } render() { const { visible, children } = this.props; // 直接通過顯隱表示 return visible && ReactDOM.createPortal( children, this.node, ); } } export default NewPortal 複製程式碼
可以很清晰的看到內容對比 unstable_renderSubtreeIntoContainer
的實現簡化了很多,然後我們在 modal.jsx
中使用:
import React, { Component } from 'react'; import NewPortal from '../newPortal/newPortal'; import './modal.css'; class Modal extends Component { constructor(props) { super(props) this.confirm = this.confirm.bind(this) this.maskClick = this.maskClick.bind(this) this.closeModal = this.closeModal.bind(this) this.state = { visible: false } } componentDidMount() { this.setState({ visible: this.props.visible }) } componentWillReceiveProps(props) { this.setState({ visible: props.visible }) } closeModal() { console.log('大家好,我叫取消,聽說你們想點我?傲嬌臉:princess:') const { onClose } = this.props onClose && onClose() this.setState({ visible: false }) } confirm() { console.log('大家好,我叫確認,樓上的取消是我兒子,腦子有點那個~') const { confirm } = this.props confirm && confirm() this.setState({ visible: false }) } maskClick() { console.log('大家好,我是蒙層,我被點選了') this.setState({ visible: false }) } render() { const { visible } = this.state; const { title, children } = this.props; return <NewPortal visible={visible}> <div className="modal-wrapper"> <div className="modal"> <div className="modal-title">{title}</div> <div className="modal-content">{children}</div> <div className="modal-operator"> <button onClick={this.closeModal} className="modal-operator-close" >取消</button> <button onClick={this.confirm} className="modal-operator-confirm" >確認</button> </div> </div> <div className="mask" onClick={this.maskClick} ></div> </div> </NewPortal> } } export default Modal; 複製程式碼
使用上與 OldPortal
一樣,接下來看看瀏覽器中看看效果是否如我們所想:

可以說 Portals
是彈窗類元件的靈魂,這裡對 Portals
的使用僅僅是作為一個引導,講解了其核心功能,並沒有深入去實現一些複雜的公共方法,有興趣的讀者可以搜尋相關的文章,都有更詳細的講解.
二. 出入場動畫實現
1. 動畫新增
從一個簡單的效果開始(使用的程式碼是以上使用 NewPortal
元件的 Modal
元件), modal
彈出時逐漸放大,放大到1.1倍,最後又縮小到1倍,隱藏時,先放大到1.1倍,再縮小,直到消失.
慣例先思考: 我們通過控制什麼達到放大縮小的效果?我們如何將放大和縮小這個過程從瞬間變為一個漸變的過程?我們在什麼時候開始放大縮小?又在什麼時候結束放大縮小?
放大和縮小我們通過 css3
的屬性 transform scale
進行控制,漸變的效果使用 transition
過度似乎是不錯的選擇,而放大縮小的時機,分為元素開始出現,出現中,出現結束,開始消失,消失中,消失結束六種狀態,然後我們分別定義這六種狀態的 scale
引數,再使用 transition
進行過度,應該就能實現我們需要的效果了:
再 modal.css
新增如下程式碼:
.modal-enter { transform: scale(0); } .modal-enter-active { transform: scale(1.1); transition: all .2s linear; } .modal-enter-end { transform: scale(1); transition: all .1s linear; } .modal-leave { transform: scale(1); } .modal-leave-active { transform: scale(1.1); transition: all .1s linear; } .modal-leave-end { transform: scale(0); transition: all .2s linear; } 複製程式碼
六種類名分別定義了出現與消失的六種狀態,同時設定了各自的過度時間,接下來我們就在不同的過程給元素新增對應的類名,就能控制元素的顯示狀態了.
在我們寫邏輯之前,我們還需要注意一點,之前我們元件的顯隱是在 NewPortal
元件中實際控制的,但是我們在 Modal
元件中新增動畫,就需要嚴格掌控顯隱的時機,比如剛渲染就要開始動畫,動畫結束之後才能隱藏,這樣就不適合在 NewPortal
元件中控制顯隱了.有的讀者就疑惑了,為什麼不直接在 NewPortal
元件中新增動畫呢?當然這個問題的答案是肯定的,但是 NewPortal
的功能是傳送,並不複雜動畫,我們要保持它的純淨,不宜與其他元件耦合.
修改 newPortal.jsx
的內容如下:
import React from 'react'; import ReactDOM from 'react-dom'; class NewPortal extends React.Component { constructor(props) { super(props) this.node = document.createElement('div'); document.body.appendChild(this.node); } render() { const { children } = this.props; return ReactDOM.createPortal( children, this.node, ); } } export default NewPortal 複製程式碼
修改 modal.jsx
的內容如下:
import React, { Component } from 'react'; import NewPortal from '../newPortal/newPortal'; import './modal.css'; class Modal extends Component { constructor(props) { super(props) this.confirm = this.confirm.bind(this) this.maskClick = this.maskClick.bind(this) this.closeModal = this.closeModal.bind(this) this.leaveAnimate = this.leaveAnimate.bind(this) this.enterAnimate = this.enterAnimate.bind(this) this.state = { visible: false, classes: null, } } componentDidMount() { this.setState({ visible: this.props.visible }) } componentWillReceiveProps(props) { if (props.visible) { // 接收到父元件的props時,如果是true則進行動畫渲染 this.enterAnimate() } } // 進入動畫 enterAnimate() { // 這裡定義每種狀態的類名,就是我們之前modal.css檔案中新增的類 const enterClasses = 'modal-enter' const enterActiveClasses = 'modal-enter-active' const enterEndActiveClasses = 'modal-enter-end' // 這裡定義了每種狀態的過度時間,對應著modal.css中對應類名下的transition屬性的時間,這裡的單位為毫秒 const enterTimeout = 0 const enterActiveTimeout = 200 const enterEndTimeout = 100 // 將顯隱狀態改為true,同時將classes改為enter狀態的類名 this.setState({ visible: true, classes: enterClasses }) // 這裡使用定時器,是因為定時器中的函式會被加入到事件佇列,帶到主執行緒任務進行完成才會被呼叫,相當於在元素渲染出來並且加上初始的類名後enterTimeout時間後開始執行. // 因為開始狀態並不需要過度,所以我們直接將之設定為0. const enterActiveTimer = setTimeout(_ => { this.setState({ classes: enterActiveClasses }) clearTimeout(enterActiveTimer) }, enterTimeout) const enterEndTimer = setTimeout(_ => { this.setState({ classes: enterEndActiveClasses }) clearTimeout(enterEndTimer) }, enterTimeout + enterActiveTimeout) // 最後將類名置空,還原元素本來的狀態 const initTimer = setTimeout(_ => { this.setState({ classes: '' }) clearTimeout(initTimer) }, enterTimeout + enterActiveTimeout + enterEndTimeout) } // 離開動畫 leaveAnimate() { const leaveClasses = 'modal-leave' const leaveActiveClasses = 'modal-leave-active' const leaveEndActiveClasses = 'modal-leave-end' const leaveTimeout = 0 const leaveActiveTimeout = 100 const leaveEndTimeout = 200 // 初始元素已經存在,所以不需要改變顯隱狀態 this.setState({ classes: leaveClasses }) const leaveActiveTimer = setTimeout(_ => { this.setState({ classes: leaveActiveClasses }) clearTimeout(leaveActiveTimer) }, leaveTimeout) const leaveEndTimer = setTimeout(_ => { this.setState({ classes: leaveEndActiveClasses }) clearTimeout(leaveEndTimer) }, leaveTimeout + leaveActiveTimeout) // 最後將顯隱狀態改為false,同時將類名還原為初始狀態 const initTimer = setTimeout(_ => { this.setState({ visible: false, classes: '' }) clearTimeout(initTimer) }, leaveTimeout + leaveActiveTimeout + leaveEndTimeout) } closeModal() { console.log('大家好,我叫取消,聽說你們想點我?傲嬌臉:princess:') const { onClose } = this.props onClose && onClose() // 點選取消後呼叫離開動畫 this.leaveAnimate() } confirm() { console.log('大家好,我叫確認,樓上的取消是我兒子,腦子有點那個~') const { confirm } = this.props confirm && confirm() this.leaveAnimate() } maskClick() { console.log('大家好,我是蒙層,我被點選了') this.setState({ visible: false }) } render() { const { visible, classes } = this.state; const { title, children } = this.props; return <NewPortal> <div className="modal-wrapper"> { visible && <div className={`modal ${classes}`}> <div className="modal-title">{title}</div> <div className="modal-content">{children}</div> <div className="modal-operator"> <button onClick={this.closeModal} className="modal-operator-close" >取消</button> <button onClick={this.confirm} className="modal-operator-confirm" >確認</button> </div> </div> } {/* 這裡暫時註釋蒙層,防止干擾 */} {/* <div className="mask" onClick={this.maskClick} ></div> */} </div> </NewPortal> } } export default Modal; 複製程式碼
效果如下:

2. 動畫元件封裝
實現了動畫效果,但是程式碼全部在 modal.jsx
中,一點也不優雅,而且也不能複用,因此我們需要考慮將之抽象成一個 Transition
元件。
思路:我們從需要的功能點出發,來考慮如何進行封裝。首先傳入的顯隱狀態值控制元素的顯隱;給與一個類名,其能匹配到對應的六種狀態類名;可以配置每種狀態的過渡時間;可以控制是否使用動畫;
在 src
目錄新建 transition
目錄,建立檔案 transition.jsx
,內容如下:
import React from 'react'; // 這裡引入classnames處理類名的拼接 import classnames from 'classnames'; class Transition extends React.Component { constructor(props) { super(props) this.getClasses = this.getClasses.bind(this) this.enterAnimate = this.enterAnimate.bind(this) this.leaveAnimate = this.leaveAnimate.bind(this) this.appearAnimate = this.appearAnimate.bind(this) this.cloneChildren = this.cloneChildren.bind(this) this.state = { visible: false, classes: null, } } // 過渡時間不傳入預設為0 static defaultProps = { animate: true, visible: false, transitionName: '', appearTimeout: 0, appearActiveTimeout: 0, appearEndTimeout: 0, enterTimeout: 0, enterActiveTimeout: 0, enterEndTimeout: 0, leaveTimeout: 0, leaveEndTimeout: 0, leaveActiveTimeout: 0, } // 這裡我們添加了首次渲染動畫。只出現一次 componentWillMount() { const { transitionName, animate, visible } = this.props; if (!animate) { this.setState({ visible }) return } this.appearAnimate(this.props, transitionName) } componentWillReceiveProps(props) { const { transitionName, animate, visible } = props if (!animate) { this.setState({ visible }) return } if (!props.visible) { this.leaveAnimate(props, transitionName) } else { this.enterAnimate(props, transitionName) } } // 首次渲染的入場動畫 appearAnimate(props, transitionName) { const { visible, appearTimeout, appearActiveTimeout, appearEndTimeout } = props const { initClasses, activeClasses, endClasses } = this.getClasses('appear', transitionName) this.setState({ visible, classes: initClasses }) setTimeout(_ => { this.setState({ classes: activeClasses }) }, appearTimeout) setTimeout(_ => { this.setState({ classes: endClasses }) }, appearActiveTimeout + appearTimeout) setTimeout(_ => { this.setState({ classes: '' }) }, appearEndTimeout + appearActiveTimeout + appearTimeout) } // 入場動畫 enterAnimate(props, transitionName) { const { visible, enterTimeout, enterActiveTimeout, enterEndTimeout } = props const { initClasses, activeClasses, endClasses } = this.getClasses('enter', transitionName) this.setState({ visible, classes: initClasses }) const enterTimer = setTimeout(_ => { this.setState({ classes: activeClasses }) clearTimeout(enterTimer) }, enterTimeout) const enterActiveTimer = setTimeout(_ => { this.setState({ classes: endClasses }) clearTimeout(enterActiveTimer) }, enterActiveTimeout + enterTimeout) const enterEndTimer = setTimeout(_ => { this.setState({ classes: '' }) clearTimeout(enterEndTimer) }, enterEndTimeout + enterActiveTimeout + enterTimeout) } // 出場動畫 leaveAnimate(props, transitionName) { const { visible, leaveTimeout, leaveActiveTimeout, leaveEndTimeout } = props const { initClasses, activeClasses, endClasses } = this.getClasses('leave', transitionName) this.setState({ classes: initClasses }) const leaveTimer = setTimeout(_ => { this.setState({ classes: activeClasses }) clearTimeout(leaveTimer) }, leaveTimeout) const leaveActiveTimer = setTimeout(_ => { this.setState({ classes: endClasses }) clearTimeout(leaveActiveTimer) }, leaveActiveTimeout + leaveTimeout) const leaveEndTimer = setTimeout(_ => { this.setState({ visible, classes: '' }) clearTimeout(leaveEndTimer) }, leaveEndTimeout + leaveActiveTimeout + leaveTimeout) } // 類名統一配置 getClasses(type, transitionName) { const initClasses = classnames({ [`${transitionName}-appear`]: type === 'appear', [`${transitionName}-enter`]: type === 'enter', [`${transitionName}-leave`]: type === 'leave', }) const activeClasses = classnames({ [`${transitionName}-appear-active`]: type === 'appear', [`${transitionName}-enter-active`]: type === 'enter', [`${transitionName}-leave-active`]: type === 'leave', }) const endClasses = classnames({ [`${transitionName}-appear-end`]: type === 'appear', [`${transitionName}-enter-end`]: type === 'enter', [`${transitionName}-leave-end`]: type === 'leave', }) return { initClasses, activeClasses, endClasses } } cloneChildren() { const { classes } = this.state const children = this.props.children const className = children.props.className // 通過React.cloneElement給子元素新增額外的props, return React.cloneElement( children, { className: `${className} ${classes}` } ) } render() { const { visible } = this.state return visible && this.cloneChildren() } } export default Transition 複製程式碼
modal.jsx
內容修改如下:
import React, { Component } from 'react'; import NewPortal from '../newPortal/newPortal'; import Transition from '../transition/transition'; import './modal.css'; class Modal extends Component { constructor(props) { super(props) this.confirm = this.confirm.bind(this) this.maskClick = this.maskClick.bind(this) this.closeModal = this.closeModal.bind(this) this.state = { visible: false, } } componentDidMount() { this.setState({ visible: this.props.visible }) } componentWillReceiveProps(props) { this.setState({ visible: props.visible }) } closeModal() { console.log('大家好,我叫取消,聽說你們想點我?傲嬌臉:princess:') const { onClose } = this.props onClose && onClose() this.setState({ visible: false }) } confirm() { console.log('大家好,我叫確認,樓上的取消是我兒子,腦子有點那個~') const { confirm } = this.props confirm && confirm() this.setState({ visible: false }) } maskClick() { console.log('大家好,我是蒙層,我被點選了') this.setState({ visible: false }) } render() { const { visible } = this.state; const { title, children } = this.props; return <NewPortal> {/* 引入transition元件,去掉了外層的modal-wrapper */} <Transition visible={visible} transitionName="modal" enterActiveTimeout={200} enterEndTimeout={100} leaveActiveTimeout={100} leaveEndTimeout={200} > <div className="modal"> <div className="modal-title">{title}</div> <div className="modal-content">{children}</div> <div className="modal-operator"> <button onClick={this.closeModal} className="modal-operator-close" >取消</button> <button onClick={this.confirm} className="modal-operator-confirm" >確認</button> </div> </div> {/* 這裡的mask也可以用transition元件包裹,新增淡入淡出的過渡效果,這裡不再新增,有興趣的讀者可以自己實踐下 */} {/* <div className="mask" onClick={this.maskClick} ></div> */} </Transition> </NewPortal> } } export default Modal; 複製程式碼
文章到這裡就寫完了,為了閱讀的完整性,每個步驟都是貼的完整的程式碼,導致全文篇幅過長,感謝您的閱讀。
ofollow,noindex">本文程式碼地址 ,歡迎star~