React弹出层组件设计思路

目录 编程

在使用原生JavaScript的网页开发中,一般采用直接插入DOM节点渲染的方式弹出弹出层与用户互动。

React采用Virtual DOM + 组件化开发的方式。如果开发者直接像原生那样操作DOM,则违背了React框架的设计初衷,带来大量的因浏览器重绘而造成的性能损失。虽然React改变了整个前端的开发方式,将前端带上工程化道路,但再强大的东西也有它的弱点,React大跨度组件间的通讯非常麻烦,需要依赖Redux库来进行简化。但简化后的通讯也不是组件间可以随意的通讯的,它们只能按约定好的话术进行交流。

例如这个对话框,半透明背景与对话框最少应该为2个组件。2个组件之间互相配合,当背景被点击时通知对话框关闭,当对话框按钮被点击时通知背景关闭。如果整个页面就这么一个对话框,那配合Redux甚至Props固定之间的通讯,非常容易能实现。

render() { 
   return ( 
       <div> 
         <Mask></Mask>
         <Pop></Pop>
       </div> 
   )
}

但现实中往往一个页面不止一个对话框组件,不可能每个需要对话框的组件中都放<Mask></Mask><Pop></Pop>。就算真这么写了,z-index的问题如何处理?网页也是存在z轴的,子元素z轴上限就是父元素的z轴。用position: fixed脱离文档流?那遮罩的width和height只能用javascript算。

难道就没有解决办法了?有没有高复用,最少操作DOM树的且结构清晰通讯简单的方法来实现React弹出层?像原生那样执行 PopTips.show() 对话框就duang一下弹出来。

当然有,不过我们得改变下React组件化开发的传统思路方式。使用构造函数去映射着操作React组件,组件间的通讯在构造函数中处理。

同时为了最大可能的减少手动操作DOM树,我们需要用到React提供给我们的 ReactDOM.render() 方法,来渲染组件。

引用下官方介绍:

在提供的 container 里渲染一个 React 元素,并返回对该组件的应用(或者针对无状态组件返回 null)。

说白了就是将组件用官方提供的函数渲染进DOM节点中。不过官方表示React17版本以后该函数就会被移除,转而使用ReactDOM.hydrate()方法。为了和页面渲染不冲突,我们需要为弹出层单独创建一个DIV容器。用来接纳所有渲染的组件。

#index.html

<html> 
..... 
<body> 
    <div id="root"></div>
    <div id="PublicPop"> 
       <!--弹出层容器--> 
    </div> 
</body>
.... 
</html>

弹出层组件为了高复用性,不会和其它组件产生任何依赖。直接创建单独目录即可。

Components
│  index.js
│  
├─Mask
│      index.js
│      index.module.css
│      
└─PopTips
        index.js
        index.module.css
#Components/index.js

import React from 'react';
import ReactDOM from 'react-dom';

/**创建构造函数容器 */
let Pop = {};
/**获取<div id="PublicPop"></div>容器 */
let PublicPop = document.getElementById('PublicPop');

/**暴露构造函数容器 */
export default Pop;

Components/index.js 是弹出层调用入口,在这里引入弹出层组件并写调用各组件的构造函数。我们先写<Mask>组件的构造函数,在写之前,我们先完成<Mask>组件js部分。主要是一些事件的回调。

#Components/Mask/index.js

import React, { Component } from 'react'
import Style from './index.module.css'

export default class index extends Component {

    constructor(props){
        super();
        /**遮罩层的Z轴 */
        this.zIndex = props.zIndex;
        /**遮罩层的背景颜色 */
        this.background = props.background;
        /**回调函数,用来监听处理遮罩层的各种事件 */
        this.callback = props.callback;
        /**遮罩层透明度 */
        this.opacity = props.opacity;
        /**将组件对象回调给调用函数,使调用函数可以直接操控该组件 */
        this.componentObject = props.componentObject;
        if(typeof this.componentObject === "function"){this.componentObject(this)}
    }



    /**遮罩点击事件 */
    MaskClick = () => {
        if(typeof this.callback === "function"){
            this.callback({
                eventName: "mouseClick"
            });
        }
    }

    /**鼠标移入事件 */
    MaskMouseOver = () => {
        if(typeof this.callback === "function"){
            this.callback({
                eventName: "mouseOver"
            });
        }
    }

    /**鼠标移出事件 */
    MaskMouseOut = () => {
        if(typeof this.callback === "function"){
            this.callback({
                eventName: "mouseOut"
            });
        }
    }

    render() {
        return (
            <div className={Style.mask} 
                onClick={this.MaskClick} 
                onMouseOver={this.MaskMouseOver} 
                onMouseOut={this.MaskMouseOut} 
                style={{
                    opacity: this.opacity,
                    background: this.background,
                    zIndex: this.zIndex
            }}></div>
        )
    }
}

css部分如下

#Components/Mask/index.module.css

.mask{
    width:100%;
    height:100%;
    position: fixed;
    left:0px;
    top:0px;
    transition: opacity 0.3s;
}

到此,遮罩组件已经完毕,回到Components/index.js上开始写调用方法。

首先引入遮罩组件,并构造函数Pop.Mask。我们要在每次调用的时候在PublicPop容器下新建一个ID是随机的容器,用来放渲染进去的遮罩组件。不同的构造函数创建不同的ID容器,当卸载组件时自动删掉对应ID的容器。

#Components/index.js

import React from 'react';
import ReactDOM from 'react-dom';
import Mask from './Mask';

/**创建构造函数容器 */
let Pop = {};
/**获取<div id="PublicPop"></div>容器 */
let PublicPop = document.getElementById('PublicPop');

/**
 * 弹出遮罩层
 * @param {string} background    //背景色
 * @param {number} zIndex        //z-index
 * @param {number} opacity       //透明度
 * @param {function} callback    //回调函数
 */
Pop.Mask = function(zIndex, background, opacity, callback){
    /**私有化 */
    this.background = background;
    this.zIndex = zIndex;
    this.opacity = opacity;
    this.callback = callback;
    /**this.getComponentObject函数传递给Mask组件后,会在Mask组件内部运行并回调会Mask组件的this对象,这个对象就存放在this.componentObject函数中,依靠这个对象,Pop.Mask函数就可以直接调用Mask组件的内部方法*/
    this.componentObject = null;

    /**初始化 */
    this.Initialization = () => {
        /**创建一个DIV容器 */
        this.Container = document.createElement("div");
        /**生成一个随机ID容器 */
        this.Container.setAttribute('id',Math.random().toString(36).slice(-8));
        PublicPop.appendChild(this.Container);
    }

    /**加载<Mask>组件 */
    this.LoadTheComponent = () => {
        /**使用React原生函数加载组件 */
        ReactDOM.render(
            <React.StrictMode>
                <Mask 
                    background={this.background} 
                    zIndex={this.zIndex} 
                    opacity={this.opacity}
                    callback={this.callback} 
                    componentObject={this.getComponentObject}
                ></Mask>
            </React.StrictMode>,
            this.Container
        );
    }

    /**获取组件对象 */
    this.getComponentObject = (e) => {
        this.componentObject = e;
    }

    /**卸载组件 */
    this.shutDown = () => {
        this.componentObject.endTheAnimation(() => {
             ReactDOM.unmountComponentAtNode(this.Container);
             PublicPop.removeChild(this.Container);
        });
    }

    this.Initialization();
    this.LoadTheComponent();
}

/**暴露构造函数容器 */
export default Pop;

接下去在任何组件中引入Components/index.js

import React, { Component } from 'react'
import Pop from '../../../../CommonComponents'
import Style from './index.module.css'

export default class index extends Component {

    componentDidMount(){
        /**new 一个构造函数,传入Mask需要的参数*/
        let Mask = new Pop.Mask("#000",18,0.2,(e) => {
              /**当鼠标在Mask上做出动作时,Mask组件会将动作回调回调用端*/
              console.log(e)
              /**检测到符合条件的动作时,直接关闭卸载Mask组件 */
              if(e.eventName === "mouseClick"){
                 Mask.shutDown();
                 console.log("Mask组件已被卸载");
              }
        });
    }

    render() {
        return (
            <div></div>
        )
    }
}

当鼠标移入移出或者点击遮罩组件时都会触发回调,比如点击遮罩组件时关闭对话框,关闭对话框时触发回调关闭遮罩组件。所有的通讯都在构造函数内部完成。想使用组件直接new一个即可。

完成遮罩组件的构造函数后,我们继续写弹窗组件的构造函数。弹窗组件和遮罩组件一样,都是对事件进行回调。回调后由构造函数来对组件进行控制变化。

#Components/PopTips/index.js

import React, { Component } from 'react'
import Style from './index.module.css'

export default class index extends Component {

    constructor(props){
        super();
        /**对话框标题 */
        this.title = props.title;
        /**对话框内容 */
        this.content = props.content;
        /**对话框标志 */
        this.ico = props.ico;
        /**对话框宽度 */
        this.width = props.width;
        /**对话框高度 */
        this.height = props.height;
        /**确定按钮后的回调 */
        this.callback = props.callback;
        /**将组件对象回调给调用函数,使调用函数可以直接操控该组件 */
        this.componentObject = props.componentObject;
        if(typeof this.componentObject === "function"){this.componentObject(this)}
    }

    /**确定按钮点击,触发回调 */
    BtnOk = () => {
        if(typeof this.callback === "function"){
            this.callback();
        }
    }

    render() {
        return (
            <div className={Style.pop} style={{
                opacity: this.opacity,
                transform: "scale(" + this.transformScale + ")",
                width: this.width + "px",
                height: this.height + "px",
                marginLeft: -(this.width/2) + "px",
                marginTop: -(this.height/2) + "px"
            }}>
                <div className={Style.title} style={{
                    width: (this.width - 20) + "px"
                }}>
                    <i className={this.ico}></i>
                    <p>{this.title}</p>
                </div>
                <div className={Style.content} style={{
                    width:(this.width - 20) + "px",
                    height: (this.height - 80) + "px"
                }}>{this.content}</div>
                <div className={Style.btn} style={{
                    width: (this.width - 20) + "px"
                }}>
                    <button className={Style.ok} onClick={this.callback}>知道啦</button>
                </div>
            </div>
        )
    }
}
#Components/PopTips/index.module.css

.pop{
    background: #fff;
    position: fixed;
    top:50%;
    left:50%;
    z-index: 20;
    border-radius:4px;
    transition: transform 0.3s,opacity 0.6s;
}

.title{
    margin-left:auto;
    margin-right:auto;
    height:30px;
    display: flex;
    justify-content: flex-start;
    border-bottom:1px solid #f0f0f0;
}
.title > i{
    width:auto;
    height:30px;
    text-align: center;
    line-height: 30px;
    margin-right:5px;
}
.title > p{
    width:auto;
    height:30px;
    line-height: 30px;
    font-size:14px;
}
.content{
    margin-left:auto;
    margin-right:auto;
    font-size:14px;
    text-indent: 2em;
    margin-top:10px;
}
.btn{
    margin-left:auto;
    margin-right:auto;
    display: flex;
    justify-content: flex-end;
}
.btn > button{
    width:5em;
    height:30px;
    background: #1890ff;
    border:none;
    color:#fff;
    cursor: pointer;
    border-radius:2px;
    outline:none;
}

PopTips的构造函数调用时不光要使用弹窗组件,还需要在里面直接调用其遮罩组件。同时当遮罩组件被点击时关闭遮罩组件和弹窗组件,当弹窗组件被按下按钮时也关闭遮罩组件和弹窗组件。

/**
 * 单按钮对话框
 * @param {string} title         对话框标题
 * @param {string/jsx} content   对话框内容
 * @param {string} ico           对话框标志
 * @param {number} width         对话框宽度
 * @param {number} height        对话框高度
 * @param {function} callback    对话框按钮回调事件
 */
Pop.PopTips = function(title,content,ico,width,height,callback){
    /**私有化 */
    this.title = title;
    this.content = content;
    this.ico = ico;
    this.width = width;
    this.height = height;
    this.callback = callback;
    /**this.getComponentObject函数传递给弹窗组件后,会在弹窗组件内部运行并回调会弹窗组件的this对象,这个对象就存放在this.componentObject函数中,依靠这个对象,Pop.PopTips函数就可以直接调用弹窗组件的内部方法*/
    this.componentObject = null;

    /**初始化 */
    this.Initialization = () => {
        /**创建一个DIV容器 */
        this.Container = document.createElement("div");
        this.Container.setAttribute('id',Math.random().toString(36).slice(-8));
        PublicPop.appendChild(this.Container);
        /**调用遮罩组件的构造函数 */
        this.Mask = new Pop.Mask("#000",18,0.2,(e) => {
            /**当遮罩被点击时关闭遮罩组件,同时也关闭弹窗 */
            if(e.eventName === "mouseClick"){
                this.Mask.shutDown();
                this.shutDown();
            }
        });
    }

    /**载入组件 */
    this.LoadTheComponent = () => {
        /**render入 */
        ReactDOM.render(
            <React.StrictMode>
                <PopTips 
                    title={this.title} 
                    content={this.content} 
                    ico={this.ico} 
                    width={this.width} 
                    height={this.height}
                    callback={this.callback} 
                    componentObject={this.getComponentObject} 
                ></PopTips>
            </React.StrictMode>,
            this.Container
        );
    }

    /**获取组件对象 */
    this.getComponentObject = (e) => {
        this.componentObject = e;
    }

    /**卸载组件 */
    this.shutDown = () => {
         /**同时关闭遮罩组件 */
         this.Mask.shutDown();
         ReactDOM.unmountComponentAtNode(this.Container);
         PublicPop.removeChild(this.Container);
    }

    this.Initialization();
    this.LoadTheComponent();
}

DIY一些动画后,在任何组件处引入Components/index.js

import Pop from '../../../../CommonComponents'

/**调用弹窗组件 */
let PopTips = new Pop.PopTips("~~","请早点休息。","ri-sun-line",300,100,() => {
    /**按钮按下后回调卸载弹窗和遮罩组件 */
    PopTips.shutDown();
});

开启效果

关闭效果