我对弹窗的设想是:
- 可定制宽高,append 到 body,fixed 定位
- 容易定制样式,来适应不断变化的多种多样的需求
在 React 中,要实现第一点,可用到 Portal 技术。
Portal 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的优秀的方案。
ReactDOM.createPortal(child, container)
第一个参数(child)是任何可渲染的 React 子元素,例如一个元素,字符串或 fragment。第二个参数(container)是一个
DOM 元素。
具体用法可查看 官网 Portals 的文档。
对此,我想先实现的是一个 hook usePortal,它的功能就是返回一个 append 到 body 下的结点。
import {
useRef, useEffect } from 'react'
/**
* 将元素 append 到 body
* @param {HTMLElement} rootElem
*/
function appendToBody(rootElem) {
document.body.appendChild(rootElem)
}
export default function usePortal() {
const rootElemRef = useRef(null)
// 延迟创建节点,而不是在初始化传入,
// 这样才不会每次运行都重新创建
/**
* It's important we evaluate this lazily:
* - We need first render to contain the DOM element, so it shouldn't happen
* in useEffect. We would normally put this in the constructor().
* - We can't do 'const rootElemRef = useRef(document.createElement('div))',
* since this will run every single render (that's a lot).
* - We want the ref to consistently point to the same DOM element and only
* ever run once.
* @link https://reactjs.org/docs/hooks-faq.html#how-to-create-expensive-objects-lazily
*/
const getRootElem = () => {
if (!rootElemRef.current) {
rootElemRef.current = document.createElement('div')
}
return rootElemRef.current
}
useEffect(() => {
const root = rootElemRef.current
const parentElem = document.createElement('div')
appendToBody(parentElem)
// 挂载到容器节点
parentElem.appendChild(root)
return () => {
root.remove()
}
}, [])
return getRootElem()
}
拿到结点后,开始着手写 Portal 组件。
全局的工具类 tool.css
.fixed {
position: fixed;
}
.z-999