Vue中分页和无限加载自动切换的实现

这段时间写Pixiv C的时候突发奇想,想做一个在PC端是分页、在移动端上是无限加载瀑布流的响应式布局。

PC端的分页用瀑布流组件限定Item的数量,Item设置成固定宽高,模拟表格,在移动端则按正常瀑布流的状态实现。

布局方面很好做,CSS3下用@media screen and (orientation: portrait)就可以做树屏状态下的响应式。

但是这个瀑布流本身,它的行为却成了一个难题。

需求

我预期的效果如下:

  • 在PC端只显示分页表格的布局
  • 在移动端,只要是横屏,仍然保持分页布局,从竖屏转为横屏,布局自动转换
  • 两个不同形态的瀑布流共享同一个状态、同一个数据源,分页的加载和无限滚动的加载统一化(使用同样的接口)。

性能方面的目标主要是横屏切竖屏、竖屏切横屏都不能卡顿。

在功能方面,从瀑布流切换到分页后,分页需要定位到当前的瀑布流最后一次加载的页数。

为什么瀑布流和分页要共享一个状态呢,因为不想重复加载数据,这个点完全可以靠前端做好优化,只对后端每一页的数据进行一次性的请求。

而且瀑布流的状态切换到分页后也不能丢失,切换回瀑布流的时候我们还是希望看到所有页的数据,而不是只有当前页,然后重新加载。共用状态这个点实际上已经是原始需求里分析抽象出来的一个新需求。

分析

其实无限滚动的加载本质上也就是加载分页内容,只是它把内容拼接到了一起实现了一个无限滚动的效果。由于对接的API本身支持无限滚动,而且是不断循环给出内容,所以我们不需要考虑分页最后一页的问题。

如果涉及到分页存在最后一页,那么分页模式下的体验需要后端返回一个总页数的数值才能做好,这里为了契合后端接口,我们不对分页做最后一页的设置。

对于屏幕横竖方向切换,浏览器提供了对应的属性和事件使用,整个需求理论上都可以实现。

瀑布流的无限加载虽然行为上和翻页是类似的,但是翻页有上一页的操作、瀑布流没有,所以二者不能共用一个方法,而且考虑到我们不能从API重复加载数据,所以上一页的数据要通过切割数组获取。

在这种行为模式下,瀑布流和翻页必然不可能共用一个方法。

实现

实现上,首先分页和无限加载瀑布流一定要固定同样的pageSize,如果pageSize发生变化会带来数组切割上的问题。

对于所有后端返回的数据,我们一律放到统一的数组里,通过判断当前不同的模式,返回不同的数据。

定义下面这样一个computed方法,就能够返回一个根据页数切分出来的数据:

1
2
3
imageSlice() {
return this.relatedImages.slice((this.relatedPage - 1 + this.pageOffset) * 6, (this.relatedPage + this.pageOffset) * 6);
}

之所以使用computed而不是在data里另外做一个变量来存用于显示的数组,是因为这样会造成,如果你从分页切换到瀑布流,那么一定会经过类似下面的过程:

1
this.displayImages = this.images;

这个操作对于瀑布流来说是不友好的,至少在vue-mansory下,它会造成瀑布流的重新渲染,而如果我们返回的只是基于数据数组的切片或者它本身,在无限加载瀑布流的状态下,程序并不会触发重新渲染。

对于屏幕监听状态的变化,在window下绑定事件即可。我没有直接读取window.orientation的值,而是在data内对这个值做了一个暂存,主要有两个原因。

第一是在template内初次渲染的过程中,window这个对象是不存在的,会报undefined错误,第二是这样可以避免window.orientation高频改变时我们可能会得到预期之外的结果。

初始值:

1
2
screenOrientation: window.orientation,
showPart: window.orientation !== 0,

注意,这里特别定义了一个showPart来做模式切换的开关,showPart本质上可以不这么定义,使用computed即可,但是我们需要控制流程,这么做更方便于流程上的控制。

进行下面的事件绑定,可以实现对屏幕转动的监听:

1
window.addEventListener('orientationchange', this.handleScreenRotate, false);

handleScreenRotate方法的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
handleScreenRotate(){
if (this.screenOrientation === 0 && window.orientation !== 0) {
// 切割显示的数组
this.$nextTick(() => {
this.$refs.related.reset();
this.showPart = true;
})
} else if (this.screenOrientation !== 0 && window.orientation === 0) {
this.$nextTick(() => {
this.$refs.related.reset();
this.showPart = false;
})
}
this.screenOrientation = window.orientation;
}

是的,这里面有一个微妙的地方。我们需要先对瀑布流执行一次reset,然后再执行瀑布流渲染模式的切换。

这是针对vue-mansory的一个Trick,vue-mansory的item可以是一个普通DOM,也可以是一个Vue组件,如果是Vue组件,那么在瀑布流内元素达到一个数量(差不多>=30)的时候,如果对瀑布流进行这种大幅度的更新,会有差不多1秒左右的卡顿。

根据Profile的分析,Vue对瀑布流内组件的更新和mansory本身的重绘产生了一些奇怪的冲突,导致Vue每更新其中一个组件,mansory就会重绘一次,造成整个切换过程有很多不必要的重绘,卡顿严重。

所以我们需要简单粗暴地先对mansory下的所有内容做清空,这样Vue不会在我们更新瀑布流内容的时候去挨个更新每一个内部组件,造成很多不必要的重绘,而是Vue重新构建瀑布流内的内容,

这个reset方法的实现也很简单:

1
2
3
reset() {
this.$refs.waterfall.$el.innerHTML = '';
}

我们直接在DOM上把瀑布流内的内容全部销毁即可,这样可以避免程序重复执行不必要的重绘。

对于共用一个分页源的切换,我采用了一个pageOffset实现,点击上一页,pageOffset就-1,点击后一页加1,取值最大为0。这个东西的好处在于如果切换到了瀑布流,再切换回分页,我们可以很好地控制分页的状态。

其次,在加载数据的时候我们是以currentPage(本案例的代码中是this.relatedPage)作为依据加载数据的,上一页操作不能对这个值产生影响。

总结

把上面的分析和实现做一个思路上的整理,配合显示、样式上的控制,我们就能够实现一个在不同设备、不同屏幕旋转状态下表现形式不一样的瀑布流。

Live Demo: https://pixivc.pwp.app/pic/63301084