小程序瀑布流组件

git仓库地址:https://github.com/kapeter/mpMasonry

前段时间,接到一个需求,要在小程序中实现不定高度的瀑布流布局。我首先去万能的百度上搜索了一波,确实有很多方案,但都是固定高度的,这和需求不符。于是决定自己写一个,考虑到后面也会有类似的需求,干脆做成一个通用组件,方便使用。

瀑布流是比较流行的一种网站页面布局,尤其在mobile端经常被用来展示信息流。前段时间,接到一个需求,要在小程序中实现不定高度的瀑布流布局。我首先去万能的百度上搜索了一波,确实有很多方案,但都是固定高度的,这和需求不符。于是决定自己写一个,考虑到后面也会有类似的需求,干脆做成一个通用组件,方便使用。

本套方案主要使用flex模型,结合小程序的特性(boundingClientRect、抽象节点),实现瀑布流布局和组件化。

组件布局

由于手机宽度的限制,一般移动端的瀑布流只有两列,不需要考虑多列的情况,因此我们的布局完全可以通过CSS3的flex模型完成。

<!-- masonry.wxml -->
<view class="masonry-list">
    <view class="masonry-list-left" style="{{ 'margin-right:' + intervalWidth }}">
        <view id="left-col-inner">
            <block wx:for="{{items}}" wx:key="{{item.id}}">
                <masonry-item wx:if="{{item.columnPosition === 'left'}}" item="{{item}}"></masonry-item>
            </block>
        </view>
    </view>
    <view class="masonry-list-right">
        <view id="right-col-inner">
            <block wx:for="{{items}}" wx:key="{{item.id}}">
                <masonry-item wx:if="{{item.columnPosition === 'right'}}" item="{{item}}"></masonry-item>
            </block>
        </view>
    </view>
</view>


// masonry.wxss
.masonry-list {
    width: 100%;
    display: flex;
    box-sizing: border-box;
}

.masonry-list-left, .masonry-list-right {
    flex: 1;
}

flex为1,给予左右两列相同的宽度,通过设置properties中的intervalWidth来控制两者间的间距。

有人可能会奇怪,为什么要在class="masonry-list-left"的view之后再加一个层级?同一层级会怎么样呢?

来看两个写法的对比图,我们统一给左边加上高度150px,右边高度设为auto。 flex的高度补全很明显,在同一层级情况下,右边高度也变成了150px,与左边一致,这会导致我们后面获取两边高度的时候拿到一样的数值,就无法判断该把元素放在哪边。因此,我们要多增加一个层级。

出现这种情况的原因是:flex的column会进行高度补全,和父容器保持一致。

渲染函数

基本布局已经完成,接下来就是要让布局“流”起来。

先来看一下传统瀑布流的原理:先通过计算出一排能够容纳几列元素,然后寻找各列之中所有元素高度之和的最小者,并将新的元素添加到该列上,如此循环下去,直至所有元素均能够按要求排列为止。

根据上述原理,渲染流程如下:

渲染流程

/**
 * 渲染函数
 * 
 * @param {Array} items  - 正在渲染的数组
 * @param {Number} i  - 当前渲染元素的下标
 * @param {Function} onComplete - 完成后的回调函数
 */
_render (items, i, onComplete) {
    if (items.length > i && !this.data.stopMasonry) {
        this.columnNodes.boundingClientRect().exec(arr => {
            const item = items[i]
            const rects = arr[0]

            const leftColHeight = rects[0].height
            const rightColHeight = rects[1].height

            this.setData({
                items: [...this.data.items, {
                ...item,
                columnPosition: leftColHeight <= rightColHeight ? 'left' : 'right'
                }]
            }, () => {
                this._render(items, ++i, onComplete)
            })
        })
    } else {
        onComplete && onComplete()
    }
}

为了满足item高度是动态的场景,需要将渲染函数设置为递归函数。以下是渲染函数的执行流程:

  • 判断下标,如果递归结束,调用完成回调函数(onComplete),函数结束,反之执行下面流程;
  • 通过boundingClientRect()调取两边的高度;
  • 比较两边高度,将结果赋给columnPosition字段;
  • 调用setData()将新的元素渲染到dom上,新的元素位置基于columnPosition字段的值;
  • 渲染完成后,在setData()的回调函数中执行下一层递归,确保下一次boundingClientRect()能获取到最新的高度。
  • setData()为异步渲染,详细说明请见小程序指南-双线程下的界面渲染
  • 由于每渲染一个元素,需要执行一次boundingClientRect()setData(),渲染时间较长。
  • exec()返回的是按请求次序构成的结果数组,即使只执行了一次请求,结果也位于res[0]而不是res。
  • boundingClientRect的详细用法可查看小程序文档-WXML节点信息API

刷新函数

有了核心的渲染函数,我们还要进行一些处理。

/**
 * 刷新瀑布流
 * 
 * @param {Array} items - 参与渲染的元素数组 
 */
_refresh(items) {
  const query = wx.createSelectorQuery().in(this)
  this.columnNodes = query.selectAll('#left-col-inner, #right-col-inner')

  return new Promise((resolve, reject) => {
    this._render(items, 0, () => {
      resolve()
    })
  })
}

_refresh函数包括两部分:

  • 获取左右两列的WXML节点(这一步放在渲染函数中,会重复获取,影响性能)
  • 返回一个Promise对象,将_render函数包起来,并在_render的完成回调函数中触发resolve(),这样就能在渲染结束后执行其他操作。

使用抽象节点剥离业务逻辑

当前存在一个问题,masonry-item组件是用来承载元素的业务逻辑,如果项目存在多处需要瀑布流,并且业务逻辑不一样,那就需要修改masonry组件,添加判断条件,这就产生了耦合,不符合通用组件的规范。因此,我们需要进行解耦。

这里需要用到“抽象节点”。以下是定义:自定义组件模版中的一些节点,其对应的自定义组件不是由自定义组件本身确定的,而是自定义组件的调用者确定的。这时可以把这个节点声明为“抽象节点”。

简单来说,就是在masonry组件内部定义抽象节点masonry-item,这个节点可以代表任何组件,只有当页面调用masonry组件时,这个组件才被确定,这样就能将业务逻辑组件剥离出来了。

具体实现很简单,在masonry组件声明抽象节点。

// masonry.json
"componentGenerics": {
    "masonry-item": true
}

在页面调用时,指定该抽象节点为哪个组件。

<!-- index.wxml -->
<!-- 指定抽象节点为img-box组件 -->
<masonry generic:masonry-item="img-box""></masonry>

注意点:节点的 generic 引用 generic:xxx="yyy" 中,值 yyy 只能是静态值,不能包含数据绑定。因而抽象节点特性并不适用于动态决定节点名的场景。

如何在页面中使用组件

1、将components目录下中masonry文件夹复制到自己项目中。

2、添加业务组件,并在业务组件中添加property,用于承载数据

// property名必须为item
properties: {
    item: { 
        type: Object
    }
}

3、引入masonry组件和所需的业务组件

// index.json
"usingComponents": {
    "masonry": "../../components/masonry/masonry",
    "img-box": "../../components/img-box/img-box"
}

4、在wxml加入masonry节点

<!-- index.wxml -->
<masonry generic:masonry-item="img-box" id="masonry" interval-width="20rpx"></masonry>

generic:masonry-item用于指定业务组件,interval-width为左右两列空隙宽度。

5、调用函数,渲染瀑布流

_doStartMasonry(items) {
    // 通过ID,获取组件实例
    this.masonryListComponent = this.selectComponent('#masonry');
    // 调用组件的start函数,渲染瀑布流
    this.masonryListComponent.start(items).then(() => {
        console.log('render completed')
    })
}

为保证页面显示效果,建议一次渲染不超过100个元素。

方法列表

函数名 函数功能 参数说明 返回值
append 批量添加元素 {Array} items - 新增的元素数组 Promise
delete 批量删除瀑布流中的元素 {Number} start - 开始下标
{Number} end - 结束下标
deleteItem 删除瀑布流中的某个元素 {Number} index - 数组下标
start 启动组件,开始渲染瀑布流 {Array} items - 参与渲染的元素数组 Promise
stop 停止渲染瀑布流,清空数据
updateItem 更新渲染数组中的某个元素 {Object} newItem - 修改后的元素
{Number} index - 需要更新的数组下标

实践案例

京东种草

原文https://www.kapeter.com/post/64

猜你喜欢

转载自blog.csdn.net/sinat_17775997/article/details/84066175