React Portal - 弹出层的优秀解决方案

3,865 阅读3分钟

对于需要使用弹出层的需求 ,Portal可以说是提供了一种完美的解决方案。相比于React Native中的实现更多的使用Modal或者绝对定位,Portal实在是简易友好得多。

场景

对话框,确认提示框,悬浮窗这些组件,一般都要做一个比当前视图层层级更高的View,但是现有的方案都很难跳出父容器的约束。如何破除这个约束?

Portal提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的优秀的方案

子节点可以被渲染到父组件之外的DOM,这不就是解除了这个约束了吗?

Portal:传送门。steam上有一款名为 Portal2 的游戏,互相利用传送门进行位移的配合闯关游戏,传送的含义和这里颇为相似,不过这里是“传送”组件。

使用和效果

普通的 render 方法就是将组件内的元素挂载到最近的DOM节点中。而 Portal 则通过一个API,将元素挂载到任意有效的DOM节点上。

ReactDOM.createPortal( children,container,key? )

具体参数类型如下:

现在开始在代码中使用:

  1. public目录下的index.html文件中,设置<div id="modal"></div>。后面 React 元素会被放到modal这个div里,这个DOM元素就是container

      <div id="root"></div>
      <div id="modal"></div>
    
  2. 对该 API 做简单封装,构建Modal组件。因为Portal API是读children子组件的,所以Portal肯定是作为容器来用。

    (这里直接操作操作了children,你也可以像Portal文档一样,在其中多加一个div层级隔离)

    const modalDivElement = document.getElementById("modal");
    
    export default function Modal({ children }) {
      return ReactDOM.createPortal(children, modalDivElement);
    }
    

    使用createPortal API,Modal 组件中的子元素children会被渲染到modalDivElement中,而不管你的Modal在何处使用。

  3. 使用Modal组件。在红色背景div中嵌入了Modal, Modal中只有一个div字符串zhangsan

    export default function App() {
      return (
        <div style={{ backgroundColor: "#a00", width: `200px`, height: `100px` }}>
          <Modal>
            <div>zhangsan</div>
          </Modal>
        </div>
      );
    }
    

    现在来看看效果:

    如果没有Modal层级,zhanghsan是应该在红色区域里面的。但是为什么加了Modal就在外面了?这时检查渲染结构,秘密就在这里:

    可以看到,Modal中的元素被渲染到了id="modal"的这个div里。来回顾一下发生了什么,我们的代码组件结构是这样:

          <div id="root">
            <div style={{ backgroundColor: "#a00", width: `200px`, height: `100px` }}>
              <Modal>
                <div>zhangsan</div>
              </Modal>
            </div>
          </div>
          <div id="modal"></div>
    

    但是实际的渲染结构却是这样(观察其中Modal子元素的变化):

          <div id="root">
            <div style={{ backgroundColor: "#a00", width: `200px`, height: `100px` }</div>
          </div>
          <div id="modal">
            <div>zhangsan</div>
          </div>
    

    实际渲染结构和代码结构并不一致了 ,是因为 Modal中的Portal将子元素方法放到了指定的container中。 子元素被跨层级渲染,就像被传送过去一样,这便是对Portal(传送)的解释。

制作一个 Modal 弹出框组件

上面只是演示了Portal传送子组件的效果,如果要做到弹出蒙层的效果,只需要在需要传送的children子元素上添加一个div包裹并添加样式,如果弹出层样式一致,可以直接Modal组件中,当然也可以在具体的children中实现并自定义样式。

实例:

例子代码:

export default function App() {
  const [isShow, setIsShow] = useState(false);
  function click(e) {
    console.info("e", e);
    setIsShow(!isShow);
  }
  return (
    <div
      style={{
        backgroundColor: "#a00",
        width: `200px`,
        height: `100px`,
      }}
      onClick={click}
    >
      {isShow && (
        <Modal>
          <span>zhangsan</span>
        </Modal>
      )}
    </div>
  );
}

Modal组件:

const modalDivElement = document.getElementById("modal");

export default function Modal({ children }) {
  const modalContent = (
    <div
      style={{
        display: "flex",
        justifyContent: "center",
        alignItems: "center",
        position: "absolute",
        top: 0,
        left: 0,
        right: 0,
        bottom: 0,
        backgroundColor: `rgba(0, 0, 0, 0.5)`,
      }}
    >
      {children}
    </div>
  );
  return ReactDOM.createPortal(modalContent, modalDivElement);
}

如果Modal交互和内容切换较多,可以进一步封装后通过ref命令式的调用,动态传入Modal和子组件切换。

Portal 事件冒泡

Portal中的事件冒泡是遵从React结构的,并不是实际渲染的DOM元素结构,也就是说上面的root节点可以获取到modal中冒泡出来的事件。

而,上面的页面中的click事件,可以获取点击红色区域触发Modal的事件,也能获取到Modal内部的点击span的事件:

这样,Modal中的交互控制并没有脱离当前的页面或组件,和没有使用Portal时的事件表现一致。