騷年,來一塊兒聊聊React Portals吧

在v16的React中,出現了一個新的特性Portals。當我第一眼看到Portals這個特性的時候,並無領略到這玩意有啥特殊的。不過近期在處理業務上的一個需求時,讓我意識到,Portals真的是很是有意思。css

場景復現

先還原一下產品需求吧。html

需求分析

在這個功能模塊中,A組件控制第一級tabs的展現,B組件控制第二級tabs的展現,C組件負責展現當前激活的tab內容;點擊組件C中的標題列表的某一項(如左圖所示),即在這個模塊中展開右圖的列表詳情D。且該詳情D會在此模塊中撐滿寬高顯示。前端

實現思路

組件C的大概結構用下列代碼模擬下。node

class C extends React.Component {
    constructor(props) {
        super(props)
        this.state = { visible: false }
    }
    handleClick = () => {
        this.setState({ visible: false })
    }

    render() {
        return (
            <div> <Others onClick={this.handleClick} /> {this.state.visible && <D />} </div> ) } } 複製代碼

經過點擊Othors中的某一個標題(模擬代碼,就不要糾結完整實現了),去修改C組件內state中的visible布爾值,進而決定D組件的顯隱。react

既然需求說的是佔滿寬高100%顯示,那麼我就很愉快的將組件D的css樣式寫成以下這般:git

.d {
    position: absolute;
    left: 0;
    top: 0;
    right: 0;
    bottom: 0;
    background: #fff;
    z-index: 10;
}
複製代碼

cmd+s保存以後,回到瀏覽器看看效果,發現確實實現了需求,交互上也和設計稿一致。github

貌似我前面說的這麼多好像沒什麼意義。可是當我喝完一瓶冰鎮的肥仔快樂水後,忽然意識到這樣的代碼確定是會出bug的。瀏覽器

bug現場

在上面的css代碼中,第一行position: absolute就是隱患所在。bash

咱們都知道應用position: absolute的元素是相對於最近的非 static 定位祖先元素來進行偏移的。在本例中,D組件的最近的祖先是C。雖然目前在C組件中咱們沒有使用諸如position: relative之類的css,可是某天若咱們須要在C組件中使用position: relative來進行元素定位時,D組件的寬高就只能撐滿C組件。(以下圖所示)app

做爲一名前端,100%還原UI能夠說是做爲前端er的尊嚴。咱們不可能去跟產品說:「你之後不能再往C組件再加定位元素了,不然會影響原來的功能。」

因此咱們如今抽象一下,在這個需求中咱們是但願當點擊C組件中的某一標題時,將D組件傳送到A組件下面去,再利用position: absolute使D組件撐滿A組件

聽起來好像咱們須要一個傳送門,當D組件穿過這個門出來後,就到達了A組件。

React世界的傳送門--Portals

Portals provide a first-class way to render children into a DOM node that exists outside the DOM hierarchy of the parent component.

Portals提供了一種很是棒的方法容許你將子節點渲染到父組件之外的DOM節點

其實在沒有深刻這個特性以前,個人腦子裏一直都不知道該找一個什麼詞去翻譯Portals,而如今真真切切以爲譯做「傳送門」真的是精髓。

Portals是什麼

咱們來簡單瞭解下Portals:

ReactDOM.createPortal(child, container)
複製代碼

第一個參數是一個可渲染的React子元素,第二個參數是個DOM元素。

代碼改造

那麼如今就讓咱們使用Portals來改造咱們的代碼。

首先在咱們須要獲取A組件的DOM元素,經過給組件A添加一個id,後續根據document.getElementById('component-a')獲取A的DOM引用:

<div id="component-a">
    {/* ... 組件A的代碼*/}
</div>
複製代碼

組件A的css須要加上一段position: relative,以確保後面的組件D是相對於組件A進行絕對定位的。

而後建立一個應用Portals的組件:

import * as React from 'react'
import { createPortal } from 'react-dom'

import './index.scss'
import { ComponentExt } from '@utils/reactExt'

export interface PortalsContainerProps {}

class PortalsContainer extends ComponentExt<PortalsContainerProps> {
    el: HTMLDivElement = null
    constructor(props: PortalsContainerProps) {
        super(props)
        const containers = document.getElementById('component-a')
        this.el = document.createElement('div')
        containers.appendChild(this.el)
    }
    componentWillUnmount() {
        document.getElementById('component-a').removeChild(this.el)
    }
    render() {
        return createPortal(<div className="portals-container">{this.props.children}</div>, this.el)
    }
}
export default PortalsContainer
複製代碼

css部分

.portals-container {
    position: absolute;
    left: 0;
    top: 0;
    right: 0;
    bottom: 0;
    background: #fff;
    z-index: 10;
}
複製代碼

最後將D組件做爲PortalsContainer的Children傳進去就能夠了:

import PortalsContainer from './PortalsContainer'

...

<PortalsContainer>
    {/* ... 組件D的代碼*/}    
</PortalsContainer>
複製代碼

如今咱們能夠看看最終經過傳送門優化後的代碼在DOM中的結構:

而後咱們就會發現很是神奇的事情。組件D明明是組件C的子元素,可是如今它的DOM結構倒是直接經過Portals插入到組件A的下面。是否是就像是React Portals爲咱們開啓了一個傳送門,讓咱們的組件D直接穿越到組件A的DOM結構中。

這樣一來,不管之後組件C加不加定位元素,咱們的組件D都是直接相對於整個模塊組件A進行定位的。

發散

當我領略到Portals這個傳送門特性時,發現諸如模態彈窗(Modal),全局提示(Message),文字提示(Tootip)之類的經常使用UI組件都能應用這個特性。倍兒爽!

就好比說,在ant-design的Popover氣泡卡片組件中,就有應用到Portals。

咱們能夠看看Popover這個組件在React中的組件結構:

箭頭處所示,就是Portals在Trigger中的應用。而最中間的Content組件,纔是咱們卡片中內容真正存在的地方。

ps: 各位要是對這種彈框類的組件有興趣,很是建議去看看rc-trigger的源碼。

結語

React對Portals的支持,很是好地解決了我在業務中遇到的問題,沒必要去考慮一些很是hack的方法。故寫篇博文記敘下這麼個過程。

回顧本身從第一次看到Portals到後面深刻實踐的這麼一個過程,感受不少時候對於業務場景邊界條件要多作探索。說不定還能收穫一些讓本身受用的知識。而不能僅僅是爲了實現需求、爲了趕進度而不作考慮,故而寫出存在漏洞隱患的代碼。

相關文章
相關標籤/搜索