当前位置: 首页 > 科技观察

折叠面板组件的设计与实现

时间:2023-03-16 18:08:01 科技观察

span{color:var(--color);}前言NutUI,你应该不陌生,前端开发的同学一定有所了解。NutUI是一个京东风格的移动端组件库,使用Vue语言编写可在H5和小程序平台上使用的应用程序。目前NutUI拥有70+组件,支持按需引用,支持TypeScript,支持自定义主题等功能,当然也支持最新的Vue3语法,可以有效帮助开发者在开发中提高效率,提升开发体验。言归正传,今天我们来学习一下折叠面板Collapse在NutUI中的实现与设计,以及开发过程中学到的新知识点。折叠面板设计其实折叠面板组件无论是在PC还是M上都是一个比较常见的组件,顾名思义就是一个可以折叠/展开的内容区域。还有广泛的使用场景,比如导航、文字详情、筛选分类等;在组件开发阶段,我们通常会进行对比分析,取长补短。所以我们简单的通过功能对比进入组件的开发。组件的本质是为了提高开发效率。我们通过对业务场景的配置进行解构和组合来实现业务需求。例如,组件库是一个工具箱。每个组件都是一个工具,例如盒子中的扳手和钳子。它为业务场景提供了各种工具。如何打造一款适合工作的工具,需要我们对平时的业务开发有一定的了解。理解和思考。一起来探索吧~展开收起组件的基本交互已经清楚了,我们的title和content的布局就比较简单了。现在我们需要完成交互开发,即展开和折叠的功能。实现展开折叠功能其实很简单。通过一个变量来控制内容的显示和隐藏就足够了。在不考虑其他因素的情况下,这种方法确实是最有效的方法。但是使用这种方式可能对我们后期的功能扩展和交互效果不是很友好。所以我的方案是通过改变折叠内容的高度来实现的。当然,这个方法也很容易理解。我们主要处理内容的内容。对于这个样式,我们默认设置它的高度为0,即内容是折叠的。因为无法确定每一折的内容,所以我们需要动态计算填充内容的高度,这也是一种适配方案。我动态计算的目的是实现后面的动画效果,提高用户体验。我是用height+transform实现的,同时利用css的属性will-change来优化动画效果。will-change为web开发者提供了一种将元素发生变化通知浏览器的方式,以便浏览器在元素的属性真正发生变化之前,提前做好相应的优化准备。这种优化可以提前准备部分复杂的计算工作,让页面的响应更快更灵敏。//组件部分核心代码constwrapperRefEle:any=wrapperRef.value;constcontentRefEle:any=contentRef.value;if(!wrapperRefEle||!contentRefEle){return;}constoffsetHeight=contentRefEle.offsetHeight||'auto';if(offsetHeight){constcontentHeight=`${offsetHeight}px`;wrapperRefEle.style.willChange='height';wrapperRefEle.style.height=!proxyData.openExpanded?0:contentHeight;}上面代码是获取元素的DOM,计算内容的高度offsetHeight并赋值,通过高度的变化和transform实现折叠展开的动画效果。灵活的标题栏紧随其后的是对标题栏功能的改进,增加了图标、自定义位置和相关的动画功能。我们先来看基本用法的右侧图标,对应内容的收起和展开。当它以交互方式展开时,它是向上箭头,当它折叠时,它是向下箭头。那么我们根据展开状态是否为变量,用一个箭头图标就可以轻松搞定。解决方法是利用css3的rotate属性来反转180°。if(parent.props.icon&&!proxyData.openExpanded){proxyData.iconStyle['transform']='rotate(0deg)';}else{proxyData.iconStyle['transform']='rotate('+parent.props.rotate+'deg)';}为了加强用户自定义,扩展组件能力,对外暴露了图标配置的API,比如自定义图标,图标旋转角度等,这些配置指的是不同的场景,比如content一些新闻报道的图片被折叠并旋转了90°。当然,标题栏文本也可以配置相关的图标,包括图标的位置、颜色、大小等。该功能增加了用户的个性化配置,可用于显示一些重要消息、新消息提醒、未查看信息等场景。一些组件库的开发者可能没有这个配置。首先我个人觉得跟组件没什么关系。组件的设计需要和业务联系起来,抽象出一些功能,这样才能更好的完善组件的功能,包括后期组件的扩展,这些都是在开发中成长的的业务。配置项升级在后期的使用过程中,我们针对某些场景对组件功能进行了优化升级。首先,增加了字幕的配置,可以很方便的通过字幕来设置(PS:可以看上图的例子)。商城移动端中的搜索分类功能,如下图场景。它会有默认的内容显示在外面,其余的内容会折叠或者折叠后展开,所以增加了slot:extraRenderAPI,让这部分内容以slot的形式存在,即方便开发者定义不同的显示形式。样式调整等上述功能的实现也比较简单,只需要在代码中添加一个slot标签即可接收传入的内容。

既然这里提到了插槽,那我就多说几句[傻笑】。关于上面提到的标题和内容的展示,设计考虑到可以让开发者省时省力,并且有更多的可操作性。基本上都是以槽的形式接收入参(仅针对本组件,内容展示相关),这样即使后端或前端处理数据带有HTML标签,也可以轻松识别,无需冗余处理.由于面板可以展开和折叠,因此存在相反被禁止的操作。我提供了一个简单的属性设置disabled来判断是否可操作,实现方法是通过设置style样式来实现的。.nu??t-collapse-item-disabled{颜色:#c8c9cc;游标:不允许;pointer-events:none;}开发设计小插曲01Scss中使用变量的作用想必大家都不陌生。说白了,CSS是可以通过JS来控制样式的,目前Vue3支持我们在CSS中使用变量,直接在代码上。span{color:var(--color);}是不是很简单?其实类似的写法之前已经有类似的插件支持了。如果你对emotionjssstyled-componentsaphroditeradiumglamor感兴趣,可以试试这些插件。小编用过styled-components,上手还是很容易的。在开始之前,我建议您了解CSS-in-JS的概念。02组件化开发适配你想成为NutUI的贡献者吗?如果你也想为NutUI贡献自己的组件,这里有一些适配小程序的要点~H5开发时获取DOM元素还是比较容易的,通过document或者ref即可。但是我们在适配小程序的时候是获取不到这个方法的,需要按照Taro提供的方法获取。从“@tarojs/taro”导入Taro,{eventCenter,getCurrentInstanceasgetCurrentInstanceTaro};eventCenter.once((getCurrentInstanceTaro()asany).router.onReady,()=>{constquery=Taro.createSelectorQuery();query.selectAll('.collapse-content').boundingClientRect();query.exec((res)=>{console.log(res);});});通过以上方法可以获取到节点信息,包括width、height、x、y等,可以尝试查看获取到的信息。另外需要注意的是,在为元素设置样式时,最好使用组件中的样式变量来接收,而不是直接赋值。//像这样改变样式conststyle=reactive({color:'red',height:'100px',});constchange=()=>{style.color='blue';}03vue3组件通信是在component开发的时候,因为nut-collapsenut-collapse-item的父子组件需要通信,我使用provide/inject方法,所以对这个通信方法进行了简单的学习。关于组件通信的方式,比如props、emit、attrs等,想必大家已经心知肚明,就不瞎说了。下面我就给大家简单分享一下provide/inject的参数传递形式。这个API在vue2中已经存在。//a.vue组件//创建一个provideimport{defineComponent,provide}from'vue';exportdefaultdefineComponent({setup(){constmsg:string='HelloNutUI';//provideoutprovide('msg',msg);}})//b.vue组件//接收数据import{defineComponent,inject}from'vue'exportdefaultdefineComponent({setup(){constmsg:string=inject('msg')||'';}})通过上面两个例子,操作很简单,但是要注意该提供没有响应。如果你想让它响应式,你需要传入并且应该是响应式数据。provide提供的数据不考虑组件层级,即发起provide的组件可以作为其所有下属组件的依赖provider。provide和inject的实现原理主要是利用prototype和prototypechain实现的。Vue3中的provide函数是在当前组件实例的provides对象属性中添加一个键值对key/value。还有一个地方就是如果当前组件和父组件的provides相同,那么当前组件实例中的provides对象和parent会建立一个链接,即prototype原型。functionprovide(key,value){if(!currentInstance){if((process.env.NODE_ENV!=='production')){warn(`provide()只能在setup()内部使用。`);}}else{//获取当前组件实例的provides属性letprovides=currentInstance.provides;//获取当前父组件的provides属性constparentProvides=currentInstance.parent&¤tInstance.parent.provides;if(parentProvides===provides){//Object.create()es6中创建对象的一种方式,可以理解为继承一个对象,添加的属性在prototype下。provides=currentInstance.provides=Object.create(parentProvides);}提供[键]=值;inject的实现我就不细说了。有兴趣的可以根据源码了解更多。从下面的代码中,我们可以大致了解到,inject是先获取当前组件的实例对象,再判断是否为根组件。如果是根组件,则返回appContext的provides,否则返回父组件的provides。如果当前key在provides中有值,则返回该值,否则判断是否有默认内容,如果默认内容为函数,则执行并绑定组件实例的代理对象通过调用方法到函数的this。否则直接返回默认内容。functioninject(key,defaultValue,treatDefaultAsFactory=false){//如果被功能组件调用,取currentRenderingInstanceconstinstance=currentInstance||当前渲染实例;if(instance){//如果实例在根目录下,则返回appContext的provides,否则返回父组件的providesconstprovides=instance.parent==null?instance.vnode.appContext&&instance.vnode.appContext.provides:instance.parent.provides;if(provides&&keyinprovides){returnprovides[key];}//如果参数大于1,第二个为默认值,第三个参数为true,第二个值为函数,则执行函数。elseif(arguments.length>1){returntreatDefaultAsFactory&&isFunction(defaultValue)?defaultValue.call(instance.proxy):defaultValue;可以大致理解为调用provideAPI时,将parent的provides设置为当前provides对象原型对象上的属性,inject获取provides对象中的属性值时,会先获取provides对象本身的属性。如果它自己找不到,它就会沿着原型链寻找下一个对象。总结本文主要介绍了NutUI中折叠面板组件的设计思路和实现原理,并分享了开发中遇到的一些问题,希望对大家的开发有所帮助。如果在开发过程中遇到问题,可以随时提交issue,NutUI团队的同学会认真对待并解决问题。如果你有好的组件,无论是业务的还是通用的,你都可以向NutUI组件库提交PR。非常欢迎大家参与共建。