ProTable 请求封装:明确协议适配与错误边界

ProTable 请求封装:明确协议适配与错误边界

Ant Design ProTable 的 request 约定很简单:接收分页、表单、排序和筛选参数,返回 datasuccesstotal。真正值得封装的是项目后端协议的适配,而不是在 Hook 内再造一套缓存或吞掉所有错误。

先定义稳定的边界

下面不从 Ant Design 的内部文件路径导入 SortOrder,而是只依赖 ProTable 所需的结构。这样可以降低组件库目录调整带来的升级成本。

import { useCallback } from 'react'

type SortValue = 'ascend' | 'descend' | null
type Sorter = Record<string, SortValue>
type Filter = Record<
  string,
  Array<string | number | boolean> | null
>

type TableParams = {
  current?: number
  pageSize?: number
  [key: string]: unknown
}

type ProTableResult<T> = {
  data: T[]
  success: boolean
  total: number
}

type ProTableRequest<T, P extends TableParams> = (
  params: P,
  sort: Sorter,
  filter: Filter
) => Promise<ProTableResult<T>>

interface PageResult<T> {
  records: T[]
  total: number
}

interface UseProTableRequestOptions<T, P extends TableParams> {
  request: (
    params: P,
    sort: Sorter,
    filter: Filter
  ) => Promise<PageResult<T>>
  onError?: (error: unknown) => void
}

export function useProTableRequest<
  T,
  P extends TableParams = TableParams
>(
  options: UseProTableRequestOptions<T, P>
): ProTableRequest<T, P> {
  const { request, onError } = options

  return useCallback(
    async (params, sort, filter) => {
      try {
        const result = await request(params, sort, filter)

        return {
          data: result.records,
          total: result.total,
          success: true
        }
      } catch (error) {
        onError?.(error)

        return {
          data: [],
          total: 0,
          success: false
        }
      }
    },
    [request, onError]
  )
}

这个 Hook 只做两件事:

  1. 把项目接口的 records/total 转成 ProTable 的返回结构。
  2. 统一失败结果,并把错误交给页面或监控处理。

使用示例

import { useCallback } from 'react'
import { ProTable } from '@ant-design/pro-components'
import { message } from 'antd'

export function UserTable() {
  const queryUsers = useCallback(
    async (
      params: TableParams,
      sort: Sorter,
      filter: Filter
    ) => {
      const response = await userService.page({
        page: params.current ?? 1,
        pageSize: params.pageSize ?? 20,
        keyword: params.keyword,
        sort,
        filter
      })

      return {
        records: response.records,
        total: response.total
      }
    },
    []
  )

  const request = useProTableRequest({
    request: queryUsers,
    onError: () => {
      message.error('用户列表加载失败')
    }
  })

  return (
    <ProTable<User>
      rowKey="id"
      columns={columns}
      request={request}
      pagination={{ defaultPageSize: 20 }}
    />
  )
}

传入 Hook 的 requestonError 应保持稳定。若直接在 JSX 每次创建新函数,useCallback 无法带来稳定引用,表格还可能因依赖变化产生额外请求。

排序参数需要显式翻译

前端通常使用 ascend/descend,后端可能使用 asc/desc,字段名也未必相同。不要把整个 sort 对象原样透传给服务端:

function toOrderBy(sort: Sorter) {
  const allowedFields = new Set(['createdAt', 'name'])

  return Object.entries(sort)
    .filter(
      (entry): entry is [string, 'ascend' | 'descend'] =>
        allowedFields.has(entry[0]) &&
        entry[1] !== null
    )
    .map(([field, order]) => ({
      field,
      direction: order === 'ascend' ? 'asc' : 'desc'
    }))
}

字段白名单不仅是类型问题,也能避免把任意客户端字段拼进数据库排序语句。

不建议放进这个 Hook 的能力

  • 请求缓存: ProTable 自己管理请求触发;跨页面缓存应由数据层负责。
  • 自动重置页码: 表单提交、重置和 actionRef.current?.reloadAndRest?.() 的语义应由页面明确选择。
  • 静默成功: 捕获异常后仍返回 success: true 会让空数据和失败无法区分。
  • 私有类型导入: antd/es/... 下的实现路径不是稳定的公共契约。
  • 业务参数拼装: 门店、租户和权限范围应在业务请求函数中清晰出现。

ProTable 官方文档:ProTable。升级组件库时,应以项目锁定版本对应的 API 为准。

最后更新于

ihopeful Blog 由博主亲笔撰写,重要信息可放心引用。