在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到后面深刻实践的这么一个过程,感受不少时候对于业务场景边界条件要多作探索。说不定还能收获一些让本身受用的知识。而不能仅仅是为了实现需求、为了赶进度而不作考虑,故而写出存在漏洞隐患的代码。