列表页常见hook封装实例

目录
  • 引言
  • 列表页常见元素
  • usePagination
  • useAntdTable

引言

本文是深入浅出 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 = &lt;TData extends Data, TParams extends Params&gt;(
  service: Service&lt;TData, TParams&gt;,
  options: AntdTableOptions&lt;TData, TParams&gt; = {},
) =&gt; {
  const {
    // form 实例
    form,
    // 默认表单选项
    defaultType = 'simple',
    // 默认参数,第一项为分页数据,第二项为表单数据。[pagination, formData]
    defaultParams,
    manual = false,
    // refreshDeps 变化,会重置 current 到第一页,并重新发起请求。
    refreshDeps = [],
    ready = true,
    ...rest
  } = options;
  // 对分页的逻辑进行处理
  // 分页也是对 useRequest 的再封装
  const result = usePagination&lt;TData, TParams&gt;(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 判断是否需要进行重置表单后执行请求逻辑。

// 初始化逻辑
// 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>;

以上就是列表页常见 hook 封装示例的详细内容,更多关于列表页 hook 封装的资料请关注我们其它相关文章!

(0)

相关推荐

  • 插件化机制优雅封装你的hook请求使用方式

    目录 引言 useRequest 简介 架构 useRequest 入口处理 Fetch 和 Plugins state 以及 setState 插件化机制的实现 核心方法 —— runAsync 请求前 —— onBefore 进行请求——onRequest 取消请求 —— onCancel 最后结果处理——onSuccess/onError/onFinally 思考与总结 引言 本文是深入浅出 ahooks 源码系列文章的第二篇,这个系列的目标主要有以下几点: 加深对 React hooks

  • vue3中的hook简单封装

    目录 vue3的hook封装 vue3的hooks总结 下面总结一下如何去书写hooks 计数器的hook vue3的hook封装 vue3最新鲜的就是组合式API了,通过组合式API我们可以把一些复杂的逻辑或一些常用的逻辑封装成一个个hook来进行调用,这样的方式也更易于维护. 使用 import useTest from "../../hooks/useTest"; export default defineComponent({   name: "vue3Test&qu

  • 动画详解Vue3的Composition Api

    目录 回顾Option Api Option Api的缺陷 Composition Api 众所周知,Vue3.0带来了一个全新的特性——Composition API. 字面意思就是“组合API”,它是为了实现基于函数的逻辑复用机制而产生的. 回顾Option Api 在了解Composition Api之前,首先回顾下我们使用Option Api遇到的问题,我们在Vue2中常常会需要在特定的区域(data,methods,watch,computed...)编写负责相同功能的代码. Opti

  • Composition Api封装业务hook思路示例分享

    目录 前序 hook的场景 useGetJobList 共同 思路历程 心得 utils 和 hook 的区别 总结 前序 近期公司的新项目一个小程序,一直想尝试 Vue3 开发项目,苦于自己的驱动力不行,学的零零碎碎的.因此小程序我直接跟项目组长说我要使用 uniapp 的 Vue3 版进行开发.开发中遇到业务场景相同的,就分装了一个hook 来减少代码,易于维护. hook的场景 这种获取列表的需求很常见吧,在我这个小程序中有3处使用到了获取列表的功能.分别是: 我的收藏.已投递岗位.未投递

  • 列表页常见hook封装实例

    目录 引言 列表页常见元素 usePagination useAntdTable 引言 本文是深入浅出 ahooks 源码系列文章,这个系列的目标主要有以下几点: 加深对 React hooks 的理解. 学习如何抽象自定义 hooks.构建属于自己的 React hooks 工具库. 培养阅读学习源码的习惯,工具库是一个对源码阅读不错的选择. 列表页常见元素 对于一些后台管理系统,典型的列表页包括筛选表单项.Table表格.Pagination分页这三部分. 针对使用 Antd 的系统,在 a

  • React前端DOM常见Hook封装示例上

    目录 引言 useEventListener useClickAway useEventTarget useTitle useFavicon 引言 本文是深入浅出 ahooks 源码系列文章的第十四篇,这个系列的目标主要有以下几点: 加深对 React hooks 的理解. 学习如何抽象自定义 hooks.构建属于自己的 React hooks 工具库. 培养阅读学习源码的习惯,工具库是一个对源码阅读不错的选择. 上一篇我们探讨了 ahooks 对 DOM 类 Hooks 使用规范,以及源码中是

  • React前端DOM常见Hook封装示例下

    目录 引言 useFullscreen useHover useDocumentVisibility 引言 本文是深入浅出 ahooks 源码系列文章的第十五篇,这个系列的目标主要有以下几点: 加深对 React hooks 的理解. 学习如何抽象自定义 hooks.构建属于自己的 React hooks 工具库. 培养阅读学习源码的习惯,工具库是一个对源码阅读不错的选择. 上文指路:React前端DOM常见Hook封装示例上 本篇接着针对关于 DOM 的各个 Hook 封装进行解读. useF

  • 微信小程序 scroll-view组件实现列表页实例代码

    scroll-view组件介绍 scroll-view是微信小程序提供的可滚动视图组件,其主要作用是可以用来做手机端经常会看到的上拉加载下拉刷新列表页!下面就以<摇出微笑>为例来讲解一下这个组件的使用吧! 为app导入新page页面 首先需要为我们的小程序导入新的page页面,项目根目录打开app.json这个项目配置文件在里面的pages数组添加"pages/allJoke/allJoke"然后设置底部导航在"tabBar"的列表项("lis

  • Yii2 GridView实现列表页直接修改数据的方法

    什么意思呢?我来简单的描述下,小编妹子提的需求是这样的,你看啊,你这列表页的数据,能不能我就直接在列表上进行点一下就直接修改啊,我再点进去修改多麻烦,太不方便了.这尼玛,这需求,是不是真想给她一棒槌. ok,我们今天就来看看在yii2中如何去利用gridview实现列表上直接修改的功能,很全面哦,我们尽量各种类型的属性都给出实例. 第一步,我们先来部署好yii2-grid 利用composer安装yii2-grid composer require kartik-v/yii2-grid "@de

  • react中常见hook的使用方式

    1.什么是hook? react hook是react 16.8推出的方法,能够让函数式组件像类式组件一样拥有state.ref.生命周期等属性. 2.为什么要出现hook? 函数式组件是全局当中一个普通函数,在非严格模式下this指向window,但是react内部开启了严格模式,此时this指向undefined,无法像类式组件一样使用state.ref,函数式组件定义的变量都是局部的,当组件进行更新时会重新定义,也无法存储,所以在hook出现之前,函数式组件有很大的局限性,通常情况下都会使

  • ruby ftp封装实例详解

     ruby ftp封装实例详解 最近自己用ruby 封装了一个Net::FTP的工具类. class FtpTool def initialize() @current_ftp = create_ftp end # 获取指定格式的文件名称列表 # 例如: source = "test/*.txt" # 返回: [source/file_name.txt] def fetch_remote_filenames(source) return [] if source.blank? log_

  • js学习总结_选项卡封装(实例讲解)

    这个插件对应的html的结构如下 <div class='box' id='tabFir'> <ul id='tabOptions'> <li class='select'>页卡一</li> <li>页卡二</li> <li>页卡三</li> </ul> <div class='select'> <div>1</div> <div>2</div&

  • vue如何通过id从列表页跳转到对应的详情页

    1. 列表页:列表页带id跳转到详情页 详情页:把id传回到后台就可以获取到数据了 2.列表页跳转到详情页并更改详情页的标题 列表页:带id和页面标题的typeid跳转到详情页 详情页:在html绑定标题,获取到传过来的typeid,然后判断typeid是多少对应返回标题. 补充:获取后台的数据,就是去访问的后台的服务器(怎么访问?答:就是你怎么访问网站那样子)然后他有定义到是必须的参数的时候,就是在连接后必须带的参数,才可以获取到后台数据,不是必须的跟在连接后面也不会影响.就如我的第二个例子,

  • Python数据类型之列表和元组的方法实例详解

    引言 我们前面的文章介绍了数字和字符串,比如我计算今天一天的开销花了多少钱我可以用数字来表示,如果是整形用 int ,如果是小数用 float ,如果你想记录某件东西花了多少钱,应该使用 str 字符串型,如果你想记录表示所有开销的物品名称,你应该用什么表示呢? 可能有人会想到我可以用一个较长的字符串表示,把所有开销物品名称写进去,但是问题来了,如果你发现你记录错误了,想删除掉某件物品的名称,那你是不是要在这个长字符串中去查找到,然后删除,这样虽然可行,那是不是比较麻烦呢. 这种情况下,你是不是

随机推荐