前面已经总结过列表页面具有的几种状态和展示列表的组件(ScrollView
等)具有的几种状态,以及负责列表页面如何与列表展示组件通信(修改列表展示组件的状态)。
接下来总结一下列表页面组件。
列表页面组件
jsx
import { upperFirst } from 'lodash';
const PageStatus = {
Init: 'init', // 初始状态
NoData: 'noData', // 无数据
Error: 'error', // 接口请求失败
List: 'list', // 请求到数据,使用列表组件进行展示
Refresh: 'refresh' // 重新请求第一页,需保留页面的前一个状态
};
const ComponentStatus = {
LoadMore: 'loadMore', // 上滑加载更多
Loading: 'loading', // 加载中
Finished: 'finished', // 列表数据全部请求完成
Failed: 'failed', // 本次加载失败
};
// 列表页面组件
const ListPageBase = {
data() {
return {
// 列表页面状态
pageStatus: '',
// 页面上一个状态,refresh状态需要展示页面的上一状态
prevPageStatus: '',
// 页面错误
pageError: null,
// 展示列表的组件状态
componentStatus: ComponentStatus.LoadMore,
};
},
render() {
// 根据pageStatus渲染对应的状态页面
// refresh状态一般需要保持页面之前的状态。
const renderStatus = this.pageStatus === PageStatus.Refresh && !this.renderRefresh ? this.prevPageStatus : this.pageStatus;
const renderMethod = `render${upperFirst(renderStatus)}`;
// 若提供了对应的渲染状态方法,则返回调用结果
if (this[renderMethod]) return this[renderMethod]();
// 次之,判断是否提供slot
let slot = this.$slots[renderStatus];
// 如果没有提供list插槽,则将default插槽渲染为list
if (!slot && renderStatus === PageStatus.List)
slot = this.$slots.default;
}
if (slot) return slot;
// 这里可以做一个兜底,渲染默认的状态组件
},
methods: {
setPageStatus(status, error) {
this.prevPageStatus = this.pageStatus;
this.pageStatus = status;
if (status === PageStatus.Error) {
this.pageError = error;
}
// 内部状态,一般外部不需要知道
// this.$emit('statusChange', status);
},
setComponentStatus(status) {
this.componentStatus = status;
}
}
};
上面是一个基础的列表页面组件。现在给其添加功能,使用mixin。
jsx
// 无数据时展示的组件,提供renderNoData方法
const NoData = {
methods: {
renderNoData() {
// 根据具体的实际情况封装组件
return <div>暂无数据</div>;
}
}
};
// 页面错误时展示的状态组件,提供renderError方法
const Error = {
methods: {
renderError() {
// 根据this.pageError具体处理
return <div>加载失败,请稍后再试</div>
}
}
};
const Init = {
methods: {
renderInit() {
return <Loading />;
}
}
};
// 使用ScrollView展示列表数据
const ListByScrollView = {
props: {
// 数据
list: {
type: Array,
default() {
return [];
}
},
},
methods: {
renderList() {
return (
<ScrollView ref="component" onLoad={}>
{this.list.map(item => <Item item={item} />)}
</ScrollView>
);
},
}
};
现在拼装ListPage
组件。
jsx
const ListPage = {
mixins: [ListPageBase, ListByScrollView, NoData, Error, Init]
};
将各个状态渲染分散在不同的组件中,并通过mixins
来进行自由组合。
添加消息订阅功能(上一篇提到的)
jsx
const ListPageEvent = {
created() {
// 进行消息订阅
// 主要用于列表页面refresh前后的通知
ListPageBus.listenMany(this, {
beforeRefresh: this.onBeforeRefresh,
afterRefresh: this.onAfterRefresh,
});
},
methods: {
onBeforeRefresh(payload) {
// 允许初始化时使用refreshPage事件通知页面状态改为init
this.setPageStatus(this.pageStatus === '' ? PageStatus.Init : PageStatus.Refresh);
},
// 页面刷新后有这几个状态:noData/list/error
// 若页面状态为list,则组件的状态可能是loadMore或finished
onAfterRefresh(payload) {
const type = typeof payload;
// 若payload为布尔类型或undefined,则表示进入list状态,true代表全部加载完成
if (['boolean', 'undefined'].includes(type)) {
this.setPageStatus(PageList.List);
// 设置组件状态
this.setComponentStatus(pageLoad ? ComponentStatus.Finished : ComponentStatus.LoadMore);
}
// 允许值为 noData/error/loadMore(默认)/finished
else if (type === 'string') {
if ([PageStatus.NoData, PageStatus.Error].includes(payload)) {
this.setPageStatus(payload);
} else {
this.setPageStatus(PageStatus.List);
this.setComponentStatus(payload === ComponentStatus.Finished ? payload : ComponentStatus.LoadMore);
}
}
// 对象类型
else {
let { pageStatus, componentStatus, error } = payload;
// 默认状态为list
if (error) {
this.setPageStatus(PageStatus.Error, error);
} else {
pageStatus = pageStatus || PageStatus.List
this.setPageStatus(pageStatus);
if (pageStatus === PageStatus.List) {
this.setComponentStatus(componentStatus || ComponentStatus.LoadMore);
}
}
}
}
}
};
列表页面的Refresh状态
由于列表页面的refresh状态我们还是展示的是上一个状态的组件,此时页面呈现上refresh状态毫无变化。
一种方式是提供renderRefresh()
方法,然后在该方法中去渲染上一列表页面状态,并添加额外内容。
jsx
const Refresh = {
methods: {
renderRefresh() {
const prevRenderMethod = `render${firstUpper(this.prevPageStatus)}`
const vnode = this[prevRenderMethod]?.();
return (
<div>
{ vnode }
<Toast type="loading" />
</div>
);
}
}
};
另一种方式是,使用watch监听pageStatus
变化。
jsx
const Refresh = {
watch: {
pageStatus(status, prevStatus) {
if (status === PageStatus.Refresh) {
// 返回值调用会取消loading加载效果。lock同一时间只能一个loading
this.loading = this.$loading({ lock: true });
}
// 由Refresh状态变化到下一状态
else if (prevStatus === PageStatus.Refresh) {
// 取消loading
this.loading?.();
// 一般刷新状态后需要重置列表页面或组件滚动高度
// 这里需要看具体的列表组件实现,滚动页面或组件容器
this.resetScrollTop?.();
}
}
}
};
当PageList组件状态Refresh过后,需要重置列表组件容器的滚动高度(尽量不要使用document作为列表的滚动容器)。
若不重置滚动高度,刷新后的第一页数据很可能会停留在列表最下方。
在ListPageBase
中setPageStatus
中处理。
jsx
methods: {
setPageStatus(status, error) {
if (this.pageStatus === PageStatus.Refresh) {
this.resetScrollerTop();
}
this.prevPageStatus = this.pageStatus;
this.pageStatus = status;
if (status === PageStatus.Error) {
this.pageError = error;
}
},
resetScrollerTop() {
// 列表组件中返回滚动容器
const scroller = this.getScroller();
if (scroller) {
scroller.scrollTop = 0;
}
}
}
在ListByScrollView
中:
jsx
const ListByScrollView = {
props: {
// 数据
list: {
type: Array,
default() {
return [];
}
},
},
methods: {
renderList() {
return (
<ScrollView ref="component" onLoad={}>
{this.list.map(item => <Item item={item} />)}
</ScrollView>
);
},
getScroller() {
return this.$refs.component.$el;
}
}
};
将设置的组件状态同步到组件中
情况一,组件的状态是通过prop传入的。
jsx
renderList() {
const { componentStatus: status } = this;
return (
<ScrollView
ref="component"
loading={status === ComponentStatus.Loading}
finished={status === ComponentStatus.Finished}
error={status === ComponentStatus.Failed}
onLoad={}
>
{this.list.map(item => <Item item={item} />)}
<template #error>
<LoadFailed />
</template>
</ScrollView>
);
},
情况二,通过调用列表展示组件内部设置状态的方法或直接修改状态
jsx
watch: {
componentStatus(status) {
// 状态值映射
const compStatus = {
[ComponentStatus.LoadMore]: 0,
[ComponentStatus.Loading]: 1,
[ComponentStatus.Finished]: 2,
[ComponentStatus.Failed]: 3,
}[status]
this.$refs.component.setStatus(compStatus);
// 或者直接修改组件内部的状态
// this.$refs.component.status = compStatus;
}
},
methods: {
renderList() {
return (
<ScrollView ref="component" onLoad={}>
{this.list.map(item => <Item item={item} />)}
</ScrollView>
);
},
}
List组件灵活定义
传入自定义的作用域插槽。
js
renderList() {
return (
<VirtualScroll ref="component" onLoad={}>
{this.list.map((item, i) => this.$scopedSlots.item(item, i, this.list))}
</ScrollView>
);
},