Teach you how to implement a perfect mobile waterfall component

background

Waterfall is a scenario that everyone often encounters in the daily development process, and some solutions are also provided in our company's internal component library. However, these schemes are applicable to a single scenario, and each implementation scheme has more or less problems. Based on this, we designed and developed a waterfall component that is compatible with multiple scenarios.

At present, there are three layout methods used to display the product flow in Zhuanzhuan: card flow, fixed waterfall flow, and staggered waterfall flow.

The flow of cards is presented in the form of a drop-down list. This layout allows users to focus on a single list item, making it easier to read. Mainly used in the second-level list page entry of Zhuanzhuan, the effect is as follows

card flow

The size and height of the fixed waterfall image area remain unchanged. A uniform height will make the entire interface look neat and not visually cluttered. Mainly used in some channel page scenarios, the effect is as follows

stationary waterfall

The visual performance of the staggered waterfall flow is a multi-column layout with elements of equal width and variable height. The home page of Zhuanzhuan and the business recommendation page will choose to carry it in this way.

staggered waterfall

Problems with existing programs

In the above three scenarios, the image height of the first and second scenarios is fixed, and the implementation is relatively simple. You can directly use the infinite loading List component. The third scenario that often goes wrong is the staggered waterfall . In this scenario, after the image is loaded, the height of the image needs to be obtained, and then added to the lowest column of the waterfall, otherwise the calculation of the lowest column will be affected, resulting in columns of different lengths.

Zhuanzhuan company mainly has the following solutions for the realization of staggered waterfall flow

  • Option 1: Use the left and right two-column layout, first divide the waterfall flow data on the first page on the left and right and render it. When the data of the second page is rendered, the first data of the second page will be taken out and rendered to the lowest column, and IntersectionObservemonitored, until the element appears in the viewport, and then the second data will be taken from the data source and added to the In the new minimum rendering column, the waterfall flow of lazy loading is realized in this way

    • Advantages: The lazy loading IntersectionObserveof , and the logic implementation is simple
    • shortcoming:
      1. The column layout only supports two columns, and does not support parameter configuration of multiple columns;
      2. The data on the first page does not conform to the specification of waterfall flow, and there is a possibility that one column is long and the other is short;
      3. IntersectionObservecompatibility issues;
      4. The event that the data is loaded is not exposed, so when it cooperates with infinitely loaded components, the problem of pulling down the interface twice is prone to occur.
  • 方案2:采用宽度百分比进行样式布局,首屏渲染就开启IntersectionObserve 监听,元素出现在视窗后,设置一个setTimeout 加载下一个瀑布流元素,同时在该dom上添加一个属性标识,防止二次触发。

    • 优点: 支持参数配置多栏布局,首屏符合瀑布流的规范,同时暴露了瀑布流加载完毕后的事件,配合无限加载时不会出现两次请求接口的问题
    • 缺点:IntersectionObserve 的兼容性问题依旧没有解决;内部DOM查询、操作频率较高;耦合无限加载List的逻辑,维护成本较高;setTimeout 无法保证图片按照正确时序加载,会导致获取最低列时不够准确
  • 方案3:使用绝对定位布局方案。实现原理是在每一个子组件 waterall-item 的内部新建一个 image 对象,监听onload 事件然后触发父组件 waterfall 进行瀑布流的重排。

    • 优点:内部逻辑简单,便于维护的同时也符合瀑布流规范,提供了瀑布流常用的几个配置项,完全加载后也会触发事件通知外部组件
    • 缺点:不支持图片懒加载;重绘次数过多(1+2+...+N),对性能不太友好;触发重绘的时机并不是最精确的时间节点(通过new image后的onload事件触发,而不是在当前image上绑定onload事件)

然后又去网上找了下开源方案,这里列举Github上的start数排行前4的解决方案

  • vue-waterfall:start数最多的一个方案
    • 缺点:需要在组件渲染前知道图片的宽度和高度,而我们一般并不会在接口中返回这些数据
  • vue-waterfall-easy:无需提前获取图片的宽高信息,采用图片预加载后再进行排版。
    • 缺点:耦合下拉、无限加载组件;包含PC端等逻辑,包体积较大,对于追求性能的页面并不友好(作为开源方案,兼容更多的场景其实无可厚非,只是这些功能我们都已经有单独的组件实现);一次加载所有图片,不支持懒加载
  • vue-waterfall2:支持高度自适应,支持懒加载
    • 缺点:内部多次创建image对象,同时还伴随着大量的计算和滚动监听。
  • vue2-waterfall:通过对masonry-layout和imagesloaded这两个开源方案的封装来实现,逻辑简单明了。
    • 缺点:不支持懒加载

用一张图来简单总结下

新瀑布流方案设计

目前并没有一款简单、易用的移动端瀑布流组件,所以打算整合已知方案,再重新实现一个新的瀑布流组件。新的瀑布流会包含以下一些优点:

  • 简单的 CSS 布局
  • 精简逻辑层面的实现
  • 支持高度自适应
  • 支持懒加载

布局方案

了解到瀑布流 CSS 布局方案主要分为三种

  • 绝对定位:上述的方案3以及开源方案 vue-waterfall-easy 采用这种布局,比较适用于PC端瀑布流
  • 宽度百分比:上述方案2以及开源方案 vue-waterfall2 采用这种布局,但这种方案会存在一些精度问题
  • Flex布局:一些大的电商网站像蘑菇街等采用这种布局

其中,Flex布局兼容性、适配都没什么问题,应该是移动端布局方案的最优解。所以新的瀑布流会采用这种布局方案

瀑布流逻辑实现

对于瀑布流的逻辑实现,也分为三种

  • 新建 image 对象,onload 时先获取图片的原始宽高,再根据瀑布流分配的宽度计算出实际渲染的高度,作为内联样式挂载到 DOM
  • 直接在接口返回的图片url中拼接图片的宽高信息,提前布局,蘑菇街等采用这种方案
  • IntersectionObserver 监听图片元素,出现在视图当中开始从瀑布流数据队列的列头中取出一个数据并渲染到当前瀑布流的最低列,如此循环往复实现瀑布流的懒加载

三种方案中,第一种比较常规,大部分开源方案就是这么实现的。但是内部需要进行高度换算,同时也不支持图片懒加载。

第二种方案应该是比较好的一个方案,图片加载前就可以开始进行排版,方便简单,也支持懒加载,用户体验也好。蘑菇街、天猫、京东等都是采用这种方案。但这种场景需要进行一些改造,比如在图片入库前将图片信息拼接到url上,或者后端接口读取图片对象,然后将图片信息返回给前端。要么改造成本较大,要么会增加服务器压力,并不太适合我们业务。

而第三种方案可以在不需要其他改造的情况下支持懒加载,应该是目前最合适的一个方案。所以新的瀑布流组件会采用IntersectionObserver 来实现瀑布流的排版

新瀑布流具体实现

IntersectionObserver兼容性

首先面临的一个问题就是 IntersectionObserver 的兼容性问题。IntersectionObserver 在解决传统的滚动监听带来的性能问题的同时,兼容性一直并没有得到一个主流的支持,可以看到 iOS 上的支持并不完美

官方提供了一个polyfill来解决上述问题,但是这个polyfill体积较大,直接引入对一些追求极致性能的页面不太友好,所以我们采用了动态引入polyfill的方法

// 不支持IntersectionObserver的场景下,动态引入polyfill
const ioPromise = checkIntersectionObserver()
  ? Promise.resolve()
  : import('intersection-observer')

ioPromise.then(() => {
  // do something
})
复制代码

不支持的 IntersectionObserver 的环境才会去加载这个polyfill,其中检测方法摘抄自 Vue lazyload

const inBrowser = typeof window !== 'undefined' && window !== null

function checkIntersectionObserver() {
  if (
    inBrowser &&
    'IntersectionObserver' in window &&
    'IntersectionObserverEntry' in window &&
    'intersectionRatio' in window.IntersectionObserverEntry.prototype
  ) {
    // Minimal polyfill for Edge 15's lack of `isIntersecting`
    // See: https://github.com/w3c/IntersectionObserver/issues/211
    if (!('isIntersecting' in window.IntersectionObserverEntry.prototype)) {
      Object.defineProperty(window.IntersectionObserverEntry.prototype, 'isIntersecting', {
        get: function() {
          return this.intersectionRatio > 0
        }
      })
    }
    return true
  }
  return false
}
复制代码

瀑布流图片加载时序

图片加载是个异步过程,如何保证瀑布流图片的加载时序呢?

直接在 IntersectionObserver 的回调函数触发后就开始进行下一张瀑布流图片的加载极易出现长短不一列以及页面抖动的情况,因为触发回调时图片可能只加载了一部分。上述方案1和方案2均存在这个问题

查看文档,可以看到 IntersectionObserver 的回调函数中提供的 IntersectionObserverEntry 对象会提供以下属性

  • time:可见性发生变化的时间,是一个高精度时间戳,单位为毫秒
  • target:被观察的目标元素,是一个 DOM 节点对象
  • rootBounds:根元素的矩形区域的信息,getBoundingClientRect() 方法的返回值,如果没有根元素(即直接相对于视口滚动),则返回null
  • boundingClientRect:目标元素的矩形区域的信息
  • intersectionRect:目标元素与视口(或根元素)的交叉区域的信息
  • intersectionRatio:目标元素的可见比例,即 intersectionRectboundingClientRect 的比例,完全可见时为1,完全不可见时小于等于0

我们可以在target上绑定onload事件,onload之后再执行下一次瀑布流数据渲染,这样能保证下一次渲染时获取最低列时的准确性

// 瀑布流布局:取出队列中位于队头的数据并添加到瀑布流高度最小的那一列进行渲染,等图片完全加载后重复该循环
observerObj = new IntersectionObserver(
  (entries) => {
    for (const entry of entries) {
      const { target, isIntersecting } = entry
      if (isIntersecting) {
        if (target.complete) {
          done()
        } else {
          target.onload = target.onerror = done
        }
      }
    }
  }
)
复制代码

IntersectionObserver二次触发问题

我们知道,采用IntersectionObserver监听目标元素,当目标元素的可见性发生变化时,回调函数一般会触发两次。一次是目标元素刚刚进入视口(开始可见),另一次是完全离开视口(开始不可见)。 为了避免第二次再次触发监听逻辑,可以在第一次触发的时候停止观察

if (isIntersecting) {
  const done = () => {
    // 停止观察,防止回拉时二次触发监听逻辑
    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()
  // 首屏采用并行渲染,非首屏采用串行渲染
  if (++count < this.firstPageCount) {
    this.$nextTick(() => this.waterfall())
  } else {
    this.$nextTick(() => this.startObserver())
  }
}
复制代码

  • 方法二:加动画,从视觉感官上消除白屏带来的影响,组件内置了两个动画,通过animation传参即可

懒加载时的白屏问题

We adopt a lazy loading solution: when the image appears in the view, the next waterfall image is loaded, which is more performance-friendly. But in this case, when the user scrolls, if the next image is loaded too slowly, there may be a short white screen time. How to solve this experience problem?

IntersectionObserverThere is a rootMarginproperty that we can use to expand the intersection area and thus load later data ahead of time. This will not only prevent the white screen when the user scrolls to the bottom, but also prevent too much rendering from affecting performance. The default setting is 400px, which is about half-screen data rendered in advance.

// 扩展intersectionRect交叉区域,可以提前加载部分数据,优化用户浏览体验
rootMargin: {
  type: String,
  default: '0px 0px 400px 0px'
}
复制代码

How to work with infinite loading components

Generally, for the convenience of maintenance, we separate the logic of infinite loading and waterfall flow, so when the waterfall flow data is rendered, we need to notify external components, otherwise it is easy to trigger the infinite loading logic before the waterfall flow is rendered. The problem of sending interface requests twice.

A judgment can be added in the process of waterfall rendering. If there is no data in the queue, the external infinite loading component will be notified to make the next request.

const done = () => {
  if (this.innerData.length) {
    this.waterfall()
  } else {
    this.$emit('rendered')
  }
}
复制代码

Summary & source code

The above are some of the problems encountered when making a new waterfall component and the corresponding solutions. Of course, there is still room for optimization of this solution, and it is currently in use as a component block within the company. The code is not open source yet. Those who need the source code can follow the official account 大转转FEand reply 瀑布流to get the source code. For the waterfall component, if you have better opinions and suggestions, welcome to communicate and discuss.

Guess you like

Origin juejin.im/post/7086330043038695432