
sunilwang
2022/07/26阅读:11主题:绿意
使用Antd开发中后台项目通用实践
作者简介
郑瑜栋:在团队拥有一个奇怪的外号“=”哥, 咱也是有标点(签)的人了
前言
Antd
是国内使用最广泛的React
中后台组件库。刚好笔者最近一直在使用TS
+React
+Antd
这套经典的技术栈开发项目,顺手把过程中的一些使用经验梳理一下,分享给大家。

这是之前笔者整理的一个数据,这个比例应该在别的公司也不会相差很多,毕竟绝大大部分的中后台页面都是CRUD
类型的,新建提交-表单查询-列表展示-数据更新 这基本就涵盖了大部分的常见开发场景。
表单场景
1. 区分新建或更新
新建与更新的表单字段大部分情况下是高度一致的 , 所以组件可以复用,详见下方的培训配置一个例子:

虽然表单组件可以复用,但其实他们在逻辑层面还是有较大区别的, 主要表现在:
-
更新场景需要传一个列表的唯一ID,而新建场景不需要。 -
更新需要回填之前的数据,新建不需要。
这就需要在复用组件的情况下,去区分两种逻辑:
-
第一种,可以传一个 type
属性来区分,create
代表新建,update
代表更新; -
第二种,直接判断父组件传入的 props
中是否存在一个更新ID
进行区分。存在,即为更新场景;不存在,就是新建;
这里更推荐第二种,因为足够简洁,不用传额外的属性。
tip: 一个好的组件封装原则:在不影响组件功能实现的前提下,props越简洁越利于维护。

Form回填
react
里实现双向绑定相比vue
里的v-model
指令方式会麻烦许多,同时也考虑到其他方面比如性能等因素,antd-Form v4
目前采用了非受控的设计,也就是说:修改表单内部状态,必须要通过调用Form实例方法来完成,比如 数据回填 就是一例。
Form
的initialValues
可以用来设置默认值,但它有个重要的特点:只在组件首次初始化时生效一次。但在实际场景中,我们的回填数据,往往是从后端接口异步获取的,所以就需要配合一些其他手段来完成这项工作。
这里提供两种思路:
-
第一种: initialValues
属性 +form.resetFields()
。 将异步请求到的回填数据,保存成一个state
,同时调用form.resetFields()
。
const [form] = Form.useForm();
const [detail, setDetail] = useState<Record<string, any>>();
useEffect(() => {
axios.get('/api/xxx').then(data => {
setDetail(data);
form.resetFields();
});
}, [form]);
return (
<>
<Form form={form} initialValues={detail}>
<Form.Item name="age" label="age">
<Input />
</Form.Item>
</Form>
</>
);
-
第二种 直接使用 form.setFieldsValue()
更新表单值。
const [form] = Form.useForm();
useEffect(() => {
axios.get('/api/xxx').then(data => {
form.setFieldsValue(data);
});
}, [form]);
return (
<>
<Form form={form}>
<Form.Item name="age" label="age">
<Input />
</Form.Item>
</Form>
</>
);
通常情况下,更推荐第二种方式, 不需要多余的状态去配合处理,但如果你还想用获取到的数据,去做点别的事情,那么你应该会更青睐第一种方式。
数据格式转换
后端需要的数据格式,往往与前端这需要的不一致,这在表单场景下尤为明显。
比如,一个date
的时间字段,后端回填的是时间戳类型的值:1628897838278
,但前端这antd的日期组件默认需要的是Moment
类型,用TypeScript
的interface
来抽象描述 , 就大概是下图这个样子:

接下来就是要实现两个转换函数
-
将后端的数据格式( ServerParams
)转换为Form需要的格式(FormValues
) -
将前端Form( FormValues
)收集的数据格式转换为后端需要的格式(ServerParams
)



而且,笔者建议只要是开发表单场景,就提前定义好两个转换方法,可以让前后端的数据转换逻辑变得更清晰,并且随着后续需求迭代,表单字段会变得越来越复杂,这种做法的收益也会越大。
多场景提交
大部分表单,一般都只有一个提交主按钮,这样用一个htmlType = \'submit\'
的按钮,配合Form的onFinish
回调就可以完成。


但是,凡事总有例外,比如页面现在设计为两个主操作:【保存草稿】 与 【提交】
-
保存草稿:只是将数据暂存起来,并不会对业务实时生效。 -
提交:会对业务实时生效。
这意味着他们是不同的逻辑,也许对应后端不同的接口,那么用onFinish
提交这个常规流程就不能用了,因为你无法在同一个方法里,区分两种不同的逻辑,你可以这样做:
-
用不同的方法区分操作类型

-
如果需要校验,调 await form.validateFields
进行表单验证,并且顺便就获取到了表单值。不需要校验的话,直接调form.getFieldsValue
获取表单值,接下来就可以通过调不同的后端接口实现不同的功能逻辑了。


列表
列表项配置
antd的列表项有时候会非常长,希望抽到一个单独文件中去维护,但是列表项的配置又依赖一些方法,那么就可以用一个参数包含actions
动作参数的函数去解决。
/**
* @param actions 操作列表
* @returns TableColumnType
*/
export function getTableColumn(actions = {}) {
return [
{
title: '培训城市',
dataIndex: 'cityName',
},
{
title: '培训主题',
dataIndex: 'title',
},
{
title: '操作',
fixed: 'right',
render(_, row) {
return (
<Button
onClick={() => actions.handleSeeDetail(row)}
type="primary"
size="small"
> 查看
</Button>
);
},
},
];
}
分页请求
抽一个公共的hook,去处理分页:
/* eslint-disable react-hooks/exhaustive-deps */
import { useState, useEffect, useRef } from 'react';
import { message, TableProps } from 'antd';
export interface PageParams {
pageNum: number;
pageSize: number;
}
type RequestHandler<RecordType> = (
pageParams: PageParams
) => Promise<{ data: Array<RecordType>; total: number }>; // 请求数据的方法
type Options = {
isInit?: boolean; // 是否在初始化时候默认触发一次请求 默认为true
};
interface Result<RecordType> {
tableProps: TableProps<RecordType>; // 表格属性
resetTable: () => void; // 手动重置表格 页码会重置为第一页
reloadTable: () => void; // 手动刷新当前页的表格数据
}
/**
* @description 方便使用antd表格,提供了分页自动处理功能
* @param request 请求方法
* @param deps 表格重置的依赖项,任意依赖项发生变化,重置页码为1,自动调用request方法
* @param Options 自定义选项
* @returns Result<T>
*/
export default function useAntdTable<T = object>(
request: RequestHandler<T>,
deps: any[] = [],
tableProps: TableProps<T> = {},
options?: Options,
): Result<T> {
const [dataList, setDataList] = useState([]); // 列表数据
const defaultPageSize = (tableProps?.pagination && tableProps?.pagination.defaultPageSize) || 10;
const [pageParams, setPageParams] = useState({
pageNum: 1,
pageSize: defaultPageSize,
}); // 当前页码及每页条数
const [total, setTotal] = useState(0); // 数据总条数
const [isFetching, setIsFetching] = useState(false); // 是否处于请求中
const isInitFlagRef = useRef(true); // 标示下是否是首次初始化渲染
const { isInit = true } = options || {};
const fetchData = async () => {
try {
setIsFetching(true);
// eslint-disable-next-line no-shadow
const { data, total } = (await request(pageParams)) || {};
setDataList(data || []);
setTotal(total || 0);
} catch (error) {
message.error(error.message || error.msg || '加载列表出错');
setDataList([]);
}
isInitFlagRef.current = false;
setIsFetching(false);
};
const stopInitReq = useRef(isInit === false);
// 分页请求
useEffect(() => {
if (stopInitReq.current) { // isInit选项为true,禁止初始化时候去请求列表
stopInitReq.current = false;
return;
}
fetchData();
}, [pageParams]);
// 依赖项变化,重置分页
useEffect(() => {
const isNeedRest = isInitFlagRef.current === false; // 只在更新场景才调用重置,首次初始化不需要
isNeedRest && setPageParams({
...pageParams,
pageNum: 1,
});
}, deps);
const onChange = ({ current, pageSize }) => {
setPageParams({
pageNum: pageSize === pageParams.pageSize ? current : 1,
pageSize,
});
};
return {
tableProps: {
...tableProps,
loading: isFetching,
onChange,
dataSource: dataList,
pagination: tableProps.pagination !== false && {
...tableProps.pagination,
current: pageParams.pageNum,
pageSize: pageParams.pageSize,
total,
},
},
resetTable() {
setPageParams({
...pageParams,
pageNum: 1,
});
setTotal(0);
setDataList([]);
},
reloadTable() {
fetchData();
setTotal(0);
setDataList([]);
},
};
}
有了这个useAntdTable
hook,在开发中使用分页,就非常简单了。

列表操作
列表的操作,交互形态大多是以按钮点击触发展示的Modal
框作为操作的载体,需要将列表ID及其他列表项参数进行传入,实现对应业务逻辑。

这里我讲解两种解决方案:
-
第一种:数据流的方式。主要是将 Modal
+操作Button
组合起来封装成一个包含功能逻辑的增强按钮组件,列表ID通过正常的数据流从高到低自然的传入。

下边就是其中操作按钮ShowNameListBtn
的实现。

优势: 按钮组件更加自治,列表的ID及其他列表字段都通过数据流的形式传入,无需特殊处理 缺点: 性能不高,有多少条数据,组件就得渲染多少次。
-
第二种:用一组专门的状态来维护当前正在操作的那条数据的信息,也是比较常规的一种方式。
-
新建一组状态。包含 Modal
展示,列表ID,及其他列表项数据。

-
给操作按钮绑定更新事件,将当前点击的列表项信息更新到上一步提到的状态上。


-
父组件状态更新,引发重渲染,将最新数据传给弹框组件。

-
优势: 按钮组件更加自治,列表的ID及其他列表字段都通过数据流的形式传入,无需特殊处理 -
缺点: 性能不高,有多少条数据,组件就得渲染多少次。
两种方案各有利弊,看自己需求,我自己用第一种多一些,因为这样组件职责更加明确,按钮组件实现自治,能大幅降低父组件的代码量。
LBG开源项目推广:
还在手写 HTML 和 CSS 吗?
还在写布局吗?
快用 Picasso 吧,Picasso 一键生成高可用的前端代码,让你有更多的时间去沉淀和成长,欢迎Star
开源项目地址:https://github.com/wuba/Picasso
官网地址:https://picassoui.58.com

作者介绍
