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(); });
开启效果
关闭效果