在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的。瀏覽器
在上面的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組件。
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:
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到後面深刻實踐的這麼一個過程,感受不少時候對於業務場景邊界條件要多作探索。說不定還能收穫一些讓本身受用的知識。而不能僅僅是爲了實現需求、爲了趕進度而不作考慮,故而寫出存在漏洞隱患的代碼。