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

教你实现一个完美的移动端瀑布组件

时间:2023-03-17 23:38:39 科技观察

后台瀑布是你在日常开发过程中经常遇到的场景。我们公司内部的组件库也提供了一些解决方案。但是,这些方案的适用场景都非常单一,每一个实施方案都或多或少存在问题。基于此,我们设计开发了兼容多种场景的瀑布流组件。目前产品流向展示采用三种布局方式:卡片流、固定瀑布流、交错瀑布流。卡片流程以下拉列表的形式呈现。这种布局允许用户专注于单个列表项,这有利于阅读。主要应用于转转二级列表页的??入口。效果如下。卡流固定瀑布流图像区域的大小和高度保持不变。统一的高度会让整个界面看起来整洁,视觉上不会杂乱无章。主要用于一些频道页面场景。效果如下:固定瀑布流,交错瀑布流,视觉上表现为由等宽可变高度的元素组成的不均匀的多列布局。以上三种场景,第一种和第二种场景的图片高度是固定的,实现起来比较简单,直接使用无限加载List组件即可。经常出错的是第三种情况:交错的瀑布。在这种场景下,需要在加载图片后获取图片的高度,然后将其添加到瀑布的最低列,否则会影响最低列的计算,导致列长不一。转转内实现交错瀑布流主要有以下优点:使用IntersectionObserve实现瀑布流的懒加载,逻辑实现简单。缺点:方案一:采用左右两栏布局,将瀑布流第一页平分在左右Stream数据上渲染。当第二页的数据渲染出来的时候,会把第二页的第一个数据拉出来渲染到最下面的列,进行IntersectionObserve监控。元素出现在窗口后,会从数据源中获取第二条数据,添加到新的最小渲染列中,懒加载的瀑布列布局只能支持两列,不支持多列的参数配置;第一页的数据不符合瀑布流的规范,有概率出现一栏很长的情况。一栏很短;IntersectionObserve的兼容性问题;没有暴露数据加载完成的事件,所以在配合无限加载组件时,容易出现两次下拉请求的问题开启IntersectionObserve监听,元素出现在窗口后,设置一个setTimeout为加载下一个瀑布流元素,在dom上添加属性标记,防止二次触发。优点:支持参数配置多栏布局,首屏符合瀑布流规范,瀑布流加载后事件暴露,结合无限加载不会出现二次请求界面的问题。缺点:IntersectionObserve仍然没有兼容性问题的解决方案;内部DOM查询,运行频率高;加上无限加载List的逻辑,维护成本高;setTimeout不能保证图片在正确的时机加载,会导致访问到最下面的列不准确。方案三:使用绝对定位布局方案。实现原理是在每个子组件waterall-item内部新建一个图片对象,监听onload事件然后触发父组件waterfall重新排列瀑布流。优点:内部逻辑简单,易于维护,也符合瀑布流规范。它提供了瀑布流中常用的几个配置项。加载完成后,会触发事件通知外部组件。缺点:不支持图片延迟加载;重绘过多(1+2+...+N),对性能不太友好;触发重绘的时机不是最准确的时间节点(由新图片后的onload事件触发,而不是绑定当前图片上的onload事件)然后去网上找了一个开源的解决方案。以下是Github上Star数排名前4的解决方案。缺点:在组件渲染之前需要知道图片的宽高,我们一般不会在接口中返回这些数据vue-waterfall[1]:star最多的方案。vue-waterfall-easy[2]:无需提前获取图片的宽高信息,在排版前使用图片预加载。缺点:耦合下拉,无限加载组件;包括PC端逻辑,封装体积大,对追求性能的页面不友好(作为开源方案,兼容更多场景无可厚非,但我们已经有单独的功能组件实现);一次性加载所有图片,不支持延迟加载vue-waterfall2[3]:支持高度自适应,支持延迟加载缺点:内部创建多个图片对象,伴随大量计算和滚动监控。vue2-waterfall[4]:通过封装两个开源方案masonry-layout和imagesloaded实现,逻辑简单明了。缺点:不支持懒加载用一张图简单总结一下新瀑布流方案的设计目前还没有简单易用的移动端瀑布流组件,所以打算整合已知的方案,重新实现一个新的瀑布流组件。新的瀑布流将包含以下优点:简单的CSS布局,精简的逻辑实现,高度自适应支持,懒加载布局方案,了解瀑布流CSS布局方案主要分为三种绝对定位:上述方案3、开源方案vue-waterfall-easy采用这种布局,更适合PC端瀑布流宽度百分比:上面方案2和开源方案vue-waterfall2采用这种布局,但是这种方案会有一些精度问题。Flex布局:蘑菇街等一些大型电路商业网站采用这种布局。Flex布局兼容性适配没有问题,应该是移动端布局方案的最优方案。因此,新的瀑布流将使用这种布局方案来实现瀑布流逻辑。对于瀑布流的逻辑实现,也分为三种。在该方案中,IntersectionObserver监听图像元素,出现在视图中,开始从瀑布数据队列的列头中取出一条数据,渲染到当前瀑布流的最低列。这样就反复实现了瀑布流的懒加载。三种方案中,第一种较为常规,大部分开源方案都是采用这种方式实现的。但是内部需要进行高度转换,不支持图片懒加载。第二种解决方案应该更好。可以在图片加载前开始排版,方便简单,而且还支持懒加载,用户体验也不错。蘑菇街、天猫、京东等均采用该方案。但是这种场景需要做一些修改,比如在图片入库前将图片信息拼接在url中,或者后端接口读取图片对象,然后将图片信息返回给前端.要么改造成本高,要么会增加服务器的压力,不太适合我们的业务。第三种方案无需其他修改即可支持懒加载,应该是目前最合适的方案。因此,新的瀑布组件将使用IntersectionObserver来实现瀑布的排版。新的瀑布将实现IntersectionObserver兼容性。我们将面临的第一个问题是IntersectionObserver的兼容性。IntersectionObserver虽然解决了传统的滚动监听带来的性能问题,但是兼容性并没有得到主流的支持。可见iOS上的支持并不完善。官方提供了一个polyfill[5]来解决上述问题,但是这个polyfill体积较大,直接导入对于一些追求极致性能的页面并不友好,所以我们在场景中采用了动态导入polyfill的方式//在不支持IntersectionObserver的地方,动态导入polyfillconstioPromise=checkIntersectionObserver()?Promise.resolve():import('intersection-observer')ioPromise.then(()=>{//dosomething})只会在不支持IntersectionObserver的环境下加载这个polyfill,检测方法摘自VuelazyloadconstinBrowser=typeofwindow!=='undefined'&&window!==nullfunctioncheckIntersectionObserver(){if(inBrowser&&'IntersectionObserver'inwindow&&'IntersectionObserverEntry'inwindow&&'intersectionRatio'inwindow.IntersectionObserverEntry.prototype){//Edge15缺失的最小polyfill`isIntersecting`//参见:https://github.com/w3c/IntersectionObserver/issues/211if(!('isIntersecting'inwindow.IntersectionObserverEntry.prototype)){Object.defineProperty(window.IntersectionObserverEntry.prototype,'isIntersecting',{get:function(){returnthis.intersectionRatio>0}})}returntrue}returnfalse}Waterfallimageloadingtiming图片加载是一个异步过程,如何保证waterfallflow图片的加载时间呢?触发IntersectionObserver的回调函数后直接开始加载下一张瀑布图,很容易出现不同长度的列和页面抖动,因为图片可能只有在触发回调时才加载方案一和方案二都存在这个问题,查看文档可以看到IntersectionObserver的回调函数中提供的IntersectionObserverEntry对象会提供如下属性time:可见性发生变化的时间,是一个高精度的时间戳单位毫秒target:要观察的目标元素是一个DOM节点对象rootBounds:根元素的矩形区域的信息,getBoundingClientRect()方法的返回值,如果没有根元素(即直接相对于视口滚动),则返回nullboundingClientRect:目标元素矩形区域信息intersectionRect:目标元素与视口(或根元素)相交区域信息intersectionRatio:目标元素可见比例,即intersectionRect与boundingClientRect的比值,完全可见时为1,完全可见时小于等于0n它是完全不可见的。可以在target上绑定onload事件,onload之后再进行下一次瀑布数据渲染,可以保证下一次渲染时获取最低列的准确性//Waterfalllayout:取出队头的数据并将Render添加到瀑布高度最小的那一列,图片加载满后重复循环(isIntersecting){if(target.complete){done()}else{target.onload=target.onerror=done}}}})IntersectionObserver二次触发问题我们知道,使用IntersectionObserver来监听目标元素,当可见性目标元素发生变化,回调函数一般会触发两次。一次是目标元素刚进入视口(开始可见),一次是完全离开视口(开始不可见)。为了避免第二次再次触发监控逻辑,可以在第一次触发时停止观察if(isIntersecting){constdone=()=>{//停止观察,防止监控逻辑被回调时再次触发observerObj.unobserve(target)}if(target.complete){done()}else{target.onload=target.onerror=done}}首屏渲染时的白屏问题是因为图片是串行加载的,图片是一张一张渲染的,这种情况下网络不好的时候白屏现象会很严重。下图目前提供了两种解决方案。一:图像首屏渲染并行渲染,后续渲染串行化。假设接口返回的一个页面上有20个瀑布元素,那么前1-4张图片会并行渲染,后5-20张图片会串行渲染。firstPageCount可以根据实际情况调整。一般首屏会渲染4-6张图片。waterfall(){//更新瀑布高度最小的列this.updateMinCol()//取出数据源中最前面的,添加到瀑布高度最小的列this.appendColData()//并行渲染用于首屏,非首屏采用串行渲染)=>this.startObserver())}}方法二:添加动画,从视觉上消除白屏的影响。组件内置了两个动画,通过动画传参可以解决懒加载时的白屏问题。我们采用懒加载的方案:当图片出现在视图中后,再加载下一张瀑布图,对性能更友好。但在这种情况下,当用户在滚动时,如果下一张图片加载太慢,可能会出现短暂的白屏时间。如何解决这个体验问题?IntersectionObserver有一个rootMargin属性,我们可以使用它来扩展相交区域。从而提前加载以下数据。这样既可以防止用户滚动到底部时出现白屏,也可以防止渲染过多影响性能。默认设置为400px,大约是提前渲染的半屏数据。//扩展intersectionRect的交集区域,可以提前加载部分数据,优化用户浏览体验。rootMargin:{type:String,default:'0px0px400px0px'}无限加载组件如何配合一般为了维护方便,我们会将无限加载和瀑布流两部分的逻辑分开,所以在渲染瀑布流数据时,需要通知外部组件,否则很容易造成瀑布流渲染前无限加载的逻辑,发送两次接口请求的问题。可以在瀑布渲染过程中加入判断。如果队列中没有数据,则通知外部无限加载组件进行下一次请求。constdone=()=>{if(this.innerData.length){this.waterfall()}else{this.$emit('rendered')}}总结以上是制作新瀑布时遇到的一些问题及相应解决方案成分。当然,这个方案还有优化的空间,目前在公司内部作为组件块使用。