要实现的具体功能如下:
编写的自定义 Modal 可以通过两种方式调用:
<Modal title={'《React进阶实践指南》'} visible={visible} >
<div> hello,world </div>
</Modal> Modal.show({ /* 自定义弹窗的显示 */
content:<p>确定购买《React进阶指南小册》吗</p>,
title:'《React进阶实践指南》',
onOk:()=>console.log('点击确定'),
onCancel:()=>console.log('点击取消'),
onClose:()=> Modal.hidden() /* 自定义弹窗的隐藏 */
})如上,Modal.show 控制自定义弹窗的显示,可以通过 Modal.hidden 控制弹窗的隐藏,业务层不需要挂载组件。
其他要求:
实现的 Modal 组件需要 props 配置项如下。
props 属性 | 属性描述 | 属性类型 |
|---|---|---|
| visible | 当前 modal 是否显示 | boolean |
| onOk | 当点击确定按钮触发 | function |
| onCancel | 当点击取消按钮触发 | function |
| closeCb | 当弹窗完全关闭后触发 | function |
| width | 弹窗宽度 | number |
| okText | 确定按钮文案 | string |
| cancelText | 取消按钮文案 | string |
| title | Modal标题 | string |
| footer | 自定义底部内容 | React Element |
| children | Modal 内容(插槽模式) | React Element |
| content | Modal 内容( props 属性模式) | React Element |
需要把弹窗组件渲染到挂载的容器之外,这样不受到父组件的影响。这里可以通过 ReactDOM.createPortal API解决这个问题。
Portal 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的优秀的方案。createPortal 可以把当前组件或 element 元素的子节点,渲染到组件之外的其他地方。
createPortal 接受两个参数:
ReactDOM.createPortal(child, container)挂载组件
一个 React 应用,可以有多个 root Fiber,所以可以通过 createRoot 来实现组件的自由挂载。
卸载组件
上面既然完成了挂载组件,下面需要在隐藏 Modal 的时候去卸载组件。可以通过 root.unmount() 来实现这个功能。
root.unmount() 会从 DOM 中卸载组件,将其事件处理器和 state 一并清除。如果容器上没有对应已挂载的组件,则什么也不会做。
import Dialog from './dialog'
class Modal extends React.PureComponent{
/* 渲染底部按钮 */
renderFooter=()=>{
const { onOk , onCancel , cancelText , okText, footer } = this.props
/* 触发 onOk / onCancel 回调 */
if(footer && React.isValidElement(footer)) return footer
return <div className="model_bottom" >
<div className="model_btn_box" >
<button className="searchbtn" onClick={(e)=>{ onOk && onOk(e) }} >{okText || '确定'}</button>
<button className="concellbtn" onClick={(e)=>{ onCancel && onCancel(e) }} >{cancelText || '取消'}</button>
</div>
</div>
}
/* 渲染顶部 */
renderTop=()=>{
const { title , onClose } = this.props
return <div className="model_top" >
<p>{title}</p>
<span className="model_top_close" onClick={()=> onClose && onClose()} >x</span>
</div>
}
/* 渲染弹窗内容 */
renderContent=()=>{
const { content , children } = this.props
return React.isValidElement(content) ? content
: children ? children : null
}
render(){
const { visible, width = 500 ,closeCb , onClose } = this.props
return <Dialog
closeCb={closeCb}
onClose={onClose}
visible={visible}
width={width}
>
{this.renderTop()}
{this.renderContent()}
{this.renderFooter()}
</Dialog>
}
}设计思路:
import React , { useMemo , useEffect ,useState } from 'react'
import ReactDOM from 'react-dom'
import { createRoot } from 'react-dom/client'
/* 控制弹窗隐藏以及动画效果 */
const controlShow = (f1,f2,value,timer)=> {
f1(value)
return setTimeout(()=>{
f2(value)
},timer)
}
export default function Dialog(props){
const { width , visible , closeCb , onClose } = props
/* 控制 modelShow 动画效果 */
const [ modelShow , setModelShow ] = useState(visible)
const [ modelShowAync , setModelShowAync ] = useState(visible)
const renderChildren = useMemo(()=>{
/* 把元素渲染到组件之外的 document.body 上 */
return ReactDOM.createPortal(
<div style={{ display:modelShow ? 'block' : 'none' }} >
<div className="model_container" style={{ opacity:modelShowAync ? 1 : 0 }} >
<div className="model_wrap" >
<div style={{ width:width + 'px'}} > {props.children} </div>
</div>
</div>
<div className="model_container mast" onClick={()=> onClose && onClose()} style={{ opacity:modelShowAync ? 0.6 : 0 }} />
</div>,
document.body
)
},[ modelShowAync, modelShow ])
useEffect(()=>{
let timer
if(visible){
/* 打开弹窗,需要先让元素显示,再触发动画 */
timer = controlShow(setModelShow,setModelShowAync,visible,30)
}else{
timer = controlShow(setModelShowAync,setModelShow,visible,1000)
}
return function (){
timer && clearTimeout(timer)
}
},[ visible ])
/* 执行关闭弹窗后的回调函数 closeCb */
useEffect(()=>{
!modelShow && typeof closeCb === 'function' && closeCb()
},[ modelShow ])
return renderChildren
}设计思路:
需要把元素渲染到组件之外,用 createPortal 把元素直接渲染到 document.body 下,为了防止函数组件每一次执行都触发 createPortal, 所以通过 useMemo 做性能优化。
因为需要渐变的动画效果,所以需要两个变量 modelShow / modelShowAync 来控制显示/隐藏,modelShow 让元素显示/隐藏,modelShowAync 控制动画执行。
对于通过组件的静态方法来实现弹窗的显示与隐藏,流程在上述基础上,要更复杂有一些。
let ModalContainer = null
const modelSysbol = Symbol('$$__model__Container_hidden')
/* 静态属性show——控制 */
Modal.show = function(config){
/* 如果modal已经存在了,那么就不需要第二次show */
if(ModalContainer) return
const props = { ...config , visible: true }
const container = ModalContainer = document.createElement('div')
/* 创建一个管理者,管理 modal 状态 */
const manager = container[modelSysbol] = {
setShow:null,
mounted:false,
hidden(){
const { setShow } = manager
setShow && setShow(false)
},
destory(){
/* 卸载组件 */
root.unmount()
/* 移除节点 */
document.body.removeChild(container)
/* 置空元素 */
ModalContainer = null
}
}
const ModelApp = (props) => {
const [ show , setShow ] = useState(false)
manager.setShow = setShow
const { visible,...trueProps } = props
useEffect(()=>{
/* 加载完成,设置状态 */
manager.mounted = true
setShow(visible)
},[])
return <Modal {...trueProps} closeCb={() => manager.mounted && manager.destory()} visible={show} />
}
/* 插入到body中 */
document.body.appendChild(container)
/* 渲染React元素 */
const root = createRoot(container)
root.render(<ModelApp {...props} />)
return manager
}
/* 静态属性——hidden控制隐藏 */
Modal.hidden = function(){
if(!ModalContainer) return
/* 如果存在 ModalContainer 那么隐藏 ModalContainer */
ModalContainer[modelSysbol] && ModalContainer[modelSysbol].hidden()
}
export default Modal接下来,描述一下流程和细节:
第一点:因为要通过调用 Modal 的静态属性来实现组件的显示与隐藏。所以用 Modal.show 来控制显示,Modal.hidden来控制隐藏。但是两者要建立起关联,所以通过全局ModalContainer属性,能够隐藏掉Modal.show 产生的元素与组件。
第二点:如果调用 Modal.show,首先会创建一个元素容器 container ,用来挂载 Modal 组件,通过 createRoot 挂载,这里需要把 container 插入到 document.body 上。
第三点:因为 Modal 组件要动态混入 visible 属性,并且做一些初始化的工作,比如提供隐藏弹窗的方法,所以创建一个 ModelApp 容器组件包裹 Modal。
第四点:因为要在弹窗消失的动画执行后,再统一卸载组件和元素,所以到了本模块难点,就是创建一个 modal manager 管理者,通过 Symbol('$$__model__Container_hidden') 把管理者和容器之间建立起关联。容器下有 hidden 只是隐藏组件,并没有销毁组件,当组件隐藏动画执行完毕,会执行 closeCb 回调函数,在回调函数中再统一卸载元素和组件。
第五点:调用Modal.hidden 本质上调用的是 manager 上的 hidden 方法 ,然后执行动画,执行隐藏元素。然后再触发 destory ,用 root.unmount() 和 removeChild 做一些收尾工作。完成整个流程。
/* 挂载方式调用modal */
export default function Index() {
const [ visible , setVisible ] = useState(false)
const [ nameShow , setNameShow ] = useState(false)
const handleClick = () => {
console.log('点击')
setVisible(!visible)
setNameShow(!nameShow)
}
/* 防止 Model 的 PureComponent 失去作用 */
const [ handleClose ,handleOk, handleCancel ] = useMemo(()=>{
const Ok = () => console.log('点击确定按钮')
const Close = () => setVisible(false)
const Cancel = () => console.log('点击取消按钮')
return [ Close , Ok , Cancel ]
},[])
return <div>
<Modal
onCancel={handleCancel}
onClose={handleClose}
onOk={handleOk}
title={'《React进阶实践指南》'}
visible={visible}
width={700}
>
<div className="feel" >
小册阅读感受: <input placeholder="写下你的感受" />
{nameShow && <p>作者: 我不是外星人</p>}
</div>
</Modal>
<button onClick={() => {
setVisible(!visible)
setNameShow(false)
}}
> model show </button>
<button onClick={handleClick} > model show ( 显示作者 ) </button>
</div>
}export default function Index(){
const handleClick =() => {
Modal.show({
content:<p>确定购买《React进阶指南小册》吗</p>,
title:'《React进阶实践指南》',
onOk:()=>console.log('点击确定'),
onCancel:()=>console.log('点击取消'),
onClose:()=> Modal.hidden()
})
}
return <div>
<button onClick={() => handleClick()} >静态方式调用,显示modal</button>
</div>
}本章节的知识点总结:
createRoot + root.unmount() 实现自由挂载/卸载组件。