我们一般会使用ScrollView、InfiniteScroll、PushLoad等组件来展示列表页面的数据。
但对于一个列表页面来说,仅仅是在列表页面有数据的状态下,才使用这些组件。
列表页面还有其它一些状态。
列表页面状态划分
init- 初始状态noData- 无数据状态error- 出错状态list- 展示列表状态refresh- 刷新状态
init 状态
刚进入列表页面的状态,一般是正在发起第一页数据的请求。
此时页面应该呈现的是一个数据加载的效果(空白页面上一个大大的loading)。
noData 状态
查询第一页,返回没有数据,此状态也可以划分到error状态下。
此时页面应该反馈用户暂无数据,并引导用户去添加数据,或去其它地方发现数据(比如当前筛选下没有数据,可以试试全部)。
error 状态
查询接口失败或用户不满足条件的一个状态。
可能是服务器报错或用户网络问题。
如果是服务器报错,可以提示用户进行反馈或稍后再试;若是用户的网络问题,可以引导用户去设置网络。
list 状态
使用ScrollView等组件展示列表数据。
如果数据没有全部请求完毕,则提示用户上滑加载更多。
refresh 状态
当页面有筛选条件或排序时,需要重新请求第一页。
与init状态的区别是,需要保持当前页面状态(如list状态),然后进入到另一个状态(noData/error/list)。并且如果页面状态是由list状态进入list状态,则需要重置组件的滚动高度。
列表展示组件
我们使用ScrollView之类的列表展示组件,来展示列表数据和进行分页数据请求触发。
列表展示组件状态划分(无下拉刷新效果)
loadMore- 上滑加载更多loading- 加载中finished/noMore- 全部加载完成failed/error- 加载失败
loadMore 上滑加载更多
当组件展示了第一页数据后,若数据还没有全部请求完毕,则可以提示用户上滑加载更多,用户进行上滑后,触发请求下一页数据。
只要用户数据还没有全部请求完成,就可以提示用户上滑加载更多,引导用户操作触发请求下一页数据。
loading 加载中
用户上滑后(达到触发下一页请求的条件),则组件状态变为加载中,这个状态也是唯一的组件自身可以感知的状态。
finished/noMore 全部加载完成
当所有数据都请求完成时,反馈给用户没有更多数据了。
此时可以在下方引导用户去添加更多数据或者去其它地方查找数据(类似暂无数据的引导效果)。
failed/error 加载失败
用户根据引导上滑,请求下一页数据时加载失败。
可能的原因是服务器报错或用户本地网络错误,如果是服务器原因,可以提示用户再次尝试或反馈问题;若是用户网络问题则可以引导用户去设置网络。
设置组件的状态
组件的这些状态中,组件唯一可以感知的状态就是loading,其它几个状态需要我们根据接口的查询情况去手动设置。
通过load事件设置组件状态
一般组件会提供load事件(或类似事件),用来通知进行下一页请求,该方法需返回一个布尔值,用来标志全部数据是否请求完毕。
load事件触发时,此时组件已感知并进入到loading状态,当load事件执行完成后,根据返回值,组件进入loadMore或finished状态。
若load事件执行报错,则组件进入到failed状态。
使用ref手动修改组件内部状态
若页面可以进行筛选、排序等操作时,一个load事件显然无法满足。
如,此时页面数据全部加载完成,用户再进行了排序操作(并非通过上滑触发),那么不会触发load事件。
此时需要手动更改组件的状态。给组件指定ref,然后调用组件内部修改状态的方法(优先)或直接修改状态值。
复杂列表页面
上面说过,复杂的列表页面有诸如筛选条件、排序,需要手动修改组件内部状态。
筛选组件、排序组件与列表组件不会总是在一个父组件中,筛选组件或排序组件无法直接拿到列表组件的ref来修改组件的状态。
一种方式是将事件或方法放在共有的祖先组件中,然后层层传递,但这种方式真的很不方便,也不利于组件的调整。
期望一种更加灵活方便的方式,可以让我们在任意地方(如组件/store/utils)中去通知列表页面自己来更改组件状态。
为列表页面建立事件总线(eventBus)
const ListPageBus = {
// 所有的页面列表组件,如果一个页面有多个listPage组件,则需要使用key值进行区分管理
comps = {},
// 监听事件
listen(comp, event, handler) {
// 如果页面上只有一个pageList组件,无需设置key
const { key = '' } = comp.$vnode;
let compSaved = this.comps[key];
// 组件没有被保存过或者组件被新建了
if (!compSaved || comp !== compSaved) {
this.comps[key] = { comp, events: {} };
// 组件销毁时需要解绑监听的事件
comp.$once('hook:beforeDestroy', () => {
// 不能移除新的同名key的组件的消息
if (comp === this.comps[key].comp) {
delete this.comps[key];
}
});
}
const { events } = this.comps[key];
let eventHandlers = events[event];
if (!eventHandlers) {
eventHandlers = events[event] = [];
}
// handler可以是一个函数或一个函数数组
if (!Array.isArray(handler)) {
handler = [handler];
}
// 防止相同的handler
handler = handler.filter(h => !events[event].includes(h));
// 订阅消息
eventHandlers.push(...handler);
},
listenMany(comp, events) {
Object.keys(events).forEach(event => {
this.listen(comp, event, events[event]);
});
},
// 消息通知
dispatch(event, payload, key = '') {
// 通过key拿到对应的组件和消息
const opt = this.comps[key];
if (!opt) return;
const { comp, events } = opt;
(events[event] || []).forEach(handler => {
// 执行用户事件
try {
handler.call(comp, payload);
} catch (e) {
console.error(`[ListPageBus] 消息执行错误:`, e);
}
});
}
};