likes
comments
collection
share

列表页面的总结与思考二

作者站长头像
站长
· 阅读数 12
  1. 列表页面的几种状态
  2. 列表展示组件(ScrollView等)的几种状态
  3. 与列表展示组件的通信(修改列表展示组件的状态)。

接下来总结一下列表页面都有哪些内容。

列表页面组件

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;
    // 如果提供对应状态的插槽内容,优先展示
    let slot = this.$slots[renderStatus];
    // 如果没有提供list插槽,则将default插槽渲染为list
    if (!slot && renderStatus === PageStatus.List)
      slot = this.$slots.default;
    }
    if (slot) return slot;
    
    // 次之,调用对应状态的render方法
    const renderMethod = `render${upperFirst(renderStatus)}`;
    // 若提供了对应的渲染状态方法,则返回调用结果
    if (this[renderMethod]) return this[renderMethod]();
    
    // 这里可以做一个兜底,渲染默认的状态组件
  },
  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;
    }
  }
};

上面定义了一个基础的列表页面组件。

在其内部管理了列表页面状态(pageStatus)和列表展示组件状态(componentStatus),并根据列表页面状态渲染内容。

接下来需要扩展默认的状态展示(可选),使用mixins。

// 无数据时展示,提供renderNoData方法
const NoData = {
  methods: {
    renderNoData() {
      // 根据具体的实际情况封装组件
      return <div>暂无数据</div>;
    }
  }
};

// 页面错误时展示,提供renderError方法
const Error = {
  methods: {
    renderError() {
      // 可以对this.pageError进行具体处理
      return <div>加载失败,请稍后再试</div>
    }
  }
};

// 初始化页面的展示 提供renderInit方法
const Init = {
  methods: {
    renderInit() {
      return <Loading />;
    }
  }
};

// 使用ScrollView展示列表数据 提供renderList方法
const ListByScrollView = {
  props: {
    // 数据
    list: {
      type: Array,
      default() {
        return [];
      }
    },
  },
  methods: {
    renderList() {
      return (
        <ScrollView ref="component" onLoad={}>
          {this.list.map(item => <Item item={item} />)}
        </ScrollView>
      );
    },
  }
};

现在拼装ListPage组件。

const ListPage = {
  mixins: [ListPageBase, ListByScrollView, NoData, Error, Init]
};

将各个状态渲染分散在不同的组件中,并通过mixins来进行自由组合。

因为不同的列表页面可能对每个状态的处理差别较大。

添加消息订阅功能(上一篇提到的消息管理)

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;
        if (error) {
          this.setPageStatus(PageStatus.Error, error);
        } else {
          // 默认状态为list
          pageStatus = pageStatus || PageStatus.List;
          this.setPageStatus(pageStatus);
          if (pageStatus === PageStatus.List) {
            this.setComponentStatus(componentStatus || ComponentStatus.LoadMore);
          }
        }
      }
    }
  }
};

列表页面的Refresh状态

列表页面进入refresh状态时,还需要延续上一个状态的组件,页面上总体效果是上一状态的停留/等待(不能同init状态一样展示空白)。

一般除了展示上一状态的组件,还需要展示正在刷新的效果。

一种方式是提供renderRefresh()方法,然后在该方法中去渲染上一列表页面状态,并添加额外内容。

const Refresh = {
  methods: {
    renderRefresh() {
      const prevRenderMethod = `render${firstUpper(this.prevPageStatus)}`
      const vnode = this[prevRenderMethod]?.();
      return (
        <div>
          { vnode }
          <Toast type="loading" />
        </div>
      );
    }
  }
};

也可以不提供renderRefresh方法,使用watch监听pageStatus变化。

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?.();
      }
    }
  }
};

当PageList组件状态Refresh过后,需要重置列表组件容器的滚动高度(不建议使用document或window作为列表的滚动容器)。

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

ListPageBasesetPageStatus方法中处理:

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中提供getScroller方法:

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传入的。

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>
  );
},

需要指出的是,onLoad事件触发后需要将componentStatus设置为ComponentStatus.Loading

情况二,通过调用列表展示组件内部设置状态的方法或直接修改状态

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组件灵活定义

使用自定义的作用域插槽。

renderList() {
  return (
    <ScrollView ref="component" onLoad={}>
      {this.list.map((...args) => this.$scopedSlots.item(args))}
    </ScrollView>
  );
},

当使用列表页面组件时:

<ListPage :list="list">
  <template #item="[item, index]">
    <!-- 可以在这里完全自定义每一条数据的展示 -->
  </template>
</ListPage>