当前位置: 首页 > Web前端 > vue.js

NutUI组件弹窗源码分析

时间:2023-03-31 17:26:55 vue.js

前言NutUI是一个京东风格的移动端Vue组件库,生态覆盖面广,支持按需加载、主题自定义、多语言等,功能强大。目前在用京东项目40+,设计精美,风格统一。NutUI在开发组件库的过程中,如何处理组件之间的层级关系?今天就来分析一下NutUI中处理层级关系的公共组件弹窗。1、什么是弹窗?它是一个公共组件,很多带有弹出层的组件都是基于这个组件开发的。封装这个组件首先解决了重新发明轮子的问题,避免了多个组件不得不开发这个公共功能。但是,它的优点不仅限于此,它还有以下优点:1、动态处理层级关系,保证后面触发的子弹窗口显示在最上面。2、多个弹窗组件同时显示时,维护一个遮罩层,动态处理遮罩层关系。这篇文章主要包括三个方面:层次关系的动态处理掩膜层管理滚动渗透问题2.层次关系的动态处理2.1.为什么要处理等级关系?当前页面顶部显示弹框、Toast提示等,那么如何定义它们的zIndex来保证当前显示的内容在页面顶部呢?正常开发时,可能会根据具体业务调整zIndex,保证外层组件的zindex值最大。然而,实际情况可能更复杂。由于不同的开发人员接手,各种业务需求迭代,在实际开发过程中可能会出现各种问题,很容易通过更改一个zIndex来“换一身”,从而导致很多其他问题,甚至将zIndex设置为无效.下面整理一下处理层次问题的各种方案。2.2.如何设置zIndex2.2.1。PLANA有人说可以统一所有组件的zIndex,从而遵循“后来居上”的规则,开发者只要在代码中调整组件的顺序,就可以保证层次关系。当级联层级相同,级联顺序相同时,DOM流中后面的元素会覆盖前面的元素。这样处理确实可以解决一部分问题,但是还是存在很多隐患。比如在最外层显示哪个弹窗可能是由用户的点击顺序决定的,不是一成不变的,每个人的开发习惯不同,有时很难统一。基于这种情况,我们可以采用统一动态的方式来生成zIndex:在组件库中,我们可以对组件库的zIndex值做一个统一的处理,每次调用动态+1。让zIndex=2000;functiongetZIndex(){return++zIndex}然后每次调用时动态赋值this.$el.style.zIndex=getZIndex();zIndex变量可以在组件库中通过这种方式统一管理,但并不适用于所有的开发情况,比如代码中充斥着各种类型的组件,zIndex就没有统一管理。然后可以使用PLANB来动态计算最大zIndex。2.2.2.PLANB这里介绍遍历DOM节点动态计算最大zIndex值的方法。getZIndex(){return[...document.all].reduce((r,e)=>Math.max(r,+window.getComputedStyle(e).zIndex||0),0)}这个方法可以使你得到此刻最大的zIndex,然后你可以给它加1。2.3.堆叠上下文(stackingcontext)在实际开发中,仅仅设置zIndex可能是不够的,即使我们将zIndex设置为最大,也不一定会显示在最上面。因为zIndex只会在当前的堆栈上下文中起作用。因此,单独设置zIndex并不全面,还必须考虑堆叠上下文的因素。我们先看下面的代码:

这时候图片会在mask层的最上面,因为它们属于一个stackingcontext,所以“谁大就往上”。但是我们添加了一个position:relative;z-index:1999;在img的父级,那就完全不一样了。
这时候我们发现图片跑到遮罩层下面了。因为img的parent开启了一个新的stackingcontext,所以zindex与overlay相比不再是img的2001而是其parent的1999。为了避免父元素的影响,我们可以将遮罩层和图片放在一个层次下。只要保持级联上下文,就可以保证“谁大谁先”。回到我们弹窗组件的开发,为了避免zIndex设置不生效的问题,我们也可以将遮罩层组件和弹窗组件并排放置,从而避免上述问题。
解决了组件之间的级联问题,于是再次暴露了多个堆叠组件包含遮罩层的问题。下面我们来看看遮罩层的管理。3、遮罩层管理3.1.为什么我们需要管理?当同时显示多个带有遮罩层的组件时,我们需要对遮罩层进行统一管理。首先,避免同时显示多个遮罩层。并动态调整遮罩层的zIndex。有关详细信息,请参阅下面的示例。如上图动态所示,点击“显示弹窗层”可以看到显示了一个遮罩层+弹窗,弹窗上有两个单元格。这时,再次点击“SelectDelivery”,维护了一个遮罩层,但是它的层级发生了变化,已经提升到两个单元格之上。恢复到之前的水平。下面我们就来看看设计思路吧。3.1.1.设计思路分层模块管理的设计思路如下图所示:1.Overlay组件负责显示遮罩层。2.overlay-manager负责动态生成zIndex并管理Overlay组件,控制Overlay维护一个实例,动态更新zI??ndex等。3.popup调用overlay-manager的各种方法获取最新的zIndex并控制显示遮罩层和弹出窗口。4.其他组件调用弹窗组件完成具体的业务组件。先说一下overlay-manager的具体实现思路。3.2.使用stack来存储组件内容首先说一下什么是stack。是一种先进后出的数据结构,非常适合现在的场景。因为用户面对屏幕的时候总是先处理最上面弹出的内容,然后再一层层处理。我们用一个数组来实现这个方法。letstack=[];stack.push(obj);stack.pop()下面我们用代码实现功能(缩小版)letmodalStack=[];let_zIndex=2000;constoverlayManager={//获取最新的zIndexgetzIndex(){return++_zIndex;},//获取最外层的弹窗组件实例gettopStack(){returnmodalStack[modalStack.length-1];},//打开遮罩层1.在modalStack中添加最新的弹出组件实例和配置2.调用更新叠加组件方法openModal(vm,config){modalStack.push({vm,config});//更新覆盖内容this.updateOverlay();},//关闭overlay层1.移除modalStack中最后添加的组件实例和配置2.调用更新遮罩层组件方法closeOverlay(vm){if(modalStack.length){modalStack.pop();}//更新遮罩层的内容this.updateOverlay();},}整体思路如下图所示:下面我们来看看如何在popup中使用overlay-manager方法。从“./overlay-manager.js”导入覆盖管理器;导出默认{道具:{值:{类型:布尔值,默认值:false,}},手表:{值(val){val?this.open():这个.close();}},方法:{open(){constconfig={zIndex:overlayManager.zIndex,};//渲染遮罩层this.renderOverlay(config);//给当前组件的zIndex赋值this.$el.style.zIndex=overlayManager.zIndex;},close(){//...},renderOverlay(){overlayManager.openModal(this);},}}控制弹窗和弹窗Display中的遮罩层并赋值zIndex,下面说说如何动态更新遮罩层。3.3.动态更新遮罩层。在上一节的最后,在打开和关闭方法中调用了updateOverlay函数。在说这个功能之前,我们需要先用vue实例化一个overlay组件,然后透传传参。functionmount(Component){constinstance=newVue({props:Component.props,render(h){returnh(Component,{props:this.$props});},}).$mount();returninstance;}实例化后需要保持单例,然后动态更新vue对象挂载的location和props来控制遮罩层是否显示和显示层级。updateOverlay(){const{clickHandle,topStack}=overlayManager;if(!overlay){overlay=mount(overlayComponent);}if(topStack){const{vm,config}=topStack;constel=vm.$el;el&&el.parentNode&&el.parentNode.nodeType!==11?el.parentNode.appendChild(overlay.$el):document.body.appendChild(overlay.$el);Object.assign(overlay,config,{value:true,});}else{overlay.value=false;}},动态更新遮罩层的位置,这个功能我们使用appendChild方法来实现自动将节点移动到新位置的功能,也就是利用了以下特点:appendChild()方法可以添加一个新的子节点到节点的子节点列表的末尾。如果newchild已经存在于文档树中,它将从文档树中删除,然后重新插入到它的新位置。以上就是掩膜层管理的基本设计思路。这就是全部吗?本着善人尽善尽美,送佛西天的精神,我们再来看一些其他的问题,比如滚透的问题。4.滚动穿透问题4.1.问题描述当显示遮罩层,滚动页面时,可以通过遮罩层影响下面的内容,如下图:从图中可以看出,向下滚动页面,下面的背景会也是滚动,那么问题来了,上面有遮罩层,为什么会影响层级以下的内容呢?我想这个问题的原因可以从DOM的事件流中找到,因为DOM中的事件不会只停留在当前对象上,而是会经过capturephase、targetphase和bubblingphase,这会影响到其他元素.下面来看看具体的解决方法。4.2.PLANA最简单的解决方案是在身体上添加样式。nut-overflow-hidden{overflow:hidden;}这个方案不是禁止滚动穿透,而是防止后台滚动,穿透的功能还是有的。经过我的测试,这个方法在pc和android上都可以正常使用,但是在ios上就不行了。为了找到同时适用于Android和ios的方法,我们继续探索。4.3.PLANB我们还可以使用另一种方法,通过添加position来禁止滚动;当窗口弹出时,我们将以下类添加到body标签中。...发现了致命的副作用。当点击弹窗时,页面会瞬间跳转到顶部。为了提高用户体验,我们需要不断探索。4.4.PLANC回到事件本身,换个思路,通过阻止touchmove事件来解决问题。接下来我们来实践一下,在遮罩层组件中添加preventDefault:防止默认();}}}};经过测试发现在遮罩层上滑动不会引起背景层滚动,但是忽略弹窗区域,在弹出框区域滚动还是会引起背景层的滚动,pop-upwindow元素和mask元素属于同一个parent下的siblinglevel,所以mask元素的preventDefault自然不会影响pop-upwindow元素。看到这里,有人可能会说,直接在弹窗元素上加上preventDefault就可以了,和遮罩层一样,但是弹窗进入主显示区,可能会有很长的一段文本滚动状态。如果滚动,则影响正常功能。那么当背景和弹窗都可以滚动时如何处理这个问题呢?经过一番研究,发现了以下规则(纯属个人观点,如有不妥欢迎评论区批评指正)。在弹窗中滑动屏幕首先处理弹窗中的滚动。当幻灯片到达终点时,会触发背景层的滚动。针对这一现象,我们做出以下解决方案。当弹窗滚动到某个方法且有滚动间隔时,允许滚动,没有滚动间隔则禁止滚动。听起来可能有点绕口,换句话说就是让弹窗保持正常滚动,禁止滚动弹窗滚动以外的内容。让我们看看下面的实现:4.4.1。判断手势方向判断用户是向上滑动还是向下滑动,首先记录滑动开始的位置,然后根据滚动的最终位置判断滑动的方向。document.addEventListener('touchstart',this.touchStart);document.addEventListener('touchmove',this.touchMove);//...touchStart(event){this.startY=event.touches[0].clientY;},touchMove(event){consttouch=event.touches[0];this.deltaY=touch.clientY-this.startY;}根据deltaY判断方向,大于0表示向上滑动,否则向下滑动。4.4.2.获取弹窗中的滚动元素第二步是找到弹窗中的滚动元素,然后结合用户手势判断当前滚动是否已经结束。找到这个滚动元素后。然后通过元素的滚动位置来判断什么时候应该禁用滚动。getScroller(el){让节点=el;while(node&&node.tagName!=='HTML'&&node.nodeType===1){const{overflowY}=window.getComputedStyle(node);if(/scroll|auto/i.test(overflowY)){返回节点;}node=node.parentNode;}}4.4.3。禁止滚动超出滚动区域constel=this.getScroller(event.target);const{scrollHeight,offsetHeight,scrollTop}=el?el:这个.$el;我们需要这三个值,scrollHeight、offsetHeight、scrollTop。scrollHeight此只读属性用于衡量元素内容的高度,包括因溢出而在视图中不可见的内容。offsetHeight是一个只读属性,返回元素的像素高度,包括元素的垂直填充和边框,并且是一个整数。scrollTop属性可以获取或设置元素内容垂直滚动的像素数。我先处理向上滑动手势的判断,判断方向+是否滑动到顶部,两个条件同时满足时禁止。if((this.deltaY>0&&scrollTop===0)event.preventDefault();}接下来判断下滑到底部的时候,逻辑同上if(this.deltaY<0&&scrollTop+offsetHeight>=scrollHeight){event.preventDefault();}这样就完成了ios上禁止滚动穿透的功能5.总结本文从两个实现角度讲解popup常用组件的实现原理以及常见问题的解决方案,为了让大家对逻辑主干有个清晰的认识,示例中减少了很多代码,文章中包含了很多个人的学习和思考过程,希望可以对大家有所帮助最后附上官网地址NutUI组件库目前正在不断优化迭代中欢迎大家使用并提出宝贵意见一、官网地址https://nutui.jd.com2.留言地址https://github.com/jdf2e/nutui/issues以梦为马,不负青春,笑看流年,未来可期,前行的路上需要你我携手,加油!