Skip to content

pagination

chuan6 edited this page Aug 11, 2018 · 3 revisions

分页接口

teambition-web 应用目前的后端主要使用了三种分页机制:

  1. url 携带查询参数 page - 指定页码(从 1 开始),和 count - 指定页长(每一页包含的条目数,最后一页除外);从后端获得对应的条目列表。
  2. url 携带查询参数 pageToken - 提示后端上次分页请求的完成状态(即后端接口在上一页请求返回结果中携带的 nextPageToken 值;在第一页的请求里,该值设为 ''),和 pageSize - 指定当前请求要拿的条目数上限;从后端获得一个对象,包含字段: result - 条目列表,nextPageToken - 下一页请求需要携带的 pageToken 参数,和 totalSize - 目标结果集当前整体的条目数量(这个字段并不总是存在,因为有些场景后端无法低成本得到全集大小)。
  3. 与上一种类似,但将分页相关的元信息放到 HTTP headers 里;前端的第一次请求与非分页请求没有差别,不需要携带额外信息;随后,如果要发起额外的分页请求,则在 request headers 里带上上一次后端在 response headers 里携带的 x-custom-pageTokenx-custom-size 字段。

teambition-sdk 目前提供的 第一版 分页接口只支持上边 第二种,但在设计时考虑了随后对另外两种(特别是第三种)的支持。

目前应用代码在对接后端接口时的痛点

需要把分页操作自有的状态在应用层面手动管理。无论是在 redux store 节点多一个 pagination 字段维护这些状态并添加对应的 reducers,还是在 react 组件内部添加 state 和 setState,掺杂在本就复杂的业务逻辑里。

解决办法

在能方便使用 Observable 的地方,通过一个自定义的操作符来将分页相关状态管理封装起来,外部完全不需要关心,只需要从操作符拿出结果就可以直接使用。

在一些依然是通过一次回调调用,触发一次分页操作的地方,提供状态变量的接管,不需要记得在每次请求之后更新状态变量。

关键接口

一个类型定义: Page.State<T>

用于表达符合 teambition-sdk 需要的分页状态,类型参数 T 代表结果元素的类型,如成员列表相关的分页接口对应的 T 就很可能是 MemberSchema。目前设计中管理的字段有:

  • urlPath: string - 当前分页请求对应的 url 的 path 部分,左右不含 /,可以直接用于 SDKFetch 的接口;
  • pageSize: number - 一页要包含的最大条目数;
  • urlQuery: {} - 当前分页请求对应的 url 的 query 部分,不需要包含 nextPageToken 或 pageSize,如搜索任务数据时,会有 { type: 'task', q: '...' }
  • nextPageToken: PageToken - 从上一页的后端返回得到的信息,用于下一页请求;
  • totalSize?: number - 后端在最近的一次返回中告知的结果集整体条目数(可能是 undefined);
  • result: T[] - 实际的结果;
  • hasMore: boolean - 通过后端在最近的一次返回中携带的信息,推算得,是否还存在更多条目可以进一步通过分页获取;
  • nextPage: number - 下一页的页码。

一个纯函数: Page.defaultState<T>()

用于生成一个可用的初始化状态 Page.State<T>。参数列表为:

  • urlPath: string - 对应生成的 Page.State<T> 里的 urlPath 字段;
  • options
    • pageSize: number - 对应生成的 Page.State<T> 里的 pageSize 字段;
    • urlQuery: {} - 对应生成的 Page.State<T> 里的 urlQuery 字段。

生成的 Page.State<T> 的其他字段还有如下初始值:

  • nextPageToken: '' - 后端定义第一个请求携带的 pageToken 是空字符串;
  • totalSize: undefined - 该字段完全由后端判定,在未发起第一次请求之前,未知;
  • result: [] - 因为一页都还没载入,结果集为空;
  • hasMore: true - hasMore 为 false 会导致在某些情况下不发请求,所以设为 true,方便发起第一次请求;
  • nextPage: 1 - 在未完成第一次请求时,下一页,就是第一页。

一个 SDKFetch 方法: expandPage<T>()

用于按需发起请求,帮助用户管理分页状态(每次请求得到的结果集接在当前 result: T[] 的尾部,hasMore: boolean 更新等等),并推出最新的分页状态 Observable<Page.State<T>>

在能方便使用 Observable 的场景里,推荐使用 sdkFetch.expandPage(initialState, { loadMore$ }),如在 epic 里:

const firstPage$ = actions$.ofType('REQUEST_MEMBERS')
const restPages$ = acitons$.ofType('REQUEST_MEMBERS_LOADMORE')

return firstPage$.switchMap(() => {
  const initialState = Page.defaultState<MemberSchema>(
    'organization/members',
    { pageSize: 50 }
  )
  return sdkFetch.expandPage(initialState, { loadMore$: restPages$ })
    .map(({ result, hasMore }) => actions.requestMembersSuccess({
      members: result,
      hasMoreMembers: hasMore
    }))
    .catch((error) => actions.requestMembersFailure({ error: error.message }))
})

在完成第一次请求之后,会在每次 loadMore$ 流有推出时,进行下一页的请求。

在使用回调,通过回调的一次次调用出发一次次分页请求的场景里,推荐使用 sdkFetch.expandPage(state, { mutate: true }),如在 react 组件里:

class C extends React.PureComponent {
  private pageState: Page.State<GroupSchema> | undefined

  componentDidMount() {
    this.pageState = Page.defaultState<GroupSchema>(
      'organization/groups',
      { pageSize: 50 }
    )
  }

  private loadGroupsInPages = () => {
    sdkFetch.expandPage(this.pageState, { mutate: true })
      .take(1)
      .subscribe(({ result, hasMore }) => {
        this.setState({ groups: result, hasMoreGroups: hasMore })
      })
  }

  ...

  render() {
    const { groups, hasMoreGroups } = this.state
    return (
      <>
        <Groups groups={ groups } />
        { hasMoreGroups ? <DDDot onEnter={ this.loadGroupsInPages } /> : <Dot /> }
      </>
    )
  }
}

这里,通过设置 { mutate: true },每次对 sdkFetch.expandPage 的调用在请求完成后都会更新 this.pageState 的内部状态。

这个函数还有不少其他 options,详情参考具体定义