详解使用React制作一个模态框
模态框是一个常见的组件,下面让我们使用React实现一个现代化的模态框吧。
组件设计
模态框想必大家都很熟悉,是工作中常用的组件,可以让我们填写或展示一些信息而不必打开一个新页面。在开始编码之前,我们先来了解一个React模态框组件应该如何设计。
React是一个状态(数据)驱动的前端框架,一个模态框最重要的状态就是打开和关闭,visible,当visible为true时,模态框打开,反之亦然。
由于React所提倡的是一种声明式,组件化的开发体验,每个组件都是状态=>界面的映射,所以,我们把visible做为模态框组件的一个prop,通过传入prop来控制
模态框的显示和隐藏,同时该组件还接受一个onClose的prop,用来关闭模态框。
一个完整的模态框还需要标题和内容,因此,我们还需要一个header的prop来传递模态框的header,并把Modal组件的children作为模态框的内容content。最后,我们的模态框Modal的调用方式是这样的:
importReact,{useEffect,useState}from'react';
importModalfrom'./components/modal';
functionApp(){
const[modalVisible,setModalVisible]=useState(true);
constopenModal=function(){setModalVisible(true)};
constcloseModal=function(){setModalVisible(false)};
return(
<>
Click
Thisismycontent
>
);
}
exportdefaultApp;
这里使用了hooks,请升级到最新版本的react来体验。
实际上,一个完整的模态框组件还应该提供一些额外的配置来方便用户使用,比如header和content的自定义样式headerClassName,contentClassName,定制操作按钮的footer,控制是否显示关闭按钮的showClose等等,
但这里为了保持教程的简单,这些简单的配置就不一一实现了,如果感兴趣可以自行练习。
确定了我们的模态框的调用方式,现在我们来总结一下完整的模态框应该具备那些特性:
- 模态框组件应该挂载在body的第一层中,不要将模态框放置到父组件中,因为模态框放置到父组件中很容易受到其他元素的干扰。
- 模态框显示后,模态框背后的背景不能随着鼠标滚轮而滚动。
- 点击模态框的遮罩层后,应该关闭模态框。
基础功能
上面分析玩模态框的功能后,让我们先开始实现一版最基础的模态框。从HTML结构上来讲,模态框组件分为overlay遮罩层和content内容两部分组成,其中content里面还应该分为header,content,footer(这里我们没有实现)三部分组成。
所以,模态框的最基本的结构如下
importReact,{PureComponent}from'react';
classModalextendsPureComponent{
render(){
const{visible,onClose,header,children}=this.props;
return(
{header}
Close
{children}
);
}
}
由于overlay元素是模态框组件的最外层的容器,所以我们可以通过控制overlay的显示和隐藏(在上面的基础结构中,通过visible属性的值来给overlay添加或删除类'visible'来控制)实现模态框的打开关闭效果。在这里我们使用display实现控制overlay的显示和隐藏(这样在关闭时并没有删除该模态框,方便下次打开可以保存内容),同时overlay还是一个占据整个窗口的半透明暗色背景,所以overlay的样式应该为
.overlay{
display:none;
position:fixed;
top:0;
right:0;
bottom:0;
right:0;
background:rgba(0,0,0,0.3);
visibility:hidden;
}
.overlay.visible{
display:block;
visibility:visible;
}
然后就是content中元素的样式,都很简单,大家看一下就好了,可以根据自己的组件规范修改这些样式。
.container{
margin:80pxauto;
width:80%;
min-height:800px;
background:#fff;
border-radius:4px;
}
.header{
display:flex;
justify-content:space-between;
padding:16px;
font-size:24px;
border-bottom:1pxsolid#d3d3d3;
}
.body{
padding:16px;
}
.closeBtn{
outline:none;
border:none;
appearance:none;
font-size:18px;
color:#d5d5d5;
cursor:pointer;
}
这样,我们最基础的一版模态框就做好了,但是这个模态框是渲染在父组件中,那么如何才能将这个模态框放到body下,作为顶层元素呢?我们可以使用Portal这个React新提供的功能。
使用portal将模态框送到body中
Portal是React16中的新功能,就像它的名称传送门一样,这个功能的作用就是将组件的DOM嗖的一下传送到另外一个地方,换句话说就是可以让你的组件渲染到其他地方,而不仅仅是在父组件中。从上面的描述中,我们知道Portal是一个作用于DOM的功能,所以Portal就在react-dom这个包下,react-dom提供了createPortal方法来创建Portal,它的第一参数是React组件,第二个参数则是接收这个组件的DOM节点。
回到我们的模态框来,为了方便的使用Portal,我们首先创建一个ModalPortal组件,该组件会首先使用createElement创建一个表示overlay的div,并使用appendChild将此div插入到body的末尾中,然后在render中,使用createPortal将ModalPortal接受的所有子组件送入overlay这个div中。通过这种方式,我们就把模态框组件变成body中的顶层元素了。
由于overlay是手动创建的DOM元素,所以当visible发生变化时,我们需要使用DOMAPI来控制overlay的显示和隐藏,所以我们在ModalPortal组件的componetDidMount和componetDidUpdate两个生命周期中,根据visible的值来增删overlay的visible类控制overlay的显示/隐藏。
importReact,{PureComponent}from'react';
import{createPortal}from'react-dom'
classModalPortalextendsPureComponent{
constructor(props){
super(props);
//createElement是一个封装后的函数,方便在创建元素时添加属性
this.node=createElement('div',{
class:`modal-${random()}${props.className}`,
});
document.body.appendChild(this.node);
}
componentDidMount(){
this.checkIfVisible();
}
componentDidUpdate(prevProps){
if(prevProps.visible!==this.props.visible){
this.checkIfVisible();
}
}
//控制overlay的显示隐藏
checkIfVisible=()=>{
const{visible}=this.props;
if(visible){
this.node.classList.add(styles.visible);
}else{
this.node.classList.remove(styles.visible);
}
};
render(){
const{children}=this.props;
returncreatePortal(children,this.node);
}
}
classModalextendsPureComponent{
...
render(){
return(
...
)
}
}
阻止背景滚动
当我们完成上面的编码之后,我们的模态框就可以实现显示/隐藏,并且处于body的顶层,但是还有一个问题,那就是如果body内容太长出现滚动时,滚动鼠标就会发现,模态框后边的背景也在滚动,这显然不是我们希望的结果。如何应对这种情况呢?
解决办法很巧妙,就是在模态框打开时,我们给body添加一个overflow:hidden的样式让body不滚动,然后关闭模态框再去除这个属性。通过这样的方式,我们就是实现在模态框打开时背景不滚动的功能了。
明白来原理之后就开始修改代码了,我们首先在constructor中使用一个变量savedBodyOverflow来保持body原始的overflow值,然后修改checkIfVisble使之可以控制overflow类的增删。
classModalPortalextendsPureComponent{
constructor(props){
...
this.savedBodyOverflow=document.body.style.overflow;
}
...
checkIfVisible=()=>{
const{visible}=this.props;
if(visible){
this.node.classList.add(styles.visible);
document.body.style.overflow='hidden';
}else{
this.node.classList.remove(styles.visible);
document.body.style.overflow=this.saveBodyOverflow;
}
}
}
点击遮罩层关闭
点击遮罩层关闭,这个应该很容易实现,给overlay添加一个点击事件监听就好了,但是要注意一点就是,当你点击遮罩层中的content时,不应当关闭。我们先回顾一下DOM2事件模型中的规定的事件流,事件从window开始,执行捕获过程,然后到目标阶段,接着执行冒泡过程,回到window,这个流程就导致我们如果点击了content,overlay同样也会触发点击事件(DOM2默认冒泡阶段触发事件)。针对这种情况,我们可以使用事件中提供的path属性,该属性描述了事件冒泡过程中从目标元素的window的一个路径,所以通过path的第一个参数,我们就可以判断这个click是哪个元素触发的了。
在我们的modal中,如果要实现点击遮罩层关闭,我们可以监听overlay元素的点击事件,然后通过path属性判断事件是否是overlay触发的,是否应该关闭模态框。因为overlay的div使我们自己生产的所以在constructor过程中就可以绑定事件了,注意在componentWillUnMount中要记得清除绑定,为了关闭模态框,别忘记将onClose通过props传递给ModalPortal组件。
classModalPortalextendsPureComponent{
constructor(props){
...
this.node.addEventListener('click',this.handleClick);
}
componentWillUnmount(){
this.node.removeEventListener('click',this.handleClick);
}
handleClick=e=>{
const{closeModal}=this.props;
consttarget=e.path[0];
if(target===this.node){
onClose();
}
};
...
}
按下ESC关闭
上面我们实现了点击遮罩层关闭模态框,然后我们应该实现按下ESC关闭这个功能。通点击事件一样,我们只需要监听keydown事件就可以了,这一次不用考虑到底是哪里触发的问题了,只要overlay监听到keydown就关闭模态框。但是这里也有一个小问题,就是overlay是div,默认是监听不到keydown事件的,对于这个问题,我们可以给div添加一个tabIndex:0的属性,通过指定tabIndex,将div赋予focusable的能力,当模态框打开后,我们手动调用focus将焦点放到overlay上,这样就能监听到键盘事件。
constESC_KEY=27;
classModalPortalextendsPureComponent{
constructor(props){
...
this.node=createElement('div',{
class:`modal-${random()}${props.className}`,
tabIndex:0,
});
this.node.addEventListener('keydown',this.handleKeyDown);
}
componentWillUnmount(){
...
this.node.removeEventListener('keydown',this.handleKeyDown);
}
checkIfVisible=()=>{
const{visible}=this.props;
if(visible){
...
this.node.focus();
}else{
...
}
};
handleKeyDown=e=>{
const{closeModal}=this.props;
if(e.keyCode===ESC_KEY){
closeModal();
}
};
...
}
消除滚动条导致的页面抖动
在上面的防止遮罩层后面背景滚动是通过在body上设置overflow:hidden来防止滚动,但是如果body已经有了滚动条,那么overflow属性会造成滚动条消失。滚动条在chrome上为15px,打开和关闭模态框会使页面不停地对这15px做处理,导则页面抖动。为了防止抖动,我们可以在滚动条消失后给body添加15px的右边距,滚动条出现后在删除右边距,通过这样的方法,页面就不会发生抖动了。
因为各个浏览器的标准不一致,所以我们应该想办法计算出滚动条的宽度。为了计算出滚动条的宽度,我们可以使用innerWidth和offsetWidth这两个属性。offsetWidth是包含边框的长度,理所当然的包含了滚动条的宽度,只需要使用offsetWidth减去innerWidth,得到的差值就是滚动条的宽度了。我们可以手动创建一个隐藏的有宽度的且有滚动条的元素,然后通过这个元素来获取滚动条的宽度。
constcalcScrollBarWidth=function(){
consttestNode=createElement('div',{
style:'visibility:hidden;position:absolute;width:100px;height:100px;z-index:-999;overflow:scroll;'
});
document.body.appendChild(testNode);
constscrollBarWidth=testNode.offsetWidth-testNode.clientWidth;
document.body.removeChild(testNode);
returnscrollBarWidth;
};
constpreventJitter=function(){
constscrollBarWidth=calcScrollBarWidth();
if(parseInt(document.documentElement.style.marginRight)===scrollBarWidth){
document.documentElement.style.marginRight=0;
}else{
document.documentElement.style.marginRight=scrollBarWidth+'px';
}
};
结语
我们上面讨论了做好一个模态框所需要考虑的技术,但是肯定还有不完善和错误的地方,所以,如果错误的地方请给我提issue我会尽快修正。代码
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。
声明:本文内容来源于网络,版权归原作者所有,内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:czq8825#qq.com(发邮件时,请将#更换为@)进行举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。