列表页面的总结与思考二

前面已经总结过列表页面具有的几种状态和展示列表的组件(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作为列表的滚动容器)。

若不重置滚动高度,刷新后的第一页数据很可能会停留在列表最下方。

ListPageBasesetPageStatus中处理。

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>
  );
},
相关推荐
你挚爱的强哥35 分钟前
✅✅✅【Vue.js】sd.js基于jQuery Ajax最新原生完整版for凯哥API版本
javascript·vue.js·jquery
y先森1 小时前
CSS3中的伸缩盒模型(弹性盒子、弹性布局)之伸缩容器、伸缩项目、主轴方向、主轴换行方式、复合属性flex-flow
前端·css·css3
前端Hardy1 小时前
纯HTML&CSS实现3D旋转地球
前端·javascript·css·3d·html
susu10830189111 小时前
vue3中父div设置display flex,2个子div重叠
前端·javascript·vue.js
IT女孩儿2 小时前
CSS查缺补漏(补充上一条)
前端·css
吃杠碰小鸡3 小时前
commitlint校验git提交信息
前端
天天进步20154 小时前
Vue+Springboot用Websocket实现协同编辑
vue.js·spring boot·websocket
虾球xz4 小时前
游戏引擎学习第20天
前端·学习·游戏引擎
我爱李星璇4 小时前
HTML常用表格与标签
前端·html
疯狂的沙粒4 小时前
如何在Vue项目中应用TypeScript?应该注意那些点?
前端·vue.js·typescript