资讯专栏INFORMATION COLUMN

常用列表页常见hook封装解析

3403771864 / 854人阅读

  我们今天来讲讲关于ahooks 源码,我们目标主要有以下几点:

  深入了解 React hooks。

  明白如何抽象自定义 hooks,且可以构建属于自己的 React hooks 工具库。

  小建议:培养阅读学习源码的习惯,工具库是一个对源码阅读不错的选择。

  列表页常见元素

  后台管理系统中常见典型列表页包括筛选表单项、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 依赖的条件放这里。 

</>复制代码

  1.  const usePagination = <TData extends DataTParams extends Params>(
  2.   service: Service<TData, TParams>,
  3.   options: PaginationOptions<TData, TParams> = {},
  4.   ) => {
  5.   const { defaultPageSize = 10, ...rest } = options;
  6.   // service 返回的数据结构为 { total: number, list: Item[] }
  7.   const result = useRequest(service, {
  8.   // service 的第一个参数为 { current: number, pageSize: number }
  9.   defaultParams[{ current: 1, pageSize: defaultPageSize }],
  10.   // refreshDeps 变化,会重置 current 到第一页,并重新发起请求,一般你可以把 pagination 依赖的条件放这里
  11.   refreshDepsAction: () => {
  12.   // eslint-disable-next-line @typescript-eslint/no-use-before-define
  13.   changeCurrent(1);
  14.   },
  15.   ...rest,
  16.   });
  17.   // 取到相关的请求参数
  18.   const { current = 1, pageSize = defaultPageSize } = result.params[0] || {};
  19.   // 获取请求结果,total 代表数据总条数
  20.   const total = result.data?.total || 0;
  21.   // 获取到总的页数
  22.   const totalPage = useMemo(() => Math.ceil(total / pageSize), [pageSize, total]);
  23.   }

  重点看下 onChange 方法:

  入参分别为当前页数以及当前每一页的最大数量。

  根据 total 算出总页数。

  获取到所有的参数,执行请求逻辑。

  当修改当前页或者当前每一页的最大数量的时候,直接调用 onChange 方法。

</>复制代码

  1.   // c,代表 current page
  2.   // p,代表 page size
  3.   const onChange = (c: number, p: number) => {
  4.   let toCurrent = c <= 0 ? 1 : c;
  5.   const toPageSize = p <= 0 ? 1 : p;
  6.   // 根据 total 算出总页数
  7.   const tempTotalPage = Math.ceil(total / toPageSize);
  8.   // 假如此时总页面小于当前页面,需要将当前页面赋值为总页数
  9.   if (toCurrent > tempTotalPage) {
  10.   toCurrent = Math.max(1, tempTotalPage);
  11.   }
  12.   const [oldPaginationParams = {}, ...restParams] = result.params || [];
  13.   // 重新执行请求
  14.   result.run(
  15.   // 留意参数变化,主要是当前页数和每页的总数量发生变化
  16.   {
  17.   ...oldPaginationParams,
  18.   current: toCurrent,
  19.   pageSize: toPageSize,
  20.   },
  21.   ...restParams,
  22.   );
  23.   };
  24.   const changeCurrent = (c: number) => {
  25.   onChange(c, pageSize);
  26.   };
  27.   const changePageSize = (p: number) => {
  28.   onChange(current, p);
  29.   };

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

</>复制代码

  1.   return {
  2.   ...result,
  3.   // 会额外返回 pagination 字段,包含所有分页信息,及操作分页的函数。
  4.   pagination: {
  5.   current,
  6.   pageSize,
  7.   total,
  8.   totalPage,
  9.   onChange: useMemoizedFn(onChange),
  10.   changeCurrent: useMemoizedFn(changeCurrent),
  11.   changePageSize: useMemoizedFn(changePageSize),
  12.   },
  13.   } 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 处理分页的逻辑。

</>复制代码

  1.   const useAntdTable = &lt;TData extends Data, TParams extends Params&gt;(
  2.   service: Service&lt;TData, TParams&gt;,
  3.   options: AntdTableOptions&lt;TData, TParams&gt; = {},
  4.   ) =&gt; {
  5.   const {
  6.   // form 实例
  7.   form,
  8.   // 默认表单选项
  9.   defaultType = 'simple',
  10.   // 默认参数,第一项为分页数据,第二项为表单数据。[pagination, formData]
  11.   defaultParams,
  12.   manual = false,
  13.   // refreshDeps 变化,会重置 current 到第一页,并重新发起请求。
  14.   refreshDeps = [],
  15.   ready = true,
  16.   ...rest
  17.   } = options;
  18.   // 对分页的逻辑进行处理
  19.   // 分页也是对 useRequest 的再封装
  20.   const result = usePagination&lt;TData, TParams&gt;(service, {
  21.   manual: true,
  22.   ...rest,
  23.   });
  24.   // ...
  25.   }

  然后处理列表页筛选 Form 表单的逻辑,这里支持 Antd v3 和 Antd v4 版本。

</>复制代码

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

  获取当前表单值,form.getFieldsValue或者form.getFieldInstance: 

</>复制代码

  1.  // 获取当前的 from 值
  2.   const getActivetFieldValues = () => {
  3.   if (!form) {
  4.   return {};
  5.   }
  6.   // antd 4
  7.   if (isAntdV4) {
  8.   return form.getFieldsValue(null() => true);
  9.   }
  10.   // antd 3
  11.   const allFieldsValue = form.getFieldsValue();
  12.   const activeFieldsValue = {};
  13.   Object.keys(allFieldsValue).forEach((key: string) => {
  14.   if (form.getFieldInstance ? form.getFieldInstance(key) : true) {
  15.   activeFieldsValue[key] = allFieldsValue[key];
  16.   }
  17.   });
  18.   return activeFieldsValue;
  19.   };

  校验表单逻辑form.validateFields: 

</>复制代码

  1.  // 校验逻辑
  2.   const validateFields = (): Promise<Record<string, any>> => {
  3.   if (!form) {
  4.   return Promise.resolve({});
  5.   }
  6.   const activeFieldsValue = getActivetFieldValues();
  7.   const fields = Object.keys(activeFieldsValue);
  8.   // antd 4
  9.   // validateFields 直接调用
  10.   if (isAntdV4) {
  11.   return (form.validateFields as Antd4ValidateFields)(fields);
  12.   }
  13.   // antd 3
  14.   return new Promise((resolve, reject) => {
  15.   form.validateFields(fields, (errors, values) => {
  16.   if (errors) {
  17.   reject(errors);
  18.   } else {
  19.   resolve(values);
  20.   }
  21.   });
  22.   });
  23.   };

  重置表单form.setFieldsValue:

</>复制代码

  1.   // 重置表单
  2.   const restoreForm = () => {
  3.   if (!form) {
  4.   return;
  5.   }
  6.   // antd v4
  7.   if (isAntdV4) {
  8.   return form.setFieldsValue(allFormDataRef.current);
  9.   }
  10.   // antd v3
  11.   const activeFieldsValue = {};
  12.   Object.keys(allFormDataRef.current).forEach((key) => {
  13.   if (form.getFieldInstance ? form.getFieldInstance(key) : true) {
  14.   activeFieldsValue[key] = allFormDataRef.current[key];
  15.   }
  16.   });
  17.   form.setFieldsValue(activeFieldsValue);
  18.   };

  修改表单类型,支持'simple'和'advance'。初始化的表单数据可以填写 simple 和 advance 全量的表单数据,开发者可以根据当前激活的类型来设置表单数据。修改 type 的时候会重置 form 表单数据。

</>复制代码

  1.   const changeType = () => {
  2.   // 获取当前表单值
  3.   const activeFieldsValue = getActivetFieldValues();
  4.   // 修改表单值
  5.   allFormDataRef.current = {
  6.   ...allFormDataRef.current,
  7.   ...activeFieldsValue,
  8.   };
  9.   // 设置表单类型
  10.   setType((t) => (t === 'simple' ? 'advance' : 'simple'));
  11.   };
  12.   // 修改 type,则重置 form 表单数据
  13.   useUpdateEffect(() => {
  14.   if (!ready) {
  15.   return;
  16.   }
  17.   restoreForm();
  18.   }, [type]);

  _submit方法:对 form 表单校验后,根据当前 form 表单数据、分页等筛选条件进行对表格数据搜索。

</>复制代码

  1.   const _submit = (initPagination?: TParams[0]) => {
  2.   setTimeout(() => {
  3.   // 先进行校验
  4.   validateFields()
  5.   .then((values = {}) => {
  6.   // 分页的逻辑
  7.   const pagination = initPagination || {
  8.   pageSize: options.defaultPageSize || 10,
  9.   ...(params?.[0] || {}),
  10.   current: 1,
  11.   };
  12.   // 假如没有 form,则直接根据分页的逻辑进行请求
  13.   if (!form) {
  14.   // @ts-ignore
  15.   run(pagination);
  16.   return;
  17.   }
  18.   // 获取到当前所有 form 的 Data 参数
  19.   // record all form data
  20.   allFormDataRef.current = {
  21.   ...allFormDataRef.current,
  22.   ...values,
  23.   };
  24.   // @ts-ignore
  25.   run(pagination, values, {
  26.   allFormDataallFormDataRef.current,
  27.   type,
  28.   });
  29.   })
  30.   .catch((err) => err);
  31.   });
  32.   };

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

</>复制代码

  1.   // Table 组件的 onChange 事件
  2.   const onTableChange = (pagination: any, filters: any, sorter: any) => {
  3.   const [oldPaginationParams, ...restParams] = params || [];
  4.   run(
  5.   // @ts-ignore
  6.   {
  7.   ...oldPaginationParams,
  8.   currentpagination.current,
  9.   pageSizepagination.pageSize,
  10.   filters,
  11.   sorter,
  12.   },
  13.   ...restParams,
  14.   );
  15.   };

  初始化的时候,会根据当前是否有缓存的数据,有则根据缓存的数据执行请求逻辑。否则,通过manual和ready判断是否需要进行重置表单后执行请求逻辑。

 

</>复制代码

  1.  // 初始化逻辑
  2.   // init
  3.   useEffect(() => {
  4.   // if has cacheuse cached params. ignore manual and ready.
  5.   // params.length > 0,则说明有缓存
  6.   if (params.length > 0) {
  7.   // 使用缓存的数据
  8.   allFormDataRef.current = cacheFormTableData?.allFormData || {};
  9.   // 重置表单后执行请求
  10.   restoreForm();
  11.   // @ts-ignore
  12.   run(...params);
  13.   return;
  14.   }
  15.   // 非手动并且已经 ready,则执行 _submit
  16.   if (!manual && ready) {
  17.   allFormDataRef.current = defaultParams?.[1|| {};
  18.   restoreForm();
  19.   _submit(defaultParams?.[0]);
  20.   }
  21.   }, []);

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

</>复制代码

  1.   return {
  2.   ...result,
  3.   // Table 组件需要的数据,直接透传给 Table 组件即可
  4.   tableProps: {
  5.   dataSource: result.data?.list || defaultDataSourceRef.current,
  6.   loadingresult.loading,
  7.   onChange: useMemoizedFn(onTableChange),
  8.   pagination: {
  9.   currentresult.pagination.current,
  10.   pageSizeresult.pagination.pageSize,
  11.   totalresult.pagination.total,
  12.   },
  13.   },
  14.   search: {
  15.   // 提交表单
  16.   submit: useMemoizedFn(submit),
  17.   // 当前表单类型, simple | advance
  18.   type,
  19.   // 切换表单类型
  20.   changeType: useMemoizedFn(changeType),
  21.   // 重置当前表单
  22.   reset: useMemoizedFn(reset),
  23.   },
  24.   } as AntdTableResult<TData, TParams>;

  列表页常见 hook 封装示例的详细内容到此结束,希望大家多多关注更多精彩内容。



文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/128255.html

相关文章

  • dva源码解析(一)

    摘要:动态处理与,封装了在运行时的进行一类增加和删除的操作,例如可以再切换到某一路由时动态的加入一个个人猜测,热更新很有可能也利用了这个两个与。以上是本人对于的粗略的理解,内容如有错误,还请大家指出。 写在前面 dva是蚂蚁金服推出的一个单页应用框架,对redux,react-router,redux-saga进行了上层封装,没有引入新的概念,但是极大的程度上提升了开发效率;下面内容为本人理...

    bladefury 评论0 收藏0
  • Ant Design Pro - 实践React Hooks -

    摘要:背景目前是社区最炙手可热的新技术,我们准备追一下热度,在当前的项目中实践一下技术。我们的项目使用的脚手架是,初步想法是把现有的一个有状态页面组件重构成函数组件。存放表单值的状态是声明在列表组件,传给表单组件。 背景 React Hooks目前是React社区最炙手可热的新技术,我们准备追一下热度,在当前的项目中实践一下Hooks技术。 我们的项目使用的脚手架是Ant Design P...

    wangbjun 评论0 收藏0
  • 解析ahooks整体架构及React工具库源码

     这是讲 ahooks 源码的第一篇文章,简要就是以下几点:  加深对 React hooks 的理解。  学习如何抽象自定义 hooks。构建属于自己的 React hooks 工具库。  培养阅读学习源码的习惯,工具库是一个对源码阅读不错的选择。  注:本系列对 ahooks 的源码解析是基于v3.3.13。自己 folk 了一份源码,主要是对源码做了一些解读,可见详情。  第一篇主要介绍 a...

    3403771864 评论0 收藏0

发表评论

0条评论

3403771864

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<