共计 4513 个字符,预计需要花费 12 分钟才能阅读完成。
项目中遇到 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),不继续加载
具体加载流程为:
-
page:触发 onReachBottom 事件,修改 pagination 组件
key
值 -
component:
key
值监听到变化,触发加载事件_loadMoreData
-
component:
_loadMoreData
中判断满足条件后,触发加载列表函数this.triggerEvent('getList')
,并传入页面参数 page 和 size。 - page:向 model 层获取数据。
-
page:获取数据后,传递给 pagination 组件
list
和total
值。 -
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。