小程序分页实践之编写可复用分页组件

项目中遇到 tab切换列表,每个tab都需要分页的需求,分页流程具有相似性,于是想将分页封装为组件,方便应用。

组件的应用已写成一个小demo,效果如下图所示(数据用mock模拟):

源码可以查看:wxapp-pagination

项目需求

具体项目需求:

  • 查看自己相关的会议(页面命名为 meetings)
  • tab切换,分为:

    • “我的会议”(我参加的会议,后面会以 "join" 为 key区分)
    • “我的预约”(我预约的会议,后面会以 "book" 为 key区分)

  • 一次加载10条(size=10),拉到底部后,加载下一页(page = page +1)

当然,作为前端,要考虑性能方面的需求:

  • 首次只加载默认tab页的首页,其他tab等到点击到对应tab才开始加载。
  • 回到已加载过的tab页,保留原数据不重新加载。

所以原型图大概就长这样:

逻辑实现

与分页逻辑相关的项目结构如下:

├── components

│ ├── meeting-item # 列表item

│ └── pagination # 分页组件

├── model

│ └── user.js # 我的相关 model

└── pages

│ └── user # 我的相关页面

│ ├── meetings # 我的会议(就是tab要分页的页面啦)

│ └── ...

└── vant-weapp

还是用图理一下他们之间的关系吧:

在组件内监听触发分页事件

触发分页的事件是滚动到页面的底部,小程序中,触发该事件是Page页面的onReachBottom事件,但是这个事件只能在Page中触发,所以要将这个触发时机传递给pagination组件。

解决思路是:组件 pagination 内,设置key属性,每当onReachBottom事件触发之后,设置组件属性 key  为一个随机字符串,当组件 pagination 监听到key的变化的时候,做出分页操作。

// components/pagination/index.js

Component({

properties: {

key: {

type: String,

observer: '_loadMoreData' // _loadMoreData 为分页操作

}

}

})

<!-- pages/user/meetings/index.wxml -->

<tabs active="{{currentTab}}" bind:change="onChange">

<tab title="我的会议" data-key="{{type['JOIN']}}">

<view class="meeting-list">

<pagination

name="JOIN"

key="{{joinKey}}"

>

</pagination>

</view>

</tab>

<tab title="我的预约">

<view class="meeting-list">

<pagination

name="BOOK"

key="{{bookKey}}"

>

</pagination>

</view>

</tab>

</tabs>

Page({

onReachBottom(){

const key = scene[+this.data.currentTab].key // 对应tab对应key

this.setData({

[key]: random(16)

})

}

})

分页组件的实现逻辑

触发到达底部之后,需要加载数据。但再加载之前,先满足加载的条件:

  • 上一页还未加载完成(loading = true),不重复加载
  • 当前页面全部加载完(ended = true),不继续加载

具体加载流程为:

  1. page:触发 onReachBottom 事件,修改 pagination组件 key 值
  2. component: key值监听到变化,触发加载事件 _loadMoreData
  3. component: _loadMoreData 中判断满足条件后,触发加载列表函数 this.triggerEvent('getList'),并传入页面参数page 和 size。
  4. page:向model层获取数据。
  5. page:获取数据后,传递给 pagination组件list和total 值。
  6. component:list 监听到变化,判断是否加载完成。

component

// components/pagination/index.js

Component({

properties: {

name: String,

key: {

type: String,

observer: '_loadMoreData' // _loadMoreData 为分页操作

},

size: { // 每次加载条目数

type: Number,

value: 10

},

total: Number, // 页面总数

list: { // 已加载条目

type: Array,

observer: '_endState' // 每次加载完新数据,判断数据是否全部加载完成

}

},

data: {

page: 0, // 当前第几页

loading: false, // 是否正在加载

ended: false // 数据是否已全部加载完成

},

methods: {

_loadMoreData(){

const { loading, ended, size } = this.data

if (loading) return // 上一页还未加载完成,不加载

if (ended) return // 当前页面全部加载完,不加载

const page = this.data.page + 1

this.setData({

loading: true, // 开始加载新页面loading设置为true

page

})

// 触发加载下一页,并传入参数

this.triggerEvent('getList', {

page,

size

})

},

_endState(val, oldVal) {

const { total, list } = this.properties

let ended = false

// 判断数据是否全部加载完成

if (list.length >= total) {

ended = true

}

this.setData({

loading: false, // 当前页面加载完成,loading设置为false

ended

})

}

}

})

page

<!-- pages/user/meetings/index.wxml -->

<pagination

name="BOOK"

key="{{bookKey}}"

bind:getList="getBookMeetings"

list="{{bookMeetings}}"

total="{{bookTotal}}"

>

</pagination>

循环列表item

pagination组件获取了可循环的列表,初始想法是循环slot,但是在slot中却获取不到 item 对象:

<view wx:for="{{list}}" wx:for-item="item" wx:key="index">

<slot></slot>

</view>

解决的办法是将每个列表项封装为组件,循环抽象节点,这样对其他页面的分页具有可拓展性。

componentGenerics 字段中声明:

// components/pagination/index.json

{

"componentGenerics": {

"selectable": true

},

// ...

}

使用抽象节点:

<!-- components/pagination/index.wxml -->

<view wx:for="{{list}}" wx:for-item="item" wx:key="index">

<selectable item="{{item}}"></selectable>

</view>

指定“selectable”具体是哪个组件:

<!-- pages/user/meetings/index.wxml -->

<pagination

generic:selectable="meeting-item"

// ... 其他属性

>

</pagination>

对应 json 文件定义对应 usingComponents :

// pages/user/meetings/index.json

{

"usingComponents": {

"pagination":"/components/pagination/index",

"meeting-item":"/components/meeting-item/index"

}

}

meeting-item 组件接收一个属性 item,这样在 meeting-item 组件中,就可以获取到循环列表的item值,并正常绘制页面。

实现切换懒加载

给pagination添加initImmediately属性,当initImmediately为true时,首次加载页面,并用变量 initState标记是否已经初始化页面过。

// components/pagination/index.js

Component({

properties: {

initImmediately: {

type: Boolean,

value: true,

observer: function(val){

if (val && !this.data.initState) {

this.initData()

}

}

},

// ...

},

data: {

initState: false, // 是否已经加载过

// ...

},

lifetimes: {

attached: function () {

if (this.data.initImmediately){

this.initData()

}

},

},

methods: {

initData(){

console.info(`${this.data.name}:start init data`)

this._loadMoreData()

this.setData({

initState: true

})

},

// ...

_endState(val, oldVal) {

if (!this.data.initState) return

// ...

},

}

})

当currentTab为当前类型的时候,initImmediately 设置为 true。

<!-- pages/user/meetings/index.wxml -->

<pagination

name="JOIN"

init-immediately="{{currentTab==type['JOIN']}}"

// ...

>

</pagination>

<pagination

name="BOOK"

init-immediately="{{currentTab==type['BOOK']}}"

// ...

>

</pagination>

组件的复用

完成了以上组件,在对其他分页的页面,可以直接复用。比如,实现一个获取全部用户列表的分页,只需要新增一个user-item的组件,像这样调用:

<pagination

name="USER"

key="{{key}}"

bind:getList="getUserList"

list="{{userList}}"

total="{{userTotal}}"

generic:selectable="user-item"

>

</pagination>

具体应用可以查看我写的小demo:wxapp-pagination。

以上是 小程序分页实践之编写可复用分页组件 的全部内容, 来源链接: utcz.com/z/318481.html

回到顶部