5

前言

Modal (模态框) 是 web 开发中十分常见的组件,即从页面中弹出的对话框。
今天我们一起来用 React Hook 手写 Modal 模态框组件.
最终实现的效果codePen如下:
modal.png

环境准备

本文代码在 create-react-app 脚手架生成的项目中运行,react 版本:

"react": "^16.12.0",
"react-dom": "^16.12.0",
"react-scripts": "3.3.0"

相应的直接引入 js 的方法,在 codePen 中可看得到全部代码。

从使用组件开始

使用 Modal 组件,最重要就是控制它的打开和关闭。我们先定义一个 modalVisible 的 state,当 modalVisible 为 true 时,模态框打开,反之关闭。
接下来就可以把 modalVisible 传入 Modal 组件来控制模态框的打开和关闭,同时 Modal 组件需要接收一个 onClose 的 prop,用来实现在组件中关闭模态框(例如点击蒙层时关闭模态框)。

const [modalVisible, setModalVisible] = useState(false);
const modalConfig = {
    visible: modalVisible,
    closeDialog: () => {
        setModalVisible(false);
    }
};
<Modal {...modalConfig}></Modal>

Modal 组件应具备良好的扩展性,做到可自定义模态框的内容(例如模态框的标题、关闭按钮、确定按钮等等)。我们把这部分自定义内容通通传入组件中。举个例子:

const modalChildren = (
    <div className="dialog">
        <span onClick={() => setModalVisible(false)} className="closeBtn">x</span>
        <div>这是内容</div>
    </div>
);

<Modal {...modalConfig}>{modalChildren}</Modal>

这部分自定义内容含有一个内容框,关闭按钮和文字内容,可以给它们添加一下样式:

/* App.css */
.openBtn {
    margin-top: 240px;
    border: 1px solid dodgerblue;
}

.dialog {
    position: absolute;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
    padding: 30px 30px;
    width: 200px;
    height: 200px;
    background-color: #fff;
    border-radius: 8px;
}

.closeBtn {
    position: absolute;
    right: 10px;
    top: 4px;
    font-size: 21px;
}

完整的使用 Modal 组件的代码:

import React, { useState } from 'react';
import './App.css';
import Modal from './components/modal';

function App() {
    const [modalVisible, setModalVisible] = useState(false);
    const modalConfig = {
        visible: modalVisible,
        closeModal: () => {
            setModalVisible(false);
        }
    };
    
    const modalChildren = (
        <div className="dialog">
            <span onClick={() => setModalVisible(false)} className="closeBtn">x</span>
            <div>这是内容</div>
        </div>
    );
    
    return (
        <div className="App">
            <button onClick={() => setModalVisible(true)} className="openBtn">open modal</button>
            <Modal {...modalConfig}>{modalChildren}</Modal>
        </div>
    );
}

export default App;

编写 Modal 组件

清楚 Modal 组件需要接收的 props 之后,我们就可以开始编写组件了。

首先,我们需要给 modal 组件增加一个蒙层:

/* modal.css */
.modal {
    position: fixed;
    top: 0;
    left: 0;
    bottom: 0;
    right: 0;
    background: rgba(0, 0, 0, 0.3);
    z-index: 99;
}

开始编写组件:

import React from 'react';
import './css/modal.css';

const Modal = (props) => {
    const { children, visible, closeModal } = props;

    function handleClick(event) {
        // 点击蒙层本身时关闭模态框,点击模态框的内容时不关闭
        if (event.target === event.currentTarget) {
            closeModal();
        }
    }

    const modal = (
        <div className="modal" onClick={handleClick}>
            {children}
        </div>
    );

    return <div>{visible && modal}</div>;
};

export default React.memo(Modal);

上面我们实现了通过visible来控制打开和关闭模态框,以及点击蒙层时关闭模态框。

接下来,我们要把模态框组件挂载在 body 的第一层中,而不是将模态框放置到父组件中。
因为模态框放置到父组件中很容易受到其他元素的干扰,仅是设置各个元素的 z-index 就使得代码难以维护。

我们可以使用 React 的 Portal 来实现把模态框组件应该挂载在 body 上:

import { createPortal } from 'react-dom';
createPortal(/* 组件内容 */, document.body)

modal2.png

Modal 组件完整代码:

import React from 'react';
import './css/modal.css';
import { createPortal } from 'react-dom';`

const Modal = (props) => {
    const { children, visible, closeModal } = props;

    function handleClick(event) {
        // 点击蒙层本身时关闭模态框,点击模态框的内容时不关闭
        if (event.target === event.currentTarget) {
            closeModal();
        }
    }

    const modal = createPortal(
        <div className="modal" onClick={handleClick}>
            {children}
        </div>,
        document.body
    );

    return <div>{visible && modal}</div>;
};

export default React.memo(Modal);

这样我们就完整地开发出一个简单的易扩展的 Modal 组件了,其实它就是一个蒙层,内容都是通过父组件传进来。

阻止背景滚动

当我们完成上面的编码之后,我们的模态框就可以实现显示/隐藏,并且处于 body 的顶层,但是还有一个问题,那就是如果 body 内容太长出现滚动时,滚动鼠标就会发现,模态框后边的背景也在滚动,这显然不是我们期望的效果。如何应对这种情况呢?

解决办法很巧妙,就是在模态框打开时,我们给 body 添加一个 overflow: hidden 的样式让 body 不滚动,关闭模态框时再去除这个样式。通过这样的方式,我们就实现在模态框打开时背景不滚动的效果了。

明白了原理之后就开始修改代码了,我们首先 在modal 组件中添加一个 bodyOverflow 的 useRef,用来保存 body 原始的 overflow 值,然后通过组件传入的 visible 来动态修改 body 的 overflow 值。

import React, { useEffect, useRef } from 'react';
import './css/modal.css';
import { createPortal } from 'react-dom';

const Modal = (props) => {
    const { children, visible, closeModal } = props;

    // 在第一次渲染时取 body 原始的 overflow 值
    const bodyOverflow = useRef(window.getComputedStyle(document.body).overflow);

    useEffect(() => { // 根据 visible 来动态修改 body 的 overflow 值
        if (visible) {
            document.body.style.overflow = 'hidden';
        } else {
            document.body.style.overflow = bodyOverflow.current;
        }
    }, [visible]);

    function handleClick(event) {
        // 点击蒙层本身时关闭模态框,点击模态框的内容时不关闭
        if (event.target === event.currentTarget) {
            closeModal();
        }
    }
    
    useEffect(() => {
        // 组件销毁时恢复 body 的 overflow 值
        return () => {
            document.body.style.overflow = bodyOverflow.current;
        }
    }, []);

    const modal = createPortal(
        <div className="modal" onClick={handleClick}>
            {children}
        </div>,
        document.body
    );

    return <div>{visible && modal}</div>;
};

export default React.memo(Modal);

打开模态框时效果如下:
modal3.png

扩展思考

有了这样一个灵活的 Modal 组件,我们可以用来做些什么呢?

在实际的开发中,经常需要用到的是一些公共的对话框组件,例如带标题、确定按钮、取消按钮和关闭按钮的弹框,又例如用于点击删除时需要弹出来的温馨提示弹框。

我们完全可以在 Modal 组件的基础上,封装这些对话框组件。如果让你实现以下的对话框组件,你会怎么做呢?赶紧来试试吧:
dialog 对话框组件

参考文章


一丁目
38 声望1 粉丝

live in the moment