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

打造如丝般顺滑的H5翻页库

时间:2023-03-17 20:34:49 科技观察

背景随着近几年移动营销页面的流行,诞生了一个中国式的名词“H5”。H5最常见的形式类似于翻转幻灯片的效果。当我们需要做H5的时候,最快的方法就是使用一些滑动插件库,比如iDangero.us出品的Swiper,百度BE-FE出品的iSlider。通过这些翻页库提供的强大的配置功能,我们可以实现炫酷的翻页效果。当然,这些库也支持自动播放、点击切换、当前页面指示等配置,所以也可以用在网页上,实现一些网页轮播效果。百度H5也使用了Swiper和iSlider作为H5运行时的翻页框架。随着用户越来越多,也遇到了一些问题:H5平台与这些库适配不佳,有些配置项无法使用。而一些必要的功能需要以“Hack”的方式实现。有些H5有很多元素和动画。在低端机型翻页时,翻页时会有“卡顿感”、“黏糊糊的感觉”,用户体验不好。而我们希望H5翻页库能够完美贴合平台自身的功能,在保持体积小的同时,翻页时能够“丝滑”。于是我们开始了研(zao)研(lun)的(zi)之旅。开始H5滑屏框架的开发,第一个问题是:页面是否随手指滑动?这也是腾讯ISUX团队的《滑屏 H5 开发实践九问》的第一个问题(本文原出处现为404,可以到其他转载网站查看),这里用本文的一张图来说明这个问题。左边的不跟随手指滑动,只需要关注手指触摸开始和离开的两个时间点,不需要考虑中间的过程。所以实现起来比较简单。但用户操作没有实时反馈,体验不够好。因此,虽然实现起来比较复杂,但我们还是决定实现之前的“用手指滑动”的效果。下图是最直观的用手指滑动的H5版本。所有的“页面”都是从上到下依次连接起来的。需要说明的是,这里的“page”是用引号引起来的,因为它们其实就是div,后面提到的pages就是指这些div。同时,我们以最常见的垂直滑动为例,水平方向也是如此。基本示意图中这些div的宽高为容器高度的100%,可见区域为中间部分。我们监听touchstart、touchmove、touchend事件,类似于鼠标拖动的原理:touchstart时,记录起点;touchmove实时计算滑动距离,让所有页面一起沿Y轴平移这个距离。touchend时,可以得到最终的滑动距离,并与设定的阈值进行比较。进入页面自动控制阶段:如果大于阈值,则页面滑动到下一页,如果小于阈值,则返回到原来的位置。简单版的深入探索在前面的部分很容易实现。如果其他需求不多,页面上的元素和动画比较少,基本够用了。但本文要探讨的是如何做到“丝滑”,其实就是两个字:性能。性能瓶颈是什么?我们的目标是:在“三多一低”(页面多、元素多、动画多、配置低)的情况下,滑动翻页时,尽可能少的卡顿。我们分两部分来看这个问题:手指离开屏幕前和手指离开屏幕后。手指离开屏幕前的性能密集型操作是:触发touchmove时,计算要移动的距离,所有页面都需要沿Y轴移动相同的距离。这时候就不可避免地要进行DOM操作,而DOM操作是非常“昂贵”的。再加上touchmove事件的频繁触发,如果性能处理的不够好,很容易卡住。为了优化性能,我们自然会想到一个策略:减少DOM操作。这里分为两部分:减少DOM操作的元素和减少DOM操作的属性。前者比如看不到的页面不参与动画。例如,后者仅更改元素的一个或几个css属性。具有较少DOM操作的元素在最初的简单版本示例中,当触发touchmove时,所有页面都沿Y轴移动。其实没必要,因为页面有相当一部分是不可见的。一般情况下,我们至少需要操作多少个页面呢?答案是两个。回想一下,当我们滑动时,我们最多可以同时看到两个页面。与将所有页面一起移动相比,此方法以指数方式提高性能。这种减少DOM操作属性的方法的主要意义在于,你只需要对DOM进行一次操作,而不是两次。其实对于滑动动画,我们只需要改变页面的transform值即可,其他的DOM操作(添加class,修改元素的innerHTML)等都可以省略。我们得到了一个初步的解决方案:在初始化的时候,将所有的页面一次性放入容器中,除了我们使用的那两个页面,display属性设置为none。touchmove时,只有这两个页面的transform属性发生变化。touchmove的过程可以写成数学表达式:x代表手指的滑动距离,s代表页面的滑动距离,sideLength为当前滑动边的长度,如果是沿y轴滑动,它是页面的高度。这里写的很像现在流行的“数据驱动”的概念。我们要实现的只是一个render函数,输入是用户的交互数据,输出是页面表现。手指离开屏幕后,当手指离开屏幕时,我们已经知道了滑动的结果(向上还是向下?翻页还是回弹?),我们要实现的只是动画效果,我们有两种选择:方案一:复用touchmove的render逻辑,使用requestAnimationFrame根据手指滑动的快慢来控制动画;方案二:使用css3过渡动画;方案一的好处是可以在手指滑动和动画的过程中使用同一个render函数,尽量减少代码的复杂度,逻辑统一;同时可以精确控制动画的每一帧,动画曲线会更平滑。缺点是可能的性能问题。方案二正好与方案一相反,其实说到底还是js动画vscss动画的问题。动画性能实验为了比较两种方案在H5翻页动画上的表现,我们举一个稍微复杂一点的例子:H5:百度无人车招聘的H5动画:从第1页到第2页CPU:6*slowdownbrowser:Chrome61.0.3163.100(64-bit)jsanimationscheme:clickherecssanimationscheme:clickherejs翻页动画方案,Profile结果css翻页动画方案,Profile结果我们通过实验可以看到,js期间动画过程中,帧率大多维持在30fps左右。而css动画基本都是60fps左右。而且在动画过程中,很明显js动画卡住了。这种情况在一些CPU和显卡配置比较低的安卓机型上尤为明显。对这道题感兴趣的同学可以看看swiper库的raf分支,也就是本次对比测试用到的js。因此,js动画方案虽然看起来更“优雅”,但可以利用“数据驱动”的理念统一解决滑动过程和动画过程的问题。实际上,性能上存在瓶颈。我们只能使用CSS动画方案来保证手指离开屏幕后的性能。正应了那句话“能用css做什么,千万不要用js来解决”。实施方案下图形象地展示了我们实施的基本思路。只有两个页面:currentPage:当前页面activePage:要翻到的下一页。其余页面在初始化时加载到DOM结构中,但显示为none,z-index全为0。为了更形象的展示,这里展示了“堆叠”的状态。SwiperSchematic为了方便访问页面,我们使用双向链表来保存页面结构。每个页面都有prev和next分别指向上一页和下一页。我们需要关注的是如何确定activePage?即,要转到的下一页。答案很简单。其实当用户开始触屏滑动时,可以判断:滑动距离x<0表示页面向上滑动,此时activepage=currentPage.next滑动距离x>0表示页面在向上移动向下滑动,此时activepage=currentPage.prev扩展翻页效果我们例子中的翻页效果是最常见的滑动效果。如何扩展对立方体、翻转等效果的支持?大家可以回头看看“手指离开屏幕之前”那一节,我们提出了s=f(x),x是用户滑动的距离,s是页面滑动的距离。让我们将s扩展为“页面翻转角度”或“页面缩放比例”以支持其他效果。其实我们在滑动的时候,就是利用了css3本身的transform属性,通过translate、rotate、scale的适当组合,可以创造出千变万化的翻页效果。这里比较讨喜的动画是指animation-timing-function,以最简单的滑动效果为例。如果是线性函数,则用户的滑动速度永远等于页面滑动速度。更流畅、更灵敏的“感觉”应该是:一开始页面滑动速度大于用户的滑动速度,随着页面的翻动,两者趋于一致。到某一点后,单位时间内页面滑动的速度开始逐渐小于用户的滑动速度,该速度用距离表示,可以得到x和s的关系如下:x之间的关系和s(水平轴是x,垂直轴是s)。这里,我们不得不提到两种动画方案:js动画和css动画。js动画方案的一个优点是可以精确控制动画的进度,而css则不行。比如用户在x=0.8的时候离开屏幕,因为使用了同一个render,js可以知道手指离开屏幕的时候x在0.8的位置,通过requestAnimationFrame完成下一个动画,而整个过程顺利而完整。css动画是不同的。css动画只是在动画开始前设置了animation-timing-function。当用户在x=0.8离开屏幕时,原来的js控件滑动过程被打断,剩下的动画由css完成。CSS不能根据手指离开屏幕的瞬间动态计算animation-timing-function,所以在连接点,两者的速度不匹配,会影响整体的动画效果。但不幸的是,js动画方案存在性能问题。我们只能对用户手指离开屏幕后的部分采用css动画方案。这种“更赏心悦目的动画”也只能在手指滑动时使用。总结与展望本文描述了在开发一个“丝滑”的H5翻页库过程中遇到的一些问题及相应的解决方案。基本的滑动翻页模型建立后,重点放在性能问题上,分为两个阶段:手指离开屏幕前和手指离开屏幕后。前一阶段主要侧重于减少DOM操作。后期着重于动画的表现,对比了js动画和css动画的性能数据,最终得出手指离开屏幕后使用css动画的结论。另外,基于“数据驱动”的思路,我们扩展了翻页效果和动画功能两部分,增强了翻页库的功能,丰富了H5的展示效果。