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

相关文章
相关标签/搜索