偷懒写个微信小程序刷新和加载更多组件

1,699 阅读7分钟

背景

小程序业务往往离不开相当多的下拉刷新和上拉加载更多。如果需要实现一个下拉刷新和上拉加载功能,我们往往需要写非常多重复的代码,例如loading控制,page,pageSize控制还有reload等控制等等一大堆的重复逻辑。特别是一个界面中存在多个列表时,这会使开发者特别的头痛。而小程序如果要实现下拉刷新、上拉加载有两种方式:(文献参考了coolui-scroller,下面组件实现也是借助了此UI组件)

  1. 页面级的(非本文重点):利用页面 Page 里提供的方法。下拉虽说是那个东西但是它只有下拉三个点的动画效果而且只能显示在头部就很尴尬。很多时候一个列表的头部往往会有一些组件比如搜索、分类导航等等。所以往往列表都是局部的非页面级的。这时候下拉时动画出现在最顶部就显得很突兀。
Page({
  onPullDownRefresh: function () {
    // 监听用户下拉刷新事件。
  },
  onReachBottom: function () {
    // 监听用户上拉触底事件。
  },
  onPageScroll: function () {
    // 监听用户滑动页面事件。
  },
});
  1. 组件级的:利用 scroll-view。 但是当你打开 scroll-view 官方文档时,映入眼帘的是一列列的参数属性方法。要完全弄懂里面的内容,恐怕你得上手写写,挨个试试里面的参数和方法才行。而对于下拉刷新这个效果文档上有个简易的 demo 可寻。上拉加载也只有 bindscrolltolower 这么个方法和 lower-threshold 阈值。所以要实现起来完全还得靠自己

总结:从代码量上来说,页面级别的组件站在封装的角度来讲没什么好封装的(组件内也不支持原生的onPullDownRefresh等事件)。因此,本文主要以组件级别(也就是基于coolui-scroller)来封装我们的组件。

准备工作

本次封装组件采用了此组件作为UI:coolui-scroller,是一个基于scroll-view封装好的组件。组件内实现了非常多好用的功能,并且内置了非常好看的样式,里面基于基于scroll-view封装好了下拉刷新加载更多等逻辑和UI样式。

image.png

痛点

我们来模拟下实现一个界面级别的加载更多功能和组件级别需要写多少麻烦的代码!

  1. 页面级:
//js
data: {
        //分页的参数
        queryOptions: {
            size: 10,
            current: 0
        },
        //请求回来的数据
        list: [],
        //标识是否加载中
        loading: false,
        //标识总数
        total: -1
    },
    onLoad () {
        this.loadData()
    },
    //请求参数方法
    async loadData () {
        if (this.data.list.length === this.data.total) return
        this.data.queryOptions.current++
        this.setData({
            loading: true
        })
        const { data } = await query(this.data.queryOptions)
        this.setData({
            total: data.total,
            list: this.data.list.concat(data.records),
            loading: false
        })
    },
    //加载更多
    onReachBottom () {
        this.loadData()
    }

//wxml
    <van-divider contentPosition="center" wx:if="{{loading}}">
        <van-loading type="spinner" size="32rpx">加载中...</van-loading>
    </van-divider>
    <van-divider contentPosition="center" wx:if="{{!loading && list.length === total}}">
        我是有底线的
   </van-divider>
  1. 组件级(借助coolui-scroller):
//js
data: {
        loadMoreSetting: {
            status: 'more',
            moreText: '上拉加载更多',
            loadingText: '加载中...',
            noMoreText: '-- 我是有底线的 --',
            color: '#999',
        },
        list: [],
        total: -1,
        queryOptions: {
            current: 0,
            size: 10
        },
        refreshSetting: {
            // isAutoTriggered: false,
            shake: false, // 设置是否开启震动
            style: "black", // 设置圆点申诉还是浅色
        },
        reload: true
    },
//wxml
<scroller background="#f5f5f5" class="scroller" enableFlex bind:loadmore="loadMore" bind:refresh="doRefresh" refresh="{{refresh}}">
    <view slot="header">
        <!-- 头部区域,可增加搜索,分类切换等功能 -->
        <slot name="header"></slot>
    </view>
    <refresh slot="refresh" type="default" config="{{refreshSetting}}" />
    <view style="margin-top: 16rpx">
        <slot></slot>
    </view>
    <loadmore slot="loadmore" status="{{loadMoreSetting.status}}" loadingText="{{loadMoreSetting.loadingText}}" noMoreText="{{loadMoreSetting.noMoreText}}" moreText="{{loadMoreSetting.moreText}}" color="{{loadMoreSetting.color}}" />
</scroller>

编写每个列表都需要进行以上的代码编写无疑是非常恶心人的。我们大胆提出设想,能不能封装一个组件代替上面重复的动作呢?例如代替分页参数,代替一些配置参数,这样写起我们的业务起来就清晰快速多了

组件预期效果

组件里面封装了下拉和上拉等UI样式和动作,我们每次编写一个列表,只需要写查询逻辑,和列表项展示;分页参数,UI等都交给组件内部去实现,同时组件提供分页,重新加载查询等操作,满足各种场景下的复杂查询。

组件设计

组件接收一个参数(下面代码中的methods),这个参数为一个函数,组件内部调用这个函数去获取数据,获取的数据包括list和total等参数。由于数据绑定只能传递 JSON 兼容数据。自基础库版本 2.0.9 开始,还可以在数据中包含函数(但这些函数不能在 WXML 中直接调用,只能传递给子组件)。。因此我们想出了曲线救国的方式,这里传入的methods是 { query: Function}

//options
    options: {
        virtualHost: true,
        multipleSlots: true // 在组件定义时的选项中启用多slot支持
    },
//组件接受参数
    properties: {
        //是否开启下拉刷新
        refresh: {
            type: Boolean,
            value: true
        },
        //这个传入的是一个对象,对象包含query属性,这个query是一个方法,用于组件内部请求数据用(properties貌似不能传一个函数 曲线救国{ query: Function })
        methods: {
            type: Object
        }
    },
    
//组件data
    data: {
        loadMoreSetting: {
            status: 'more',
            moreText: '上拉加载更多',
            loadingText: '加载中...',
            noMoreText: '-- 我是有底线的 --',
            color: '#999',
        },
        list: [],
        total: -1,
        queryOptions: {
            current: 0,
            size: 10
        },
        refreshSetting: {
            // isAutoTriggered: false,
            shake: false, // 设置是否开启震动
            style: "black", // 设置圆点申诉还是浅色
        },
        //是否重新刷新
        reload: true
    },
    
//组件methods
        //加载更多方法
        async loadMore () {
            const loadMoreSetting = this.data.loadMoreSetting
            if (loadMoreSetting.status == 'loading' || this.data.total === this.data.list.length) return

            this.setData({
                'loadMoreSetting.status': 'loading'
            })
            let query = this.data.methods.query;
            this.data.queryOptions.current++;
            //如果是reload 需要清空外面的数组数据
            const { total, data } = await query(this.data.queryOptions, this.data.reload);
            //重置
            this.data.reload = false;
            this.data.total = total;
            this.data.list = this.data.list.concat(data);
            if (this.data.total === this.data.list.length) {
                this.setData({
                    'loadMoreSetting.status': 'noMore'
                })
            } else {
                this.setData({
                    'loadMoreSetting.status': 'more'
                })
            }
        },
        //重置请求参数
        reload () {
            this.data.queryOptions = {
                current: 0,
                size: 10
            }
            this.data.total = -1;
            this.data.list.length = 0;
            this.data.reload = true;
            this.loadMore()
        },
        //下拉刷新
        doRefresh () {
            this.reload()
        }
        
//wxml
<scroller background="#f5f5f5" class="scroller" enableFlex bind:loadmore="loadMore" bind:refresh="doRefresh" refresh="{{refresh}}">
    <view slot="header">
        <!-- 头部区域,可增加搜索,分类切换等功能 -->
        <slot name="header"></slot>
    </view>
    <refresh slot="refresh" type="default" config="{{refreshSetting}}" />
    <view style="margin-top: 16rpx">
        //这里是内容区插槽,也就是列表项显示内容
        <slot></slot>
    </view>
    <loadmore slot="loadmore" status="{{loadMoreSetting.status}}" loadingText="{{loadMoreSetting.loadingText}}" noMoreText="{{loadMoreSetting.noMoreText}}" moreText="{{loadMoreSetting.moreText}}" color="{{loadMoreSetting.color}}" />
</scroller>

组件使用

组件内部调用传入的methods.query,methods.query绑定的是页面中一个methods,方法有两个参数(ctx,reload),可以在ctx中获取到当前的current(page)和size(pageSize);并且可以根据ctx中的参数去扩展自己的高级查询

//js
    data: {
        //数据绑定 传个组件 { query: Function }
        methods: null,
    },
    onLoad: function (options) {
        //由于小程序不能在定义data的时候访问this,这里需要在生命周期中再绑定methods
        this.setData({
            methods: {
                query: this.loadUnRead
            }
        })
    },
    //请求方法
    async loadUnRead (ctx, reload) {
        //ctx { current:0, size:10 } 这里是组件内部的分页参数
        const { data } = await query(Object.assign({}, ctx, { params: 0 }))//这里扩展自己的查询参数
        //是否重新加载
        if (reload) {
            this.data.unRead.length = 0
        }
        this.setData({
            unRead: this.data.unRead.concat(data.records)
        })
        //需要return data和total给到组件内部,组件内部需要做分页处理
        return {
            data: data.records,
            total: data.total
        }
    },
    
//wxml
        <scroll-list id="unRead" methods="{{unReadMethods}}">
            //这里是插槽的内容
            <view class="item" wx:for="{{unRead}}" wx:key="index" data-id="{{item.id}}" bindtap="jump">
                <van-image round width="70rpx" height="70rpx" src="https://img.yzcdn.cn/vant/cat.jpeg" />
                <view class="item-right">
                    <view class="title">{{item.title}}</view>
                    <view class="desc ellipsis-2">{{item.content}}</view>
                </view>
            </view>
        </scroll-list>

组件弊端

  1. 小程序中不支持在data定义中访问this。由于我们需要给组件传一个函数,这个函数是页面实例的一个方法(例如上面提到的loadUnRead方法,定义在页面实例中,这样可以在方法里面setData,也可以获取组件内部的params处理分页高级查询等),但是小程序不能像vue一样直接在data中访问到this,因此只能在组件生命周期函数中设置好参数再传入到组件内部。
    onLoad: function (options) {
        //由于小程序不能在定义data的时候访问this,这里需要在生命周期中再绑定methods
        this.setData({
            methods: {
                query: this.loadUnRead
            }
        })
    },

2:数据绑定只能传递 JSON 兼容数据,不能直接绑定函数。自基础库版本 2.0.9 开始,还可以在数据中包含函数(但这些函数不能在 WXML 中直接调用,只能传递给子组件)。因此这里曲线救国:

    //这里包裹了一个对象就可以绑定函数进去了
    this.setData({
            unReadMethods: {
                query: this.loadUnRead
            }
        })

3:小程序不支持作用域插槽(深度吐槽微信小程序,人家阿里小程序都支持了) 如果支持作用于插槽,可以直接在组件外部访问到data或者item属性,组件外部甚至都不用去维护list数组,直接负责在模板上显示内容就可以。针对这个情况,目前这里采用在组件外部同时维护一份list数据

    async loadUnRead (ctx, reload) {
        const { data } = await query(Object.assign({}, ctx, { alreadyRead: 0 }))
        if (reload) {
            this.data.unRead.length = 0
        }
        //因为不支持作用域插槽,所以这里设置数据绑定到wxml中显示
        this.setData({
            unRead: this.data.unRead.concat(data.records)
        })
        return {
            data: data.records,
            total: data.total
        }
    },

920e8c34775bfed3df9434a748a2bae.png

关于组件自适应高度

一个列表的高度,往往都不是设置一个固定的值的。这里主要是通过flex布局flex: 1来解决高度自适应问题。参考:virtualHost 虚拟化组件节点

效果

GIF 2022-5-19 18-49-49.gif

最后

如果觉得不错可以给作者点个赞。如果哪里写得有问题,欢迎留言提出!