本文是深入浅出 ahooks 源码系列文章的第十六篇,该系列已整顿成文档-地址。感觉还不错,给个 star 反对一下哈,Thanks。

列表页常见元素

对于一些后盾管理系统,典型的列表页包含筛选表单项、Table表格、Pagination分页这三局部。

针对应用 Antd 的零碎,在 ahooks 中次要是通过 useAntdTable 和 usePagination 这两个 hook 来封装。

usePagination

usePagination 基于 useRequest 实现,封装了常见的分页逻辑。

首先通过 useRequest 解决申请,service 约定返回的数据结构为 { total: number, list: Item[] }

其中 useRequest 的 defaultParams 参数第一个参数为 { current: number, pageSize: number }。并依据申请的参数以及返回的 total 值,得出总的页数。

还有 refreshDeps 变动,会重置 current 到第一页「changeCurrent(1)」,并从新发动申请,个别你能够把 pagination 依赖的条件放这里。

const usePagination = <TData extends Data, TParams extends Params>(  service: Service<TData, TParams>,  options: PaginationOptions<TData, TParams> = {},) => {  const { defaultPageSize = 10, ...rest } = options;  // service 返回的数据结构为 { total: number, list: Item[] }  const result = useRequest(service, {    // service 的第一个参数为 { current: number, pageSize: number }    defaultParams: [{ current: 1, pageSize: defaultPageSize }],    // refreshDeps 变动,会重置 current 到第一页,并从新发动申请,个别你能够把 pagination 依赖的条件放这里    refreshDepsAction: () => {      // eslint-disable-next-line @typescript-eslint/no-use-before-define      changeCurrent(1);    },    ...rest,  });    // 取到相干的申请参数  const { current = 1, pageSize = defaultPageSize } = result.params[0] || {};  // 获取申请后果,total 代表数据总条数  const total = result.data?.total || 0;  // 获取到总的页数  const totalPage = useMemo(() => Math.ceil(total / pageSize), [pageSize, total]);}

重点看下 onChange 办法:

  • 入参别离为当前页数以及以后每一页的最大数量。
  • 依据 total 算出总页数。
  • 获取到所有的参数,执行申请逻辑。
  • 当批改当前页或者以后每一页的最大数量的时候,间接调用 onChange 办法。
// c,代表 current page// p,代表 page sizeconst onChange = (c: number, p: number) => {  let toCurrent = c <= 0 ? 1 : c;  const toPageSize = p <= 0 ? 1 : p;  // 依据 total 算出总页数  const tempTotalPage = Math.ceil(total / toPageSize);  // 如果此时总页面小于以后页面,须要将以后页面赋值为总页数  if (toCurrent > tempTotalPage) {    toCurrent = Math.max(1, tempTotalPage);  }  const [oldPaginationParams = {}, ...restParams] = result.params || [];  // 从新执行申请  result.run(    // 注意参数变动,次要是当前页数和每页的总数量发生变化    {      ...oldPaginationParams,      current: toCurrent,      pageSize: toPageSize,    },    ...restParams,  );};const changeCurrent = (c: number) => {  onChange(c, pageSize);};const changePageSize = (p: number) => {  onChange(current, p);};

最初返回申请的后果以及 pagination 字段,蕴含所有分页信息。另外还有操作分页的函数。

return {  ...result,  // 会额定返回 pagination 字段,蕴含所有分页信息,及操作分页的函数。  pagination: {    current,    pageSize,    total,    totalPage,    onChange: useMemoizedFn(onChange),    changeCurrent: useMemoizedFn(changeCurrent),    changePageSize: useMemoizedFn(changePageSize),  },} as PaginationResult<TData, TParams>;

小结:usePagination 默认用法与 useRequest 统一,但外部封装了分页申请相干的逻辑。返回的后果多返回一个 pagination 参数,蕴含所有分页信息,及操作分页的函数。

毛病就是对 API 申请参数有所限度,比方入参构造必须为 { current: number, pageSize: number },返回后果为 { total: number, list: Item[] }

useAntdTable

useAntdTable 基于 useRequest 实现,封装了罕用的 Ant Design Form 与 Ant Design Table 联动逻辑,并且同时反对 antd v3 和 v4。

首先调用 usePagination 解决分页的逻辑。

const useAntdTable = <TData extends Data, TParams extends Params>(  service: Service<TData, TParams>,  options: AntdTableOptions<TData, TParams> = {},) => {  const {    // form 实例    form,    // 默认表单选项    defaultType = 'simple',    // 默认参数,第一项为分页数据,第二项为表单数据。[pagination, formData]    defaultParams,    manual = false,    // refreshDeps 变动,会重置 current 到第一页,并从新发动申请。    refreshDeps = [],    ready = true,    ...rest  } = options;  // 对分页的逻辑进行解决  // 分页也是对 useRequest 的再封装  const result = usePagination<TData, TParams>(service, {    manual: true,    ...rest,  });  // ...}

而后解决列表页筛选 Form 表单的逻辑,这里反对 Antd v3 和 Antd v4 版本。

// 判断是否为 Antd 的第四版本const isAntdV4 = !!form?.getInternalHooks;

获取以后表单值,form.getFieldsValue 或者 form.getFieldInstance

// 获取以后的 from 值const getActivetFieldValues = () => {  if (!form) {    return {};  }  // antd 4  if (isAntdV4) {    return form.getFieldsValue(null, () => true);  }  // antd 3  const allFieldsValue = form.getFieldsValue();  const activeFieldsValue = {};  Object.keys(allFieldsValue).forEach((key: string) => {    if (form.getFieldInstance ? form.getFieldInstance(key) : true) {      activeFieldsValue[key] = allFieldsValue[key];    }  });  return activeFieldsValue;};

校验表单逻辑 form.validateFields:

// 校验逻辑const validateFields = (): Promise<Record<string, any>> => {  if (!form) {    return Promise.resolve({});  }  const activeFieldsValue = getActivetFieldValues();  const fields = Object.keys(activeFieldsValue);  // antd 4  // validateFields 间接调用  if (isAntdV4) {    return (form.validateFields as Antd4ValidateFields)(fields);  }  // antd 3  return new Promise((resolve, reject) => {    form.validateFields(fields, (errors, values) => {      if (errors) {        reject(errors);      } else {        resolve(values);      }    });  });};

重置表单 form.setFieldsValue

// 重置表单const restoreForm = () => {  if (!form) {    return;  }  // antd v4  if (isAntdV4) {    return form.setFieldsValue(allFormDataRef.current);  }  // antd v3  const activeFieldsValue = {};  Object.keys(allFormDataRef.current).forEach((key) => {    if (form.getFieldInstance ? form.getFieldInstance(key) : true) {      activeFieldsValue[key] = allFormDataRef.current[key];    }  });  form.setFieldsValue(activeFieldsValue);};

批改表单类型,反对 'simple''advance'。初始化的表单数据能够填写 simple 和 advance 全量的表单数据,开发者能够依据以后激活的类型来设置表单数据。批改 type 的时候会重置 form 表单数据。

const changeType = () => {  // 获取以后表单值  const activeFieldsValue = getActivetFieldValues();  // 批改表单值  allFormDataRef.current = {    ...allFormDataRef.current,    ...activeFieldsValue,  };  // 设置表单类型  setType((t) => (t === 'simple' ? 'advance' : 'simple'));};// 批改 type,则重置 form 表单数据useUpdateEffect(() => {  if (!ready) {    return;  }  restoreForm();}, [type]);

_submit 办法:对 form 表单校验后,依据以后 form 表单数据、分页等筛选条件进行对表格数据搜寻。

const _submit = (initPagination?: TParams[0]) => {  setTimeout(() => {    // 先进行校验    validateFields()      .then((values = {}) => {        // 分页的逻辑        const pagination = initPagination || {          pageSize: options.defaultPageSize || 10,          ...(params?.[0] || {}),          current: 1,        };        // 如果没有 form,则间接依据分页的逻辑进行申请        if (!form) {          // @ts-ignore          run(pagination);          return;        }        // 获取到以后所有 form 的 Data 参数        // record all form data        allFormDataRef.current = {          ...allFormDataRef.current,          ...values,        };        // @ts-ignore        run(pagination, values, {          allFormData: allFormDataRef.current,          type,        });      })      .catch((err) => err);  });};

另外当表格触发 onChange 办法的时候,也会进行申请:

// Table 组件的 onChange 事件const onTableChange = (pagination: any, filters: any, sorter: any) => {  const [oldPaginationParams, ...restParams] = params || [];  run(    // @ts-ignore    {      ...oldPaginationParams,      current: pagination.current,      pageSize: pagination.pageSize,      filters,      sorter,    },    ...restParams,  );};

初始化的时候,会依据以后是否有缓存的数据,有则依据缓存的数据执行申请逻辑。否则,通过 manualready 判断是否须要进行重置表单后执行申请逻辑。

// 初始化逻辑// inituseEffect(() => {  // if has cache, use cached params. ignore manual and ready.  // params.length > 0,则阐明有缓存  if (params.length > 0) {    // 应用缓存的数据    allFormDataRef.current = cacheFormTableData?.allFormData || {};    // 重置表单后执行申请    restoreForm();    // @ts-ignore    run(...params);    return;  }  // 非手动并且曾经 ready,则执行 _submit  if (!manual && ready) {    allFormDataRef.current = defaultParams?.[1] || {};    restoreForm();    _submit(defaultParams?.[0]);  }}, []);

最初,将申请返回的数据通过 dataSource、 pagination、loading 透传回给到 Table 组件,实现 Table 的数据以及状态的展现。以及将对 Form 表单的一些操作方法裸露给开发者。

return {  ...result,  // Table 组件须要的数据,间接透传给 Table 组件即可  tableProps: {    dataSource: result.data?.list || defaultDataSourceRef.current,    loading: result.loading,    onChange: useMemoizedFn(onTableChange),    pagination: {      current: result.pagination.current,      pageSize: result.pagination.pageSize,      total: result.pagination.total,    },  },  search: {    // 提交表单    submit: useMemoizedFn(submit),    // 以后表单类型, simple | advance    type,    // 切换表单类型    changeType: useMemoizedFn(changeType),    // 重置以后表单    reset: useMemoizedFn(reset),  },} as AntdTableResult<TData, TParams>;

本文已收录到集体博客中,欢送关注~