ahooks useTable
我们项目和组件库计划重构,由原来class组件的方案过渡到react hooks函数组件方案。针对于table这块,我们除了UI组件做了优化,我们的项目层级也要做些实践.使用过Umi的使用者都知道,Umi最成功的解决方式是它的插件方案,我们也想使用插件式方案,我们项目已经使用了ahooks的use-request,偶然间发现useTable就是使用的插件渐进式方案,很适合我们呀,盘它... 参考文档usetable-ahooks
看下它的官方文档提供的hooks,
我们可以看到,useTable生态提供了下一代table使用模式,table扩展功能实现plugin,已经官方提供的核心库useFormTable和useTable。
我们项目的组件库也升级为基于antd二次封装的组件,看了下useTable的项目结构吧,可以看到下面packages里面有多个包,基于我们要改造的例子最近的就是antd-table了吧,我们进入目录see see...![]()
我们看下antd-table的目录结构:
![]()
我们看下antd-table的目录plugin的实现:
import { methods } from '@ahooksjs/use-table';
const filterTransformer = (filters) => {
return Object.keys(filters).reduce((acc, key) => {
return {
...acc,
[key]: {
selectedKeys: filters[key] || [],
},
};
}, {});
};
const tableActions = {
sort: ({ sorter }, props) => {
props.onSort(sorter.field, sorter.order);
},
filter: ({ filters }, props) => {
props.onFilter(filterTransformer(filters));
},
};
const useAntdTablePlugin = () => {
return {
props: (ctx) => {
return {
tableProps: ({ primaryKey, ...props }) => {
return {
...props,
rowKey: primaryKey,
pagination: false,
onChange: (_, filters, sorter, { action }) => {
const fn = tableActions[action] || (() => {});
fn({ filters, sorter }, props);
},
};
},
paginationProps: ({ onChange, onPageSizeChange, ...props }) => {
return {
...props,
// Antd 不能区分【页码大小】的触发还是【页改变】的触发
onChange: (current, pageSize) => {
const { params } = ctx;
// 切换页码
if (current === params.current) {
ctx.query(
{ pageSize, current: ctx.options.current },
{
queryFrom: methods.ON_PAGE_SIZE_CHANGE,
}
);
}
// 切换页
if (params.pageSize === pageSize) {
ctx.query(
{ pageSize, current },
{
queryFrom: methods.ON_PAGE_SIZE_CHANGE,
}
);
}
},
};
},
};
},
};
};
export default useAntdTablePlugin;
再来看下index.ts的实现:
import useTable, { Obj, IResponse, Options } from '@ahooksjs/use-table';
import useAntdTablePlugin from './plugin';
const useAntdTable = (service: (params: Obj) => Promise<IResponse>, options?: Options) => {
const antdTablePlugin = useAntdTablePlugin();
const plugins = options?.plugins || [];
const props = useTable(service, {
...options,
plugins: [...plugins, antdTablePlugin],
});
return props;
};
export * from '@ahooksjs/use-table';
export default useAntdTable;
export { useAntdTablePlugin };
我们看到上面antd-table项目里面结构和参考文档可以看到,antd-table是基于useTable为核心添加plugin模式来实现。我们再细细地分析下antd-table plugin的实现吧。
可以看到,我们正常的table组件可以拆分成form表单查询,table展示区,pagination分页管理 三大模块。
table展示区和分页的操作事件分开来处理,分别是tableProps和paginationProps,思路相当清晰,tableProps主要处理filter和sort的事件,paginationProps主要处理当前页和每页展示数据多少的变化事件监听。
我们组件也是基于antd来封装的,所以我们可以参考这个自己来封装下适合我们项目的plugin功能。
下面主要是介绍下我们基于自己项目组件库封装的plugin应用已经useTable的原理。
useTable流程
看图肯定必看文字直观,我这边将我82年的流程图拉出来溜溜
毕竟流程图看起来会比较清晰些整个流程在做什么?
useTable主流程
useTable大概核心就是将请求,table操作属性,pagination操作属性传入,导出封装后的查询方法,获取当前查询条件方法,处理后的table属性,pagination属性
源码功力还是很深厚的,使用到了redux compose的概念,也使用到了洋葱模型及生命周期。
核心的use-table如果想细致了解useTablePlugin和useQueryDisplay则看下面的流程图
useTablePlugin流程图
每个生命周期的也附上:
useTablePlugin可以看到主要是封装了middlewares和props属性。
- middlewares 会根据不同的生命周期去做分类,主要分为prepare,didRender,willQuery,yourTurn, - querying, didQuery钩子。
- props属性主要是依次执行usePaginationProps, useTableProps, useParams处理过后的props数据。
useQueryDisplay流程图
useQueryDisplay主要作用:
- 设置全局上下文ctx
- 混合middlware和props属性
- 把最新混合过的plugin和props暴露出来 画的比较长,各位如果看不清需要自己放大哦,如果看不清,可以私聊本人,可以把流程图原件发你。
useHuayunTablePlugin
前提:我们的组件库也是基于antd封装的业务组件,所以我们可以仿照antd-table封装符合我们业务组件的plugin。 首先还是先看下我们组件库的风格吧
设计还是比较前言吧。。。
这里就不贴码了哈,贴个图可以吧
我们还是秉承最小功能原则,将tableProps和paginationProps分开处理。
table页面上的filter过滤机查询条件,分页过滤查询我们底层组件库已经处理,我们这边还是要保持放开。
请求入参变化我们也提供了onChangeParams辅助处理一些事件,
filterQueryParams是我们想针对一些请求字段做特殊处理的特殊方法。
table基类封装
源码撸完了,插件写好了,万事俱备吗,只欠东风了...
基类该怎么实现比较好呢,我们秉承的原则就是:用户使用最少的代码实现最基础的table能力,提供更多的table扩展能力,赋予它更强大的生命力。
我们设计的不一定好,但是适合我们自己项目很重要。
这边我们来看下我们基类封装入参吧
基类入参定义
/**
* 基类入参定义
*/
export interface UseBaseListProps {
// selectedRowKeys?: string[]; // 选中数据列表
// pagination?: any; // 分页数据信息
service: (params: any) => Promise<any>;
/**
* transformRequest 是否基类处理数据转成antd需要的数据格式 默认为true
*/
transformRequest?: boolean;
/**
* defaultParams 默认请求入参
*/
defaultParams?: Record<string, any>;
/**
* autoFirstQuery 是否默认查询数据 默认为true
*/
options?: Options;
/**
* 是否显示searchBarSlot
*/
isShowSearchBarSlot?: boolean;
/**
* searchBarSlotLeft扩展功能
*/
searchBarSlotLeft?: () => React.ReactNode;
/**
* searchBarSlot扩展功能
*/
searchBarAdvanceSlot?: () => React.ReactNode;
/**
* SearchBar的搜索placeholder和名称
*/
searchOption?: any;
/**
* 不需要额外处理的数据,默认请求数据
*/
untreatedParamKeys?: string[];
/**
* 列表是否支持滚动
*/
getScroll?: () => {};
/**
* SearchBar回显查询条件展示
*/
searchFilterParams?: Record<string, any>;
/**
* 查询条件别名
*/
getParamsAlias?: (ParamsAlias|ParamsAliasBasic) | (() => (ParamsAlias|ParamsAliasBasic));
/**
* table列数据
*/
getTableColumns: () => any[];
defaultHiddenColumnKeys?: string[]; // 默认影藏的列key
handleRefresh?: () => void; // 刷新列表数据
onHandleColumnChange?: (hiddenColumn?: string[]) => void; // 隐藏显示列的回调函数
/**
* 筛选过滤处理
*/
// filterHandler?: (key:string) => ((value: any) => void) | null;
/**
* params请求数据回调
*/
onChangeParams?: (selectedRowKeys?: React.Key[], params?: any) => void;
/**
* 是否过滤展示查询条件
*/
filterQueryParams?: (params: any) => any;
/**
* 是否支持滚动 默认不支持
*/
supportScroll?: boolean;
/**
* 是否支持分页 默认支持
*/
supportPagination?: boolean;
/**
* 是否支持chexkbox选中行 默认支持
*/
supportRowSelection?: boolean;
/**
* 额外的rowSelection
*/
advanceRowSelection?: Record<string, any>;
/**
* 空数据展示
*/
emptyData?: React.ReactNode;
/**
* antd的配置
*/
sticky?: Record<string, any>;
/**
* 列表按钮操作
*/
operateButtons?: ButtonProps[];
/**
* 额外的选中事件操作
*/
extraChangeSelect?: (
selectedRowKeys: React.Key[],
selectedRows: any[],
) => void;
/**
* 是否支持searchBar
*/
supportSearchBar?: boolean;
/**
* 路由信息,用于匹配ws推送刷新
*/
route?: Route;
}
这里只会说个主要思想,不会把源码全部贴出来,毕竟是公司项目,大家都懂得...
service请求类的封装
/**
* 获取当前需要传入table的service方法
*/
const requestService = transformRequest ? (params: any) => {
const { current, pageSize, ...formData } = params;
const queryCond = {
pageSize,
pageNumber: current,
...defaultParams,
...formData,
};
return service(queryCond).then((res) => ({
data: {
total: res.totalCount,
dataSource: res.data.map((data: any, i: number) => ({
...data,
key: i,
})),
},
})).catch((err) => {
// 统一捕获异常
handleRequestError(err);
});
} : service;
默认设置刷新函数
/**
* 刷新操作
*/
const defaultHandleRefresh = () => {
query(getParams());
};
const { handleRefresh = defaultHandleRefresh } = props;
过滤器别名生成器
/**
* 别名生成器
* @returns ParamsAlias
*/
const paramsAliasGenerator = (): ParamsAlias => {
if(!getParamsAlias) {
return {};
}
// 请求参数
let filterParams = !!searchFilterParams ? {...searchFilterParams} : {...queryParams};
let result = getParamsAlias;
if(typeof getParamsAlias === 'function') {
result = getParamsAlias();
}
let filterKeyArr:string[] = [...defaultUntreatedParamKeys, ...untreatedParamKeys?.filter((item) => !defaultUntreatedParamKeys.includes(item))];
// 清除需要清除的数据
if(Object.keys(filterParams).length) {
Object.keys(filterParams).forEach((item) => {
if(filterKeyArr.includes(item)) {
delete filterParams[item as keyof typeof filterParams];
}
});
}
// 将剩下的别名数据整合
if(Object.keys(filterParams).length) {
Object.keys(filterParams).forEach((item) => {
const column = getTableColumns()?.find((columnItem) => columnItem.key === item);
let columnOptions = column?.options || column?.filter?.options;
const optionValue = columnOptions?.find((option: any) => option.value === filterParams[item as keyof typeof filterParams])?.title;
// 别名中没有对应的映射,则插入别名
if(!result[item as keyof typeof result]) {
(result[item as keyof typeof result] as ParamsAlias) = {
title: column?.title,
...column?.filter && {
value: optionValue,
},
}
}
// 当前别名设置只设置过string数据
if(typeof result[item as keyof typeof result] === 'string') {
(result[item as keyof typeof result] as ParamsAlias) = {
title: result[item as keyof typeof result],
...column?.filter && {
value: optionValue,
},
};
} else if(!(result[item as keyof typeof result] as ParamsAlias).value && column?.filter) {
// 当前有别名映射对象,有title,没有value
(result[item as keyof typeof result] as ParamsAlias) = {
...(result[item as keyof typeof result] as ParamsAlias) ,
value: optionValue,
};
}
})
}
return result as ParamsAlias;
}
我们不需要ref去控制service调用,是不是无缝链接,我们再看下我们的基类调用吧
直接可以拿到tableDom直接去渲染我们的table数据,不需要去管我们基类内部实现,使用起来是不是也很简单。
本章先聊到这里吧,如果大家有什么想深入了解的,可以在评论区回复。