這能夠在這裏看:http://leozdgao.me/reactzhong-de-portalzu-jian/javascript
幾個月前遇到了寫模態窗(modal)的需求,當初其實沒什麼思路,不知道怎麼用更React的方式實現模態窗,因而去學習了下ReactBootstrap的源代碼,發現了一個Portal組件,經過這個Portal的概念實現了React式的模態窗,諸如tooltip或者是notification等組件也是一樣的道理。最近在看React-conf的視頻時又聽到Ryan提到,最近從新回去看ReactBootstrap的源代碼,發現其實變化挺大的,原先Portal的部分已經被抽象出了另外一個庫react-overlays,因而準備總結下這個部分。html
模態窗扮演這至關於桌面應用的MessageBox的角色,各個瀏覽器對這個部分的支持有些缺陷(這裏指alert或confirm這些),好比每一個瀏覽器實現效果有差別、用戶能夠禁止其顯示,還有最重要的是沒有辦法靈活控制。java
因而咱們本身來實現,要扮演一個MessageBox,那麼咱們但願一個modal應該是永遠被置於頂層的,而又因爲Stacking Context的關係(在此不贅述),咱們會將modal直接append在body元素下,設置一個屬於它的z-index區間什麼的。react
回到React,在React中要實現一個模態窗,能夠是這樣的:jquery
handleClick () { $('.modal').modalShow() // 假設這是一個jquery的modal插件 }
我不知道是否是存在這樣一個jquery插件(應該是有的,不過我jquery用的很少),不過你們應該明白個人意思,利用React對其餘庫的友好來曲線救國。git
另外的一種方式是實現一個Modal類,經過Modal.show()
這樣的方法調用,這個方法會負責將模態窗render在它應該出現的地方,這個思路我一開始也有想到,不過本身其實更傾向於嘗試聲明式的React組件實現。github
那麼實現React式的模態窗會遇到什麼問題呢?好比有一個Container組件(承載頁面結構和業務邏輯的組件),在頁面的邏輯中會有一個modal彈出來,那麼咱們但願聲明式的寫法是這樣的:bootstrap
<div> <button>Show</button> {/* portals */} <Modal isShowed={this.state.modalShowed}> <p>Modal showed</p> </Modal> </div>
這裏存在的問題就是Stacking Context,對於一個通用組件而言沒有辦法保證上下文的樣式,因而就要講講這個Portal組件。瀏覽器
因此咱們須要的一個通用組件,它作以下的事情:app
能夠聲明式的寫在一個組件中
並不真正render在被聲明的地方
支持過渡動畫
那麼,像modal、tooltip、notification等組件都是能夠基於這個組件的。咱們叫這個組件爲Portal。
Portal這個東西我不知道怎麼給它一個合適的中文名,最初是在ReactBootstrap的項目裏看到,以後React-conf又提到,那麼相信應該是一個通用的概念了,因爲這個組件並不真正render在它被聲明的地方,姑且就翻譯爲『傳送門』吧......
首先,因爲它並不真正render在被聲明的地方,那麼:
render () { return null }
恩,是的,沒有辦法在render方法裏作文章,直接讓它返回null便可,它會在被聲明處留下一個noscript
標籤,無所謂了。
那麼真正的render是在哪裏進行的呢?咱們先準備下_renderOverlay
這個方法:
_renderOverlay() { let overlay = !this.props.children ? null : React.Children.only(this.props.children) if (overlay !== null) { this._mountOverlayTarget() // Save reference for future access. this._overlayInstance = React.render(overlay, this._overlayTarget) } else { // Unrender if the component is null for transitions to null this._unrenderOverlay() this._unmountOverlayTarget() } }
咱們把Portal的惟一子組件做爲是要一個遮罩物(overlay),要承載這個遮罩物,咱們須要一個DOM容器,因而咱們在_mountOverlayTarget
方法裏建立一個div
,也就是this._overlayTarget
,因而調用React.render
方法將組件掛載到這個div
節點上,並將保持對該實例的引用this._overlayInstance
。
一般狀況下,對於React組件來講,不直接操做DOM,並且React.render
方法咱們一般都是在入口點調用一次,其餘時候基本不用,然而對於Portal組件來講,這兩點都是必要的。
相應的unrender的部分,比較簡單,分別釋放this._overlayTarget
和this._overlayInstance
:
_unmountOverlayTarget() { if (this._overlayTarget) { this.getContainerDOMNode().removeChild(this._overlayTarget) this._overlayTarget = null } } _unrenderOverlay() { if (this._overlayTarget) { React.unmountComponentAtNode(this._overlayTarget) this._overlayInstance = null } }
好了,那麼咱們須要在何處調用_renderOverlay
呢,很容易想到:
componentDidMount () { this._renderOverlay() } componentDidUpdate () { this._renderOverlay() }
而後記得要擦屁股:
componentWillUnmount() { this._unrenderOverlay(); this._unmountOverlayTarget(); }
爲了增長Portal的靈活性,能夠給它傳一個container
屬性,用來指定『傳送門』的位置(默認爲body元素)。
實現上其實基本上就是這樣了,這裏要簡單提一下,以前就ReactBootstrap對Portal組件的實現而言,把isShowed
的邏輯給加在Portal裏,增長了一些實現的複雜度,這個項目好像重構過一波,如今的實現中isShowed
的邏輯被移出去了,Portal僅用於充當『傳送門』的角色,那麼以Modal爲例:
render () { if (this.props.isShowed) { return ( <Portal> <div> <div className='modal'>{this.props.children}</div> <div className='backdrop'></div> </div> </Portal> ) } else return null }
感受這樣的設計確實比以前更科學,而這個部分也被單獨抽象到了react-overlays中。
並不想在Portal組件裏再額外加入動畫相關的邏輯了,因而準備再封裝一層,加上對過渡動畫的支持。
提供幾個思路,一個是經過操做classname,這裏以模態窗爲例,先上代碼:
componentWillReceiveProps (nextProps) { const { show } = nextProps if (!show && this.props.show && this.props.closeTimeout) { // ready to close this.setState({ delaying: true, closing: true, opened: false }) setTimeout(() => { this.setState({ delaying: false, closing: false }) }, this.props.closeTimeout) } } componentDidUpdate (prevProps, prevState) { const { show } = prevProps if (!show && this.props.show) { // first show setTimeout(() => { // need do it in next loop this.setState({ opened: true }) }) } }
分別在合適的時機加上相應的class便可,對於show這個動做來講沒什麼問題,但對於close而言,顯然咱們須要等到transition的過渡時間結束後才真正unrender咱們的組件,因而咱們給它一個可傳入的屬性叫closeTimeout
,並在組件內具備一個this.state.delaying
這個狀態,那麼咱們的render邏輯應該是這樣的:
if (this.props.show || this.state.delaying) { return ( <Portal> <div className={classnames([ 'modal', { opened: this.state.opened, closing: this.state.closing } ])}> {this.props.children} </div> </Portal> ) } else { return null }
再靈活一點就是自定義opened和closing的classname了,這裏不贅述。
這是一種方法,不過動畫的部分不怎麼React式,是的,React動畫又是另外一塊內容了,這裏不會詳述,由於彷佛還不怎麼成熟,不過仍是給出一些可供參考的庫吧:
簡單地貼點代碼:
render () { return ( <Portal> <TimeoutTransitionGroup enterTimeout={200} leaveTimeout={250} transitionName='modal-anim'> {this.props.isShowed ? (<div className='modal'> {this.props.children} </div>) : null} </TimeoutTransitionGroup> </Portal> ) }
固然這裏的是否須要transition、timeout以及transitionName都應該是可配置的,做爲示例代碼就簡單點寫了。
推薦你們看看react-overlays,能夠直接使用裏面的Portal實現還有一些其餘有用的通用組件,文檔在這裏。或者其實有一個單獨的react-modal的實現也能夠直接用。
好了,結束了。