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

Picker组件的设计与实现

时间:2023-03-31 15:35:43 vue.js

前言今天的主题是NutUIPicker组件的设计与实现。Picker组件是NutUI的一个选择器组件。它用于显示一系列值集。用户可以滚动以选择集合中的项目,也可以支持多个系列的值组供用户单独选择。下面用一张效果图来看看这个组件实现了哪些功能。说到NutUI,可能有些人不是很了解,我们先简单介绍一下。NutUI是一个京东风格的移动端Vue组件库,开发和服务于移动Web界面的企业级前中后端产品。通过NutUI,您可以快速构建风格统一的页面,提高开发效率。目前已有50+组件,广泛应用于京东各项移动服务。接下来,我们将通过以下话题来展开今天的内容:为什么要封装组件?NutUIPicker组件实现原理遇到的问题1.为什么要封装组件当业务发展到一定规模时,会遇到很多类似的功能接口,每次重新开发都会影响开发效率,而这些类似的代码可能有一些潜在的问题。一旦暴露,我们需要花费大量时间处理业务中的相同代码。如果我们将这些相同的代码合理化分离,封装组件,进行多次调用,我们会发现开发效率实现了质的飞跃。通过一张图来看看封装组件的好处:封装组件不仅可以让协同开发变得高效和规范,同时组件化的前端开发方式也可以为后续的业务扩展带来更多的便利。二、NutUIPicker组件的实现原理该组件在日常业务需求中比较常见。它不仅可以承载简单的选项卡功能,还可以满足更复杂的日期时间选择功能,或者级联地址选择功能。我们还有一个基于picker组件的日期时间组件的封装,有兴趣的可以访问NutUI组件库查看。通过文章的前言,我们对picker组件实现了哪些功能有了一个大概的了解。它使用类似于轮子的三维旋转来选择选择集中的项目。我们先来看一下组件源码的目录结构:我们主要关注最后三个文件。本着就近原则,我们将相关文件放在同一个目录下。基于单一职责的原则,我们将组件颗粒化,保证组件尽可能的简单和通用。picker组件分为父组件picker.vue和子组件picker-slot.vue,子组件只负责轮子交互处理。父组件负责处理业务类逻辑。子组件roller部分1.先看dom部分的分工{{item.value}}

{{item.value}}
nut-picker-indicator:分割线nut-picker-content:高亮选中areanut-picker-roller:不想看到滚轮区域的代码?“小二,上图!”2.在css部分,将nut-picker-indicator设置在最高层,避免被覆盖cover.nut-picker-indicator{...z-index:3;}nut-picker-rollerwheelarea.nut-picker-roller{z-index:1;变换样式:保留3d;……nut-picker-roller-item{背面可见性:隐藏;位置:绝对;顶部:0;...}}要实现一些3D效果,transform-style:preserve-3d;是必不可少的,一般来说,这个属性是作用于3D变换的父元素,也就是舞台元素,让子元素具有3D属性效果。在CSS的3D世界中,默认情况下,我们可以看到后面的元素。为了实用,我们经常让后面的元素不可见,所以设置子元素backface-visibility:hidden;值得注意的是,transform设置了-style:preserve-3d这个属性并不能防止子元素溢出。如果设置了overflow:hidden,transform-style:preserve-3d将失效。我们通过模拟轮子的转动来实现组件的交互效果,用侧视图更直观的看。接下来,我们就来看看如何实现吧。首先,需要模拟一个球体,将选择集的每一项(以下简称“滚轮项”)设置为position:absolute,共享同一个中心点,也就是球体的中心,然后堆叠在反过来。让我们先回顾一些基础知识。translate3d()函数可以使元素在三维空间中移动。这种变形的特点是使用3D矢量的坐标来定义元素在每个方向上移动的量。当z轴值越大时,元素越靠近观察者。我们设置z轴,使滚轮项目的两端到达球体表面。z轴的大小相当于球体的半径,因为我们设置可见区域的高度为260,所以设置半径为104。如果半径太小,需要佩戴高倍放大镜玻璃找到滚轮项目。如果半径太大,滚轮项就会跑到我们的后脑勺……眼睛就长不出来了后脑勺竟然发生了这么恐怖的事情!所谓距离创造美,保持适当的距离(80%)才是最美的。setRollerStyle(index){return`translate3d(0px,0px,104px)`;}这时候我们发现所有的rolleritem都是从球体中心堆叠到球体上的两点,我们需要对它们进行平铺按周边开。这时候我们就需要用到rotate3d()属性。我们的滚轮绕X轴旋转,所以设置X轴rotate3d(1,0,0,a),a是一个角度值,用来指定元素在3D空间旋转的角度,值为正值,元素顺时针旋转,否则元素逆时针旋转。这个角度怎么设置?可以通过一个圆心角公式推导出来。圆心角的度数等于它对应的圆弧的度数。我们的半径是104,弧长是36(我们预设的显示区域),这样一个角度的圆角计算就是20,有没有被文字包围的感觉?我们通过一张图来更直观的理解一下。使用上面的分析,让我们动态设置滚轮项目的最终位置。setRollerStyle(index){return`transform:rotate3d(1,0,0,${-this.rotation*index}deg)translate3d(0px,0px,104px)`;}需要注意的是,轮子项的数量可能vary有很多,不止一个圈的可能性很大,但是我们既不能一刀切的只给用户显示指定的个数,也不能全部显示出来造成重叠的问题。这时候我们就需要把多余的部分隐藏起来。我们知道角度值a是20度,圆是360度,所以最多可以显示18。我们以当前中心为基点,在前面显示8。9稍后显示。isHidden(index){return(index>=this.currIndex+9||index<=this.currIndex-8)?true:false;}3.添加事件最后我们来添加滑动事件,首先获取Vue实例关联的DOM元素,设置touchstart、touchmove、touchend事件,需要注意的是一定要记得在beforeDestroy事件中销毁这些事件。touchstart事件用于记录起点,touchmove和touchend事件用于记录滚动终点,计算差值,动态设置滚轮最外层元素的滚动距离和滚动角度。滚动时,需要修正滚动距离,保证最终滚动距离为lineSpacing的倍数(滚轮项高度为36)。我们还添加了弹性效果,让touchmove滚动超出滚动范围,然后在touchend事件中修正position为第一项和最后一项。我们来看看具体的实现。setMove(move,type,time){让updateMove=move+this.transformY;if(type==='end'){//touchend滚动处理//超出限制滚动距离的修正if(updateMove>0){updateMove=0;}if(updateMove<-(this.listData.length-1)*this.lineSpacing){updateMove=-(this.listData.length-1)*this.lineSpacing;}//设置滚动距离为lineSpacing值的倍数letendMove=Math.round(updateMove/this.lineSpacing)*this.lineSpacing;让deg=`${(Math.abs(Math.round(endMove/this.lineSpacing))+1)*this.rotation}deg`;this.setTransform(endMove,type,time,deg);this.timer=setTimeout(()=>{this.setChooseValue(endMove);},时间/2);this.currIndex=(Math.abs(Math.round(endMove/this.lineSpacing))+1);}else{//touchmove滚动处理letdeg='0deg';如果(updateMove<0){deg=`${(Math.abs(updateMove/this.lineSpacing)+1)*this.rotation}deg`;}else{deg=`${((-updateMove/this.lineSpacing)+1)*this.rotation}deg`;}this.setTransform(updateMove,null,null,deg);this.currIndex=(Math.abs(Math.round(updateMove/this.lineSpacing))+1);}},在touchend中,在滚轮的父元素中加入transition“easingfunction”,模拟惯性滚动效果setTransform(translateY=0,type,time=1000,deg){this.$refs.roller.style.transition=type==='结束'?`转换${time}mscubic-bezier(0.19,1,0.22,1)`:'';this.$refs.roller.style.transform=`rotate3d(1,0,0,${deg})`;}通过以上内容,我们的滚轮效果已经基本成型。但是我们也想要类似ios上时间选择器的高亮当前区域的效果,怎么实现呢?我们尝试了以下三种方法。第一个是考虑在滚轮项目停留在高亮区域时改变字体,但是在实践中发现只能在滚动结束时改变字体,而不能在滚动过程中设置,即不是一个友好的经历。二是能不能巧妙的运用CSS,利用背景渐变和background-size来完成渐变,利用遮罩层来实现!.nu??t-picker-mask{位置:绝对;顶部:0;左:0;右:0;底部:0;背景图像:线性渐变(180deg,hsla(0,0%,100%,.9),hsla(0,0%,100%,.6)),线性渐变(0deg,hsla(0,0%,100%,.9),hsla(0,0%,100%,.6));背景位置:顶部,底部;背景大小:100%108px;背景重复:不重复;z-index:3;}这里把背景设置成黄色,这样我们就可以看到效果了。感觉还行,搞定了吗?我们在PC端模拟一切正常,但在真机上却出现了诡异的画面。向上滑动弹出时,遮罩层显示会延迟,影响体验效果。只有禁用上滑过渡效果才能正常显示。去除滑动效果是不可能的,只能考虑其他方式。三是是否可以设置辅助滚动,也就是上面说的高亮区域,覆盖在滚轮上。其中每个元素的高度等于可见区域的高度。当滚轮滑动时,高亮区域内的List元素也随之滑动。实践证明,这种方法可以避免上述两种方法的缺点,完美解决我们的需求。我们来看看具体的实现方法。.nu??t-picker-content{位置:绝对;高度:36px;....nut-picker-roller-item{高度:36px;...}}然后在上面的setTransform函数中,增加高亮显示区域的滚动效果。setTransform(translateY=0,type,time=1000,deg){...this.$refs.list.style.transition=type==='end'?`转换${time}mscubic-bezier(0.19,1,0.22,1)`:'';this.$refs.list.style.transform=`translate3d(0,${translateY}px,0)`;}父组件部分除了滚动效果外,还有一些灰色业务内容如遮罩层、滑动popup、workbar等,我们交给父组件来处理。我们的业务也涉及到多个列,所以父组件可以将props数据拆分给子组件,让各个子组件相互独立,监听子组件的事件,传给外层。3、遇到的问题我们的组件是基于px实现的。在问题中,一些用户遇到了一些问题,这里提供了解决方案。1、使用px2rem时,滚轮转动有偏差。因为px转rem的值有时会有偏差,而且有多个小数位,导致滚动高度和实际转换的高度有偏差。我们可以通过如下配置解决问题一:在.postcssrc.js配置文件中,过滤掉nutuimodule.exports=({file})=>{return{plugins:[...pxtorem({rootValue:rootValue,propList:['*'],minPixelValue:2,selectorBlackList:['.nut']//set})}}第二:postcss-px2rem-exclude而不是postcss-px2remnpmuninstallpostcss-px2remnpmipostcss-px2rem-exclude-D//在.postcssrc.js配置中module.exports=({file})=>{return{plugins:[...pxtorem({remUnit:rootValue,exclude:'/node_modules/@nutui/nutui/packages/picker'})]}}2.使用lib-flexible,减少组件。我们的css是在data-dpr为1的情况下写的,如果使用lib-flexible,页面要设置以后我们也会考虑从代码层面解决上面的问题。综上所述,以上就是本文的全部内容。主要介绍Picker组件的一些设计思想和实现原理。如果您对此组件感兴趣,不妨查看并试用一下。如果大家在使用上有什么问题,可以在issues上提问,我们会第一时间解答和修复,后续会继续优化迭代组件,访问NutUI组件库,更多组件正等着你去发现。