ahooks 源码解读系列 - 16

997 阅读5分钟

这个系列是将 ahooks 里面的所有 hook 源码都进行解读,通过解读 ahooks 的源码来熟悉自定义 hook 的写法,提高自己写自定义 hook 的能力,希望能够对大家有所帮助。

为了和代码原始注释区分,个人理解部分使用 ///开头,此处和 三斜线指令没有关系,只是为了做区分。

往期回顾

来到最后一部分啦,Table 部分的 hooks ~
完结撒花,感谢大家一直来的点赞、关注 ~
话说我觉得我一开始取的名字不太对,应该叫 “逐行一字不拉之硬啃 ahooks 源码系列” 才对,🤦‍♀️ 。。。

Table

对常用列表场景进行了封装。

useAntdTable

梦开始的地方~ 看名字就知道是为 antd 量身定制的

import useRequest from '@ahooksjs/use-request';
import { useState, useCallback, useEffect, useRef } from 'react';
import {
  CombineService,
  PaginatedParams,
  BasePaginatedOptions,
  PaginatedOptionsWithFormat,
  PaginatedFormatReturn,
  PaginatedResult,
} from '@ahooksjs/use-request/lib/types';
import useUpdateEffect from '../useUpdateEffect';
import usePersistFn from '../usePersistFn';

export {
  CombineService,
  PaginatedParams,
  BasePaginatedOptions,
  PaginatedOptionsWithFormat,
  PaginatedFormatReturn,
  PaginatedResult,
};

export interface Store {
  [name: string]: any;
}

type Antd3ValidateFields = (fieldNames: string[], callback: (errors, values) => void) => void;
type Antd4ValidateFields = (fieldNames?: string[]) => Promise<any>;

export interface UseAntdTableFormUtils {
  getFieldInstance?: (name: string) => {}; // antd 3
  setFieldsValue: (value: Store) => void;
  getFieldsValue: (...args: any) => Store;
  resetFields: (...args: any) => void;
  validateFields: Antd3ValidateFields | Antd4ValidateFields;
  [key: string]: any;
}

export interface Result<Item> extends PaginatedResult<Item> {
  search: {
    type: 'simple' | 'advance';
    changeType: () => void;
    submit: () => void;
    reset: () => void;
  };
}

export interface BaseOptions<U> extends Omit<BasePaginatedOptions<U>, 'paginated'> {
  form?: UseAntdTableFormUtils;
  defaultType?: 'simple' | 'advance';
}

export interface OptionsWithFormat<R, Item, U>
  extends Omit<PaginatedOptionsWithFormat<R, Item, U>, 'paginated'> {
  form?: UseAntdTableFormUtils;
  defaultType?: 'simple' | 'advance';
}

function useAntdTable<R = any, Item = any, U extends Item = any>(
  service: CombineService<R, PaginatedParams>,
  options: OptionsWithFormat<R, Item, U>,
): Result<Item>;
function useAntdTable<R = any, Item = any, U extends Item = any>(
  service: CombineService<PaginatedFormatReturn<Item>, PaginatedParams>,
  options: BaseOptions<U>,
): Result<Item>;
function useAntdTable<R = any, Item = any, U extends Item = any>(
  service: CombineService<any, any>,
  options: BaseOptions<U> | OptionsWithFormat<R, Item, U>,
): any {
  const {
    form,
    refreshDeps = [],
    manual,
    defaultType = 'simple',
    defaultParams,
    ...restOptions
  } = options;

  /// 使用 paginated 和 manual 模式
  const result = useRequest(service, {
    ...restOptions,
    paginated: true as true,
    manual: true,
  });

  const { params, run } = result;

  /// 在下面的 _submit 方法中,会将全量的表单数据和上次的请求type 作为第三个参数传入 run 方法
  const cacheFormTableData = params[2] || ({} as any);

  // 优先从缓存中读
  const [type, setType] = useState(cacheFormTableData.type || defaultType);

  // 全量 form 数据,包括 simple 和 advance
  const [allFormData, setAllFormData] = useState<Store>(
    cacheFormTableData.allFormData || (defaultParams && defaultParams[1]) || {},
  );
  
  /// 之所以会有当前展示的 form 这种说法,是因为内置了一个切换表单的功能
  /// 本 hook 支持渲染两种类型的 form ,一种 simple 一种 advance
  /// 可以很方便的实现那种展开更多搜索条件的交互
  // 获取当前展示的 form 字段值
  const getActivetFieldValues = useCallback((): Store => {
    if (!form) {
      return {};
    }
    // antd 3
    if (form.getFieldInstance) {
      const tempAllFiledsValue = form.getFieldsValue();
      const filterFiledsValue: Store = {};
      Object.keys(tempAllFiledsValue).forEach((key: string) => {
        if (form.getFieldInstance ? form.getFieldInstance(key) : true) {
          filterFiledsValue[key] = tempAllFiledsValue[key];
        }
      });
      return filterFiledsValue;
    }
    // antd 4
    return form.getFieldsValue(null, () => true);
  }, [form]);

  const formRef = useRef(form);
  formRef.current = form;
  /* 初始化,或改变了 searchType, 恢复表单数据 */
  useEffect(() => {
    if (!formRef.current) {
      return;
    }
    // antd 3
    if (formRef.current.getFieldInstance) {
      // antd 3 需要判断字段是否存在,否则会抛警告
      const filterFiledsValue: Store = {};
      Object.keys(allFormData).forEach((key: string) => {
        if (formRef.current!.getFieldInstance ? formRef.current!.getFieldInstance(key) : true) {
          filterFiledsValue[key] = allFormData[key];
        }
      });
      formRef.current.setFieldsValue(filterFiledsValue);
    } else {
      // antd 4
      formRef.current.setFieldsValue(allFormData);
    }
  }, [type]);

  // 首次加载,手动提交。为了拿到 form 的 initial values
  useEffect(() => {
    // 如果有缓存,则使用缓存,重新请求
    if (params.length > 0) {
      run(...params);
      return;
    }

    // 如果没有缓存,触发 submit
    if (!manual) {
      _submit(defaultParams);
    }
  }, []);
  
  /// 在切换 type 之前,将当前表单的值和之前记录的值合并之后记录下来,达到支持两种表单的目的
  const changeType = useCallback(() => {
    const currentFormData = getActivetFieldValues();
    setAllFormData({ ...allFormData, ...currentFormData });

    const targetType = type === 'simple' ? 'advance' : 'simple';
    setType(targetType);
  }, [type, allFormData, getActivetFieldValues]);

  const validateFields: () => Promise<any> = useCallback(() => {
    const fieldValues = getActivetFieldValues();
    if (!form) {
      return Promise.resolve();
    }
    
    /// 根据当前表单的值,确定需要校验的字段名
    const fields = Object.keys(fieldValues);
    /// 使用特性检测,不满足则 validateFields 返回的不是 promise
    if (!form.getInternalHooks) {
      return new Promise((resolve, reject) => {
        form.validateFields(fields, (errors, values) => {
          if (errors) {
            reject(errors);
          } else {
            resolve(values);
          }
        });
      });
    }

    return (form.validateFields as Antd4ValidateFields)(fields);
  }, [form]);

  const _submit = useCallback(
    (initParams?: any) => {
      /// 放在 setTimeout 中,也就是等待下一次宏队列再运行,应该是防止 changeType 之后立刻提交导致提交的表单数据不对
      setTimeout(() => {
        validateFields()
          .then(() => {
            const activeFormData = getActivetFieldValues();
            // 记录全量数据
            const _allFormData = { ...allFormData, ...activeFormData };
            setAllFormData(_allFormData);
            
            /// 使用三个参数调用 run :分页相关参数、搜索表单数据、全量搜索表单数据以及表单类型
            // has defaultParams
            if (initParams) {
              run(initParams[0], activeFormData, {
                allFormData: _allFormData,
                type,
              });
              return;
            }

            run(
              {
                pageSize: options.defaultPageSize || 10,
                ...((params[0] as PaginatedParams[0] | undefined) || {}), // 防止 manual 情况下,第一次触发 submit,此时没有 params[0]
                current: 1,
              },
              activeFormData,
              {
                allFormData: _allFormData,
                type,
              },
            );
          })
          .catch((err) => err);
      });
    },
    [getActivetFieldValues, run, params, allFormData, type],
  );

  const reset = useCallback(() => {
    if (form) {
      form.resetFields();
    }
    _submit();
  }, [form, _submit]);

  const resetPersistFn = usePersistFn(reset);

  // refreshDeps 变化,reset。
  useUpdateEffect(() => {
    if (!manual) {
      resetPersistFn();
    }
  }, [...refreshDeps]);

  const submit = usePersistFn((e) => {
    if (e && e.preventDefault) {
      e.preventDefault();
    }
    _submit();
  });

  return {
    ...result,
    search: {
      submit,
      type,
      changeType,
      reset,
    },
  };
}

export default useAntdTable;

useFusionTable

为 fusion form 和 fusion table 量身定做的

import {
  CombineService,
  PaginatedParams,
  BasePaginatedOptions,
  PaginatedOptionsWithFormat,
  PaginatedFormatReturn,
  PaginatedResult,
} from '@ahooksjs/use-request/lib/types';

import useAntdTable from '../useAntdTable';
import { fieldAdapter, resultAdapter } from './fusionAdapter';

export {
  CombineService,
  PaginatedParams,
  BasePaginatedOptions,
  PaginatedOptionsWithFormat,
  PaginatedFormatReturn,
  PaginatedResult,
};

export interface Store {
  [name: string]: any;
}

export interface Field {
  getFieldInstance?: (name: string) => {}; // antd 3
  setValues: (value: Store) => void;
  getValues: (...args: any) => Store;
  reset: (...args: any) => void;
  validate: (callback: (errors, values) => void) => void;
  [key: string]: any;
}

export interface Result<Item> extends Omit<PaginatedResult<Item>, 'tableProps'> {
  paginationProps: {
    onChange: (current: number) => void;
    onPageSizeChange: (size: number) => void;
    current: number;
    pageSize: number;
    total: number;
  };
  tableProps: {
    dataSource: Item[];
    loading: boolean;
    onSort: (dataIndex: String, order: String) => void;
    onFilter: (filterParams: Object) => void;
  };
  search: {
    type: 'simple' | 'advance';
    changeType: () => void;
    submit: () => void;
    reset: () => void;
  };
}

export interface BaseOptions<U> extends Omit<BasePaginatedOptions<U>, 'paginated'> {
  field?: Field;
  defaultType?: 'simple' | 'advance';
}

export interface OptionsWithFormat<R, Item, U>
  extends Omit<PaginatedOptionsWithFormat<R, Item, U>, 'paginated'> {
  field?: Field;
  defaultType?: 'simple' | 'advance';
}

function useFusionTable<R = any, Item = any, U extends Item = any>(
  service: CombineService<R, PaginatedParams>,
  options: OptionsWithFormat<R, Item, U>,
): Result<Item>;
function useFusionTable<R = any, Item = any, U extends Item = any>(
  service: CombineService<PaginatedFormatReturn<Item>, PaginatedParams>,
  options: BaseOptions<U>,
): Result<Item>;


/// 主要是使用 fieldAdapter 将 field 转化成 form
/// 使用 resultAdapter 将结果转化成 fusion 需要的形式
/// 可以理解为一个已经处理了输入输出的 useAntdTable 的语法糖 
function useFusionTable<R = any, Item = any, U extends Item = any>(
  service: CombineService<any, any>,
  options: BaseOptions<U> | OptionsWithFormat<R, Item, U>,
): any {
  const ret = useAntdTable(service, {
    ...options,
    form: options.field ? fieldAdapter(options.field) : undefined,
  });

  return resultAdapter(ret);
}

export default useFusionTable;

下面是核心转化方法

import { Field } from './index';

export interface Store {
  [name: string]: any;
}
interface UseAntdTableFormUtils {
  getFieldInstance?: (name: string) => {}; // antd 3
  setFieldsValue: (value: Store) => void;
  getFieldsValue: (...args: any) => Store;
  resetFields: (...args: any) => void;
  validateFields: () => Promise<any>;
  [key: string]: any;
}

/// 将 fusion 的方法转化为 antd 的方法
export const fieldAdapter = (field: Field) =>
  ({
    getFieldInstance: (name: string) => field.getNames().includes(name),
    setFieldsValue: field.setValues,
    getFieldsValue: field.getValues,
    resetFields: field.reset,
    validateFields: (fields, callback) => {
      field.validate(callback);
    },
  } as UseAntdTableFormUtils);

/// 将 useAntdTable 的返回值转化为 fusion 需要的格式
export const resultAdapter = (result: any) => {
  const tableProps = {
    dataSource: result.tableProps.dataSource,
    loading: result.tableProps.loading,
    onSort: (dataIndex: String, order: String) => {
      result.tableProps.onChange(
        { current: result.pagination.current, pageSize: result.pagination.pageSize },
        result.filters,
        {
          field: dataIndex,
          order,
        },
      );
    },
    onFilter: (filterParams: Object) => {
      result.tableProps.onChange(
        { current: result.pagination.current, pageSize: result.pagination.pageSize },
        filterParams,
        result.sorter,
      );
    },
  };

  const paginationProps = {
    onChange: result.pagination.changeCurrent,
    onPageSizeChange: result.pagination.changePageSize,
    current: result.pagination.current,
    pageSize: result.pagination.pageSize,
    total: result.pagination.total,
  };

  return {
    ...result,
    tableProps,
    paginationProps,
  };
};

以上内容由于本人水平问题难免有误,欢迎大家进行讨论反馈。