基于 Umi 搭建 React 快速开发框架:封装高效的列表增删改查功能
在当今快速迭代的前端开发环境中,减少重复性工作、提升开发效率是每个团队追求的目标。对于中后台管理系统而言,列表页的增删改查操作占据了开发的半壁江山。如果每个页面都从零开始编写查询表单、表格、分页、模态框等,无疑是巨大的资源浪费。
Umi,作为一个可扩展的企业级前端应用框架,为 React 技术栈提供了开箱即用的工程化能力。结合 Umi 的插件生态和最佳实践,我们可以搭建一套高复用性的快速开发框架,将常见的增删改查操作进行抽象和封装。
本文将详细讲解如何基于 Umi@4 搭建一个 React 开发框架,并核心封装一套功能完善、易于使用的列表增删改查高阶组件(HOC)或自定义 Hook,从而实现列表页面的快速开发。
目录#
一、项目初始化与基础配置#
首先,我们使用 Umi 官方脚手架创建一个新项目。
# 使用 pnpm 创建项目(推荐)
pnpm dlx create-umi@latest my-app
# 或使用 npm
npx create-umi@latest my-app
cd my-app在项目初始化时,选择 Simple App 模板即可。完成后,安装我们后续所需的依赖。
pnpm add @ant-design/pro-components antd
# 确保 umi 和 @umijs/plugins 版本正确,通常在 package.json 中已包含关键配置 (.umirc.ts):
import { defineConfig } from 'umi';
export default defineConfig({
plugins: ['@umijs/plugins/dist/dva', '@umijs/plugins/dist/pro-components'],
dva: {}, // 启用 dva 状态管理
// 配置代理,解决跨域问题(根据实际后端地址调整)
proxy: {
'/api': {
target: 'https://your-api-domain.com',
changeOrigin: true,
pathRewrite: { '^/api': '' },
},
},
// 引入 Antd 并自动优化样式
antd: {},
});二、技术栈选择与理由#
- Umi@4:作为底层框架,提供路由、构建、部署、插件等一站式解决方案,稳定性高。
- React + TypeScript:保证代码的类型安全和可维护性。
- Ant Design:UI 组件库,提供丰富的、设计语言统一的组件。
- ProComponents:基于 Ant Design 的高阶组件库,特别是
ProTable,极大地简化了表格和表单的开发。 - Dva:一个基于 Redux 和 Redux-saga 的数据流方案,用于管理异步操作和全局状态。虽然 React Query 和 SWR 也很流行,但 Dva 与 Umi 集成更简单,概念对于团队上手更容易。
三、核心思想:抽象与封装#
我们的目标是创建一个名为 withTablePro 的高阶组件或一个 useTablePro 的 Hook。它需要封装以下功能:
- 状态管理:查询条件、分页信息、表格加载状态、选中行等。
- 数据获取:自动根据查询条件和分页信息发起请求。
- 常用操作:刷新、重置、删除、批量删除等。
- UI 集成:与
ProTable和ProForm无缝集成。
这样,开发一个列表页只需要:
- 定义接口 API。
- 配置表格列和查询表单项。
- 用
withTablePro包裹页面组件或使用useTableProHook。
四、实现步骤详解#
4.1 定义通用类型#
首先,在 src/types/table.d.ts 中定义通用的类型,这是 TypeScript 项目保持类型安全的基础。
// 通用的分页响应结构(与后端约定)
export interface PaginationResponse<T> {
list: T[];
current: number;
pageSize: number;
total: number;
}
// 通用的查询参数基础结构
export interface BaseQueryParams {
current: number;
pageSize: number;
[key: string]: any; // 允许其他任意查询字段
}
// 列表页组件的 Props(用于 HOC)
export interface TableProProps<T, Q extends BaseQueryParams> {
queryParams: Q;
tableData: PaginationResponse<T> | undefined;
loading: boolean;
selectedRows: T[];
onSearch: (params?: Partial<Q>) => void;
onReset: () => void;
onDelete: (record: T) => Promise<void>;
onBatchDelete: (records: T[]) => Promise<void>;
// ... 其他需要透传的方法
}4.2 构建基础查询表单组件#
创建一个可复用的查询表单组件 src/components/SearchForm/index.tsx。它接收一个表单配置项数组,并自动渲染表单。
import React from 'react';
import { ProForm, ProFormText, ProFormSelect } from '@ant-design/pro-components';
interface SearchFormProps {
onSearch: (values: any) => void;
onReset: () => void;
}
const SearchForm: React.FC<SearchFormProps> = ({ onSearch, onReset }) => {
return (
<ProForm
submitter={{
searchConfig: {
submitText: '查询',
resetText: '重置',
},
onReset: () => {
onReset();
},
}}
onFinish={async (values) => {
onSearch(values);
}}
layout="inline"
>
<ProFormText name="name" label="用户名" placeholder="请输入用户名" />
<ProFormSelect
name="status"
label="状态"
valueEnum={{
1: '启用',
0: '禁用',
}}
placeholder="请选择状态"
/>
{/* 更多的表单项可以根据页面 Props 动态传入 */}
</ProForm>
);
};
export default SearchForm;4.3 封装数据请求层#
在 src/services/ 目录下创建 API 文件。我们使用 Umi 的 request 方法(基于 fetch)。
// src/services/user.ts
import { request } from 'umi';
import type { PaginationResponse, BaseQueryParams } from '@/types/table';
export interface UserItem {
id: number;
name: string;
email: string;
status: number;
createTime: string;
}
export interface UserQueryParams extends BaseQueryParams {
name?: string;
status?: number;
}
export async function queryUserList(params: UserQueryParams) {
return request<PaginationResponse<UserItem>>('/api/users', {
method: 'GET',
params,
});
}
export async function deleteUser(id: number) {
return request(`/api/users/${id}`, {
method: 'DELETE',
});
}4.4 实现核心 Hook:useTablePro#
这是封装的核心,我们创建一个自定义 Hook src/hooks/useTablePro.ts 来管理所有状态和逻辑。
import { useState, useCallback, useRef } from 'react';
import { message } from 'antd';
import type { PaginationResponse, BaseQueryParams } from '@/types/table';
interface UseTableProOptions<T, Q extends BaseQueryParams> {
// 获取数据的函数
fetchData: (params: Q) => Promise<PaginationResponse<T>>;
// 默认的查询参数
defaultQueryParams?: Q;
}
export default function useTablePro<T, Q extends BaseQueryParams>({
fetchData,
defaultQueryParams,
}: UseTableProOptions<T, Q>) {
// 状态定义
const [queryParams, setQueryParams] = useState<Q>({
current: 1,
pageSize: 10,
...defaultQueryParams,
} as Q);
const [tableData, setTableData] = useState<PaginationResponse<T>>();
const [loading, setLoading] = useState(false);
const [selectedRows, setSelectedRows] = useState<T[]>([]);
// 用于强制刷新表格的引用
const fetchIdRef = useRef(0);
// 核心:获取表格数据
const getTableData = useCallback(async () => {
const fetchId = ++fetchIdRef.current;
setLoading(true);
try {
const response = await fetchData(queryParams);
// 防止旧的请求覆盖新的请求结果
if (fetchId === fetchIdRef.current) {
setTableData(response);
}
} catch (error) {
message.error('获取数据失败');
console.error(error);
} finally {
if (fetchId === fetchIdRef.current) {
setLoading(false);
}
}
}, [fetchData, queryParams]);
// 初始化或查询条件变化时自动获取数据
// 你可以使用 useEffect 触发,或在页面组件中手动调用 onSearch
// useEffect(() => { getTableData(); }, [getTableData]);
// 搜索
const onSearch = useCallback((newParams?: Partial<Q>) => {
setQueryParams((prev) => ({
...prev,
...newParams,
current: 1, // 搜索时重置到第一页
}));
// 注意:这里不直接调用 getTableData,由 useEffect 依赖项触发更安全
}, []);
// 重置
const onReset = useCallback(() => {
setQueryParams({
current: 1,
pageSize: 10,
...defaultQueryParams,
} as Q);
}, [defaultQueryParams]);
// 删除单条记录
const onDelete = useCallback(
async (record: T) => {
try {
// 这里需要根据实际情况调用删除 API
// await deleteUser((record as any).id);
message.success('删除成功');
getTableData(); // 刷新表格
} catch (error) {
message.error('删除失败');
}
},
[getTableData]
);
// 批量删除
const onBatchDelete = useCallback(
async (records: T[]) => {
if (records.length === 0) {
message.warning('请至少选择一条记录');
return;
}
try {
// const ids = records.map((r: any) => r.id);
// await batchDeleteUser(ids);
message.success('批量删除成功');
setSelectedRows([]);
getTableData();
} catch (error) {
message.error('批量删除失败');
}
},
[getTableData]
);
// 处理表格变化(分页、排序、筛选)
const onTableChange = (pagination: any, filters: any, sorter: any) => {
setQueryParams((prev) => ({
...prev,
current: pagination.current,
pageSize: pagination.pageSize,
...filters,
...(sorter.field && { sorter: `${sorter.field}_${sorter.order}` }),
}));
};
return {
// 状态
queryParams,
tableData,
loading,
selectedRows,
// 方法
setQueryParams,
setSelectedRows,
getTableData,
onSearch,
onReset,
onDelete,
onBatchDelete,
onTableChange,
};
}4.5 创建高阶组件 withTablePro#
虽然 Hook 很流行,但高阶组件对于某些场景(如装饰器模式)依然清晰。我们可以选择性地实现一个 HOC。
// src/hocs/withTablePro.tsx
import React from 'react';
import useTablePro from '@/hooks/useTablePro';
import type { TableProProps, BaseQueryParams, PaginationResponse } from '@/types/table';
function withTablePro<T, Q extends BaseQueryParams>(
fetchData: (params: Q) => Promise<PaginationResponse<T>>,
defaultQueryParams?: Q
) {
return (WrappedComponent: React.ComponentType<TableProProps<T, Q>>) => {
return (props: any) => {
const tablePro = useTablePro<T, Q>({ fetchData, defaultQueryParams });
return <WrappedComponent {...props} {...tablePro} />;
};
};
}
export default withTablePro;五、实践案例:用户管理页面#
现在,我们来使用上面封装的 Hook 快速创建一个用户列表页面 src/pages/user/index.tsx。
import React, { useEffect } from 'react';
import { ProTable } from '@ant-design/pro-components';
import { Button, Space, Popconfirm, message } from 'antd';
import { PlusOutlined } from '@ant-design/icons';
import type { ProColumns } from '@ant-design/pro-components';
import useTablePro from '@/hooks/useTablePro';
import SearchForm from '@/components/SearchForm';
import { queryUserList, deleteUser, UserItem, UserQueryParams } from './service';
const UserList: React.FC = () => {
// 使用封装的 Hook,传入 API 和默认参数
const {
queryParams,
tableData,
loading,
selectedRows,
setSelectedRows,
onSearch,
onReset,
onDelete,
onBatchDelete,
onTableChange,
getTableData,
} = useTablePro<UserItem, UserQueryParams>({
fetchData: queryUserList,
defaultQueryParams: { status: 1 },
});
// 组件挂载后手动触发数据获取,或者可以在 useTablePro 中使用 useEffect
useEffect(() => {
getTableData();
}, [queryParams]); // 依赖 queryParams,当其变化时自动重新获取
// 表格列配置
const columns: ProColumns<UserItem>[] = [
{
title: 'ID',
dataIndex: 'id',
key: 'id',
width: 80,
},
{
title: '用户名',
dataIndex: 'name',
key: 'name',
},
{
title: '邮箱',
dataIndex: 'email',
key: 'email',
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
valueEnum: {
1: { text: '启用', status: 'Success' },
0: { text: '禁用', status: 'Error' },
},
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
valueType: 'dateTime',
},
{
title: '操作',
key: 'action',
width: 200,
render: (_, record) => (
<Space>
<Button size="small" type="link">
编辑
</Button>
<Popconfirm
title="确定要删除吗?"
onConfirm={() => onDelete(record)}
okText="确定"
cancelText="取消"
>
<Button size="small" type="link" danger>
删除
</Button>
</Popconfirm>
</Space>
),
},
];
return (
<div>
{/* 查询表单 */}
<SearchForm
onSearch={(values) => onSearch(values)}
onReset={onReset}
/>
{/* 操作按钮栏 */}
<ProTable<UserItem>
headerTitle="用户列表"
rowKey="id"
columns={columns}
dataSource={tableData?.list || []}
loading={loading}
pagination={{
current: queryParams.current,
pageSize: queryParams.pageSize,
total: tableData?.total || 0,
showSizeChanger: true,
showQuickJumper: true,
}}
rowSelection={{
selectedRowKeys: selectedRows.map((item) => item.id),
onChange: (_, selectedRows) => {
setSelectedRows(selectedRows);
},
}}
tableAlertRender={({ selectedRowKeys, onCleanSelected }) => (
<span>
已选 {selectedRowKeys.length} 项
<a style={{ marginLeft: 8 }} onClick={onCleanSelected}>
取消选择
</a>
</span>
)}
tableAlertOptionRender={({ onCleanSelected }) => (
<Button type="link" danger onClick={() => onBatchDelete(selectedRows)}>
批量删除
</Button>
)}
toolBarRender={() => [
<Button key="add" type="primary" icon={<PlusOutlined />}>
新建用户
</Button>,
]}
onChange={onTableChange}
/>
</div>
);
};
export default UserList;六、最佳实践与常见问题#
最佳实践#
- TypeScript 化:始终为接口、组件 Props、函数参数等定义清晰的类型,这是长期维护的基石。
- 关注点分离:将数据获取逻辑(Service)、UI 展示逻辑(Page/Component)和通用业务逻辑(Hook/HOC)清晰地分离。
- 适度的抽象:不要过度封装。
useTablePro应保持核心通用功能,对于特定页面的特殊逻辑,应在页面组件中实现,或通过传入配置项来扩展。 - 错误处理:在
fetchData和操作函数中要有完善的错误捕获和用户提示(如message)。 - 性能优化:使用
useCallback和useMemo避免不必要的重渲染。useRef用于处理竞态请求。
常见问题#
- Q: 请求竞态问题如何解决?
A: 我们在
useTablePro中使用了useRef来生成一个唯一的fetchId,确保只有最后一次请求的结果会被渲染。 - Q: 如何添加更复杂的查询表单?
A: 可以改造
SearchForm组件,使其接收一个fields配置数组,根据配置动态渲染不同的ProForm组件。 - Q: 如何集成新建/编辑的模态框?
A: 可以在
useTablePro中添加visible,editingRecord等状态,并提供onCreate,onEdit方法。或者在页面组件中单独管理模态框状态,只利用 Hook 管理表格数据。 - Q: 想用 React Query 替代 Dva/useEffect 做数据获取可以吗?
A: 完全可以。Umi 社区有
@umijs/plugins/dist/react-query插件。你可以将useTablePro中的useEffect数据获取逻辑替换为useQuery,并将删除等操作改为useMutation,这样可以获得更强大的缓存和同步能力。
七、总结#
通过本文的步骤,我们成功地基于 Umi 框架搭建了一个高效的 React 开发环境,并核心封装了一个功能强大的 useTablePro Hook(及可选的 withTablePro HOC)。这个封装将列表页的通用逻辑(状态、数据获取、操作)高度抽象,使开发者能够专注于页面本身的配置和业务细节。
这套方案的优势在于:
- 开发效率极高:新建一个标准列表页只需配置 columns 和 API。
- 一致性:所有列表页遵循相同的模式和交互,便于维护和理解。
- 可扩展性:Hook 的设计使其易于扩展以满足更复杂的业务场景。
希望这篇详细的指南能帮助你搭建属于自己的前端快速开发框架。