React中的Portal組件

幾個月前遇到了寫模態(tài)窗(modal)的需求疗我,當初其實沒什么思路咆畏,不知道怎么用更React的方式實現(xiàn)模態(tài)窗,于是去學習了下ReactBootstrap的源代碼吴裤,發(fā)現(xiàn)了一個Portal組件旧找,通過這個Portal的概念實現(xiàn)了React式的模態(tài)窗,諸如tooltip或者是notification等組件也是同樣的道理麦牺。最近在看React-conf的視頻時又聽到Ryan提到钮蛛,最近重新回去看ReactBootstrap的源代碼,發(fā)現(xiàn)其實變化挺大的剖膳,原先Portal的部分已經(jīng)被抽象出了另一個庫react-overlays魏颓,于是準備總結(jié)下這個部分。

模態(tài)窗的實現(xiàn)思路

模態(tài)窗扮演這相當于桌面應用的MessageBox的角色吱晒,各個瀏覽器對這個部分的支持有些缺陷(這里指alert或confirm這些)甸饱,比如每個瀏覽器實現(xiàn)效果有差異、用戶可以禁止其顯示仑濒,還有最重要的是沒有辦法靈活控制柜候。

于是我們自己來實現(xiàn),要扮演一個MessageBox躏精,那么我們希望一個modal應該是永遠被置于頂層的,而又由于Stacking Context的關(guān)系(在此不贅述)鹦肿,我們會將modal直接append在body元素下矗烛,設置一個屬于它的z-index區(qū)間什么的。

回到React箩溃,在React中要實現(xiàn)一個模態(tài)窗瞭吃,可以是這樣的:

handleClick () {
   $('.modal').modalShow() // 假設這是一個jquery的modal插件
}

我不知道是不是存在這樣一個jquery插件(應該是有的,不過我jquery用的不多)涣旨,不過大家應該明白我的意思歪架,利用React對其他庫的友好來曲線救國。

另外的一種方式是實現(xiàn)一個Modal類霹陡,通過Modal.show()這樣的方法調(diào)用和蚪,這個方法會負責將模態(tài)窗render在它應該出現(xiàn)的地方止状,這個思路我一開始也有想到,不過自己其實更傾向于嘗試聲明式的React組件實現(xiàn)攒霹。

那么實現(xiàn)React式的模態(tài)窗會遇到什么問題呢怯疤?比如有一個Container組件(承載頁面結(jié)構(gòu)和業(yè)務邏輯的組件),在頁面的邏輯中會有一個modal彈出來催束,那么我們希望聲明式的寫法是這樣的:

<div>
  <button>Show</button>
  {/* portals */}
  <Modal isShowed={this.state.modalShowed}>
    <p>Modal showed</p>
  </Modal>
</div>

這里存在的問題就是Stacking Context集峦,對于一個通用組件而言沒有辦法保證上下文的樣式,于是就要講講這個Portal組件抠刺。

什么是Portal組件

所以我們需要的一個通用組件塔淤,它做如下的事情:

  • 可以聲明式的寫在一個組件中
  • 并不真正render在被聲明的地方
  • 支持過渡動畫

那么,像modal速妖、tooltip高蜂、notification等組件都是可以基于這個組件的。我們叫這個組件為Portal买优。

Portal這個東西我不知道怎么給它一個合適的中文名妨马,最初是在ReactBootstrap的項目里看到,之后React-conf又提到杀赢,那么相信應該是一個通用的概念了烘跺,由于這個組件并不真正render在它被聲明的地方,姑且就翻譯為『傳送門』吧......

實現(xiàn)一個Portal組件

首先脂崔,由于它并不真正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方法里創(chuàng)建一個div早抠,也就是this._overlayTarget蕊连,于是調(diào)用React.render方法將組件掛載到這個div節(jié)點上悬垃,并將保持對該實例的引用this._overlayInstance咪奖。

通常情況下,對于React組件來說羊赵,不直接操作DOM,而且React.render方法我們通常都是在入口點調(diào)用一次昧捷,其他時候基本不用,然而對于Portal組件來說靡挥,這兩點都是必要的序矩。

相應的unrender的部分跋破,比較簡單,分別釋放this._overlayTargetthis._overlayInstance

_unmountOverlayTarget() {
  if (this._overlayTarget) {
    this.getContainerDOMNode().removeChild(this._overlayTarget)
    this._overlayTarget = null
  }
}

_unrenderOverlay() {
  if (this._overlayTarget) {
    React.unmountComponentAtNode(this._overlayTarget)
    this._overlayInstance = null
  }
}

好了毒返,那么我們需要在何處調(diào)用_renderOverlay呢,很容易想到:

componentDidMount () {
  this._renderOverlay()
}

componentDidUpdate () {
  this._renderOverlay()
}

然后記得要擦屁股:

componentWillUnmount() {
  this._unrenderOverlay();
  this._unmountOverlayTarget();
}

為了增加Portal的靈活性拧簸,可以給它傳一個container屬性,用來指定『傳送門』的位置(默認為body元素)盆赤。

實現(xiàn)上其實基本上就是這樣了贾富,這里要簡單提一下,之前就ReactBootstrap對Portal組件的實現(xiàn)而言牺六,把isShowed的邏輯給加在Portal里颤枪,增加了一些實現(xiàn)的復雜度,這個項目好像重構(gòu)過一波淑际,現(xiàn)在的實現(xiàn)中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組件里再額外加入動畫相關(guān)的邏輯了台囱,于是準備再封裝一層淡溯,加上對過渡動畫的支持。

提供幾個思路簿训,一個是通過操作classname咱娶,這里以模態(tài)窗為例米间,先上代碼:

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的過渡時間結(jié)束后才真正unrender我們的組件,于是我們給它一個可傳入的屬性叫closeTimeout琼了,并在組件內(nèi)具有一個this.state.delaying這個狀態(tài)逻锐,那么我們的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動畫又是另一塊內(nèi)容了盏档,這里不會詳述,因為似乎還不怎么成熟燥爷,不過還是給出一些可供參考的庫吧:

簡單地貼點代碼:

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實現(xiàn)還有一些其他有用的通用組件,文檔在這里鱼填∫┯校或者其實有一個單獨的react-modal的實現(xiàn)也可以直接用。

好了苹丸,結(jié)束了愤惰。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市赘理,隨后出現(xiàn)的幾起案子宦言,更是在濱河造成了極大的恐慌奠旺,老刑警劉巖施流,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異忿晕,居然都是意外死亡银受,警方通過查閱死者的電腦和手機鸦采,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來肄程,“玉大人,你說我怎么就攤上這事吐限」邮迹” “怎么了?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵狐粱,是天一觀的道長肌蜻。 經(jīng)常有香客問我必尼,道長,這世上最難降的妖魔是什么豆挽? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任帮哈,我火速辦了婚禮娘侍,結(jié)果婚禮上泳炉,老公的妹妹穿的比我還像新娘。我一直安慰自己氧腰,他們只是感情好,可當我...
    茶點故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著斤富,像睡著了一般锻狗。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上油额,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天潦嘶,我揣著相機與錄音崇众,去河邊找鬼。 笑死锰蓬,一個胖子當著我的面吹牛眯漩,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播舱卡,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼灼狰,長吁一口氣:“原來是場噩夢啊……” “哼浮禾!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起蝴簇,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤熬词,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后歪今,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體烦秩,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡贿肩,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年兴革,在試婚紗的時候發(fā)現(xiàn)自己被綠了泊柬。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片兽赁。...
    茶點故事閱讀 39,785評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡闸氮,死狀恐怖教沾,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情授翻,我是刑警寧澤,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布巡语,位于F島的核電站男公,受9級特大地震影響合陵,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜踏拜,卻給世界環(huán)境...
    茶點故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一速梗、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧姻锁,春花似錦、人聲如沸烁设。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽糠睡。三九已至,卻和暖如春信认,著一層夾襖步出監(jiān)牢的瞬間均抽,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工潦蝇, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留深寥,地道東北人。 一個月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓则酝,卻偏偏與公主長得像沽讹,于是被迫代替她去往敵國和親返十。 傳聞我的和親對象是個殘疾皇子妥泉,可洞房花燭夜當晚...
    茶點故事閱讀 44,713評論 2 354

推薦閱讀更多精彩內(nèi)容