前言
antd 是一套优秀的React组件库,已成为使用 React 技术栈的团队开发中后台项目的一项 基础设施 ,刚好笔者最近一直在使用TS+React+antd开发中后台项目,所以,想把一些使用经验梳理一下,分享给大家。
这是之前笔者整理的一个数据,这个比例应该在别的公司也不会相差很多,毕竟绝大大部分的中后台页面都是CRUD类型的,新建提交-表单查询-列表展示-数据更新 这基本就涵盖了大部分的常见开发场景。
表单场景
区分新建或更新
新建与更新的表单字段大部分情况下是高度一致的 , 所以组件可以复用,详见下方的培训配置一个例子:
虽然表单组件可以复用,但其实他们在逻辑层面还是有很大区别的:
- 更新场景需要传一个列表的唯一ID,而新建场景不需要
- 更新需要回填之前的数据,新建不需要 这就需要在复用组件的情况下,去区分逻辑:
- 第一种,可以传一个
type属性来区分,type Type = 'create' | 'update'; - 第二种,直接判断父组件传入的
props中是否存在一个更新ID进行区分,存在,就是更新,不存在,就是新建; 这里更推荐第二种,因为足够简洁,不用传额外的属性。
一个好的组件封装原则:在不影响组件功能实现的前提下,props越简洁越利于维护。
Form回填
react里实现双向绑定比vue里的v-model指令方式会麻烦许多,同时也考虑到其他的设计上取舍,antd-Form目前采用了完全非受控的设计,想修改表单内部状态,就必须要通过调用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,去处理分页:
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 }>; // 请求数据的方法
interface Result<RecordType> {
tableProps:TableProps<RecordType> // 表格属性
resetTable:()=>void // 手动重置表格 页码会重置为第一页
reloadTable:()=>void // 手动刷新当前页的表格数据
}
/**
* @description 方便使用antd表格,提供了分页自动处理功能
* @param request 请求方法
* @param deps 表格重置的依赖项,任意依赖项发生变化,重置页码为1,自动调用request方法
* @returns Result<T>
*/
export default function useAntdTable<T = object>(request:RequestHandler<T>, deps:any[] = [], tableProps:TableProps<T> = {}):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 fetchData = async () => {
try {
setIsFetching(true);
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);
};
useEffect(() => {
fetchData();
}, [pageParams]);
useEffect(() => {
const isNeedRest = !isInitFlagRef.current;// 除去首次渲染,之后的更新场景,如果重置依赖项deps,任意元素改变,触发重置
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([]);
},
};
}
有了这个useAntdTablehook,在开发中使用分页,就非常简单了。
列表操作
列表的操作,交互形态大多是以按钮点击触发展示的Modal框作为操作的载体,需要将列表ID及其他列表项参数进行传入,实现对应业务逻辑。
这里我讲解两种解决方案:
- 第一种:数据流的方式。主要是将
Modal+操作Button组合起来封装成一个包含功能逻辑的增强按钮组件,列表ID通过正常的数据流从高到低自然的传入。
下边就是其中操作按钮ShowNameListBtn的实现。
优势: 按钮组件更加自治,列表的ID及其他列表字段都通过数据流的形式传入,无需特殊处理
缺点: 性能不高,有多少条数据,组件就得渲染多少次。
- 第二种:用一组专门的状态来维护当前正在操作的那条数据的信息,也是比较常规的一种方式。
-
新建一组状态。包含
Modal展示,列表ID,及其他列表项数据。 -
给操作按钮绑定更新事件,将当前点击的列表项信息更新到上一步提到的状态上。
- 父组件状态更新,引发重渲染,将最新数据传给弹框组件。
优势:渲染开销小。Modal只需在父组件引用渲染一次。
缺点:组件划分不够解耦,父组件多了一组状态的维护。列表ID这些参数严格意义上并不应该属于父组件的state的范畴。
两种方案各有利弊,看自己需求,我自己用第一种多一些,因为这样组件职责更加明确,按钮组件实现自治,能大幅降低父组件的代码量。
持续更新中...
需求不停止,文章也会持续更新下去... 希望大家评论区多多交流自己的经验,或者给予批评指正。