列表页hook封装
Gopal 人气:0引言
本文是深入浅出 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 依赖的条件放这里。
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 size const 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, ); };
初始化的时候,会根据当前是否有缓存的数据,有则根据缓存的数据执行请求逻辑。否则,通过 manual
和 ready
判断是否需要进行重置表单后执行请求逻辑。
// 初始化逻辑 // init useEffect(() => { // 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>;
加载全部内容