react如何优雅的处理多弹窗需求(模态框注入)

raunyuyuan

react 优雅实现多弹窗的需求

提出问题

现在有这样一个需求:存在 A,B,C,D 四个弹窗,并且要在1,2,3,4, 5 等页面分别打开其中的几个,并在关闭时调用函数,该怎么去做呢?
例子:

    <ModalA />
    <ModalB />
    <ModalC />
    <ModalD />
    这些模态框需要在
    <Component1 />
    <Component2 />
    <Component3 />
    <Component4 />
    <Component5 />
    分别打开其中的几个并在关闭时触发不同的回调函数。

常规解法

一般ui库中,如ant-design, material-ui提供的Modal组件中都有一个控制开关的visible 或者open属性。

我们处理上面需求的时候。如果Component1 用到了ModalA, ModalB, Modal,这时候会在Component1中新建3个state,分别对应三个弹窗的开关状态

import ModalA from 'ModalA'
import ModalB from 'ModalB'
import modalC from 'ModalC'

export default function Component1() {
    const [avisible, setavisible] = useState(false)
    const [bvisible, setbvisible] = useState(false)
    const [cvisible, setcvisible] = useState(false)
    const handleCloseA = () => {
        setavisible(false)
    }
    
    const handleCloseB() => {
        setavisible(false)
    }
    
    const handleCloseC= () => {
        setavisible(false)
    }
    
    return (
        <ModalA
            visible={avisible}
            onModalAMissionDone={callback1}
            onClose={handleCloseA}
            // ... remain props
        />
        <ModalB
            visible={bvisible}
            onModalBMissionDone={callback1}
            onClose={handleCloseB}
            // ... remain props
        />
        <ModalC
            visible={bvisible}
            onModalCMissionDone={callback1}
            onClose={handleCloseC}
            // ... remain props
        />
        
    )
}

首先,观察以上代码,我们为了在Component1中复用三个弹窗,多了三个visible属性, 并且因为Modal是独立的组件,我们还要多三个打开的函数,三个关闭的函数。这样我们用到一个弹窗,就需要多一个state,多两个维护state的函数,对当前Component1组件可以说是很不友好,且繁琐,

如果继续在Component2, Component3 ...中继续这样复用弹窗 无疑是一场小灾难。

解决方案1

构造一个open方法,一个新的div append到body内最下方可以写出modal.js这个文件:

// modal.js
import ReactDom from 'react-dom'

export function open (ModalCom, props) {
    const $div = document.createElement('DIV');
    document.body.appendChild($div)
    function close () {
        ReactDom.unmountComponentAtNode($div)
        document.removeChild($div)
    }
    // 将弹窗组件通过
    ReactDom.render(
        <ModalCom
            visible={true}
            onClose={close}
            {...props}
        />,
        $div)
        
    return close
    
}

这样我们就可以在Component1这样去使用ModalA,B,C:

    import {open} from 'modal.js'
    import ModalA from 'ModalA'
    // ...
    export function Component1() {
        const handleOpenModalA = () => {
            const close = open(ModalA, { 
                onModalAMissionDone: () => message.success('success')
            })
        }
        const handleOpenModalB = () => {
            const close = open(ModalB, { 
                onModalBMissionDone: () => message.success('success')
            })
        }
        const handleOpenModalC = () => {
            const close = open(ModalC, { 
                onModalCMissionDone: () => message.success('success')
            })
        }
        return (
            <>
                <button onClick={handleOpenModalA}>openA</button>
                <button onClick={handleOpenModalB}>openB</button>
                <button onClick={handleOpenModalC}>openC</button>
            </>
        )
    }

乍一看,解决了维护三个state的问题,只需要三个open函数,传参,但却造成了其他的问题:

  1. return的close 方法为局部变量,共享会使用全局变量 或者 useRef的方式,略微增加了复杂度。
  2. open方法调用多次,会打开多个相同的弹窗
  3. 每次close的时候,弹窗组件彻底销毁,性能开销过大

对于问题1,2 我们可以利用WeakMap等解决, 我们不妨相像我们的函数会返回一个背包一样的东西,open的时候打开收集弹窗到背包里面, close的时候从背包中拿出弹窗,关闭并删掉。并在一个文件中初始化背包,背包中的弹窗不能重复:

    // modal.js
    import ReactDom from 'react-dom'
    export default generateModalBag() {
        const mapClose = new WeakMap();
        
        return {
            open: (ModalCom, props) {
                // 防止重复
                if (mapClose.has(ModalCom)) return;
                const $div = document.createElement('DIV');
                document.body.appendChild($div)
                function close () {
                    ReactDom.unmountComponentAtNode($div)
                    document.removeChild($div)
                    mapClose.delete($div)
                }
                ReactDom.render(
                <ModalCom
                    visible={true}
                    onClose={close}
                    {...props}
                />,
                $div)
                mapClose.set(ModalCom, close)
                return close
            },
            close: (ModalCom){
                const fun = mapClose.get(ModalCom)
                if (typeof fun === 'function') fun()
            }
        }
    }
    

于是在Component1中我们就可以这样使用

    import generateModalBag from 'modal.js'
    import ModalA from 'ModalA'
    const modal = generateModalBag()
    
    export function Component1() {
        const handleOpenModalA = () => {
            modal.open(ModalA, { 
                onModalAMissionDone: () => message.success('success')
            })
        }
        
        const handleCloseModalA = () => {
            modal.close(ModalA)
        }
        return (
            <>
                <button onClick={handleOpenModalA}>openA</button>
            </>
        )
    }

这样我们就解决了问题1,2 。并且不会重复打开moalA弹窗, 却无法解决问题3,有没有更好的解决问题3的方案呢?那我们必须推倒这个方案重新设计

方案2: 利用高阶组件实现模态框注入

我们可以将visible属性放入高阶组件去进行维护,因为弹窗是多处公用的,可以看作是1,2,3等组件的依赖,我们可以采用注入的方式。根据modal动态创建state,并暴露modal构造函数映射 close,open方法的map给需要使用modal的组件:

    export default function injectModal(...modals) {
        return (WrapCom) => {
            function ModalContainer() {
                const [visibles, setVisibles] = useState(new Array(modals.length)).fill(false));
                const [modalsProps, setmodalsProps] = useState(new Array(modals.length)).fill({}))
                return (
                    <>
                        <WrapCom
                            {...this.props}
                            modalBag={xxx}
                        />
                        {modals.map((Modal, idx) => {
                            return (
                                <Modal
                                    key={idx}
                                    onClose={generateCloseFun(idx)}
                                    visible={visibles[idx]}
                                    {...modalsProps[idx]}
                                />
                            )
                        }}
                    </>
                )
            }
            ModalContainer.displayName="InjectModalContainer"
            return ModalContainer
        }
    }

我们可以将moalBag作为props传递给Component1 对弹窗进行管理,这样在Component1中就可以这样使用ModalA, ModalB, ModalC

import injectModal from 'injectModal'

function ComponentA({modalBag}) {
    const handleOpenA = () => {
        const customProps = {} // 自定义props
        modals.find(ModalA).open(customProps)
    }
    const closeModalA = () => {
        const customProps = {} // 自定义props
        modals.find(ModalA).close()
    }
    return (
       <ButtonA onClick={handleOpenA}/>
    )
}
export default injectModal(ModalA, ModalB, ModalC)(ComponentA)

这样就解决了上面提到的1,2,3三个问题,open,close方法在高阶组件中调用动态创建的对应的vsisible state的改变,不需要自己去维护开关state,不需要创建多余的dom,可以实现关闭时不销毁弹窗,可以适应第三方库,还可以自己构造useOpen useClose Hooks或者componentDidOpen componentDidClose 生命周期 处理弹窗中的一系列行为。稍微改造一下我们甚至可以注入其他组件,当然这里唯一需要注意的就是处理ref的行为

感谢。

阅读 5.1k
10 声望
1 粉丝
0 条评论
10 声望
1 粉丝
文章目录
宣传栏