Refine v5 的 useImport Hook 完全指南:从 CSV 批量导入到数据 Provider 的底层实现
【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine
useImport是 Refine 的 Ant Design 集成(@refinedev/antd)中用于从 CSV 文件批量导入数据到管理后台的核心 Hook。它逐行(或按批次)调用数据提供者的create/createMany方法,内部借助 Papa Parse 完成 CSV 解析,并返回与 Ant Design<Upload>、<Button>组件直接兼容的属性。阅读本文后,你将掌握 useImport 的全部配置项、返回值、关系数据映射(mapData)实战技巧,以及从@refinedev/core到@refinedev/antd的完整调用链与底层实现原理。
本文以 documentation/docs/ui-integrations/ant-design/hooks/use-import/index.md 为骨架,并结合 packages/antd/src/hooks/import/index.tsx、packages/core/src/hooks/import/index.tsx 等仓库源码展开。
useImport 是什么
useImportHook 允许你从一个CSV文件导入数据。对于文件中的每一行,它会根据你的配置调用数据提供者的create或createMany方法。内部使用 Papa Parse 解析文件内容,返回值与 Ant Design 的<Upload>与<Button>组件完全兼容。
从包结构看,@refinedev/antd的useImport是对@refinedev/core中同名 Hook 的扩展封装(antd 封装位于 packages/antd/src/hooks/import/index.tsx,core 实现位于 packages/core/src/hooks/import/index.tsx)。这意味着 core 版本的所有能力你都可以使用,antd 版本在其之上额外提供了:
- 与 Ant Design 组件体系对接的
uploadProps/buttonProps; - 默认的导入进度通知(基于 Ant Design 的
notification与Progress组件,见 packages/antd/src/hooks/import/index.tsx)。
也就是说,@refinedev/antd版本面向的是"接入 Ant Design 界面"的场景;如果你在无 UI 依赖(headless)的环境下工作,可以直接使用 core 版本。
基础用法
配合 ImportButton 使用(推荐)
最简单的用法是配合@refinedev/antd提供的<ImportButton>组件:
import { ImportButton, useImport } from "@refinedev/antd"; export const PostList: React.FC = () => { const importProps = useImport(); return <ImportButton {...importProps}>Import</ImportButton>; };<ImportButton>内部用一个<Upload>包裹<Button>,并内置了导入图标(ImportOutlined)、data-testid与 className(见 packages/antd/src/components/buttons/import/index.tsx)。它的属性类型ImportButtonProps要求分别传入uploadProps和buttonProps(见 packages/antd/src/components/buttons/types.ts),这恰好与useImport的返回值一一对应,因此可以直接展开传递。
更多关于
<ImportButton>的细节,可参考 documentation/docs/ui-integrations/ant-design/components/buttons/import-button/index.md。
不使用 ImportButton 的自定义用法
如果你需要更自由的定制,可以只取uploadProps和buttonProps两个属性,自行组合 Ant Design 的<Upload>与<Button>:
import { useImport } from "@refinedev/antd"; import { Upload, Button } from "antd"; export const PostList: React.FC = () => { const { buttonProps, uploadProps } = useImport(); return ( <Upload {...uploadProps}> <Button {...buttonProps}>Import</Button> </Upload> ); };uploadProps已经替你处理了文件选择、禁用自动上传、隐藏上传列表、限定.csv扩展名等细节(见下文"返回值的内部细节"一节),因此这里只需要把属性展开即可。
Properties 配置项详解
resource
resource决定数据提供者的create/createMany方法将被调用在哪个资源上。默认情况下,它从当前 URL 路由推断资源名(antd 版本通过useResourceParams解析,见 packages/antd/src/hooks/import/index.tsx)。
useImport({ resource: "posts", });如果你有多个同名资源,可以传入identifier而非name。identifier只作为资源匹配的主键,而数据提供者方法仍使用<Refine/>组件中定义的name。从实现上看,antd 版本会优先取resource?.identifier ?? resource?.name传给 core 版本(见 packages/antd/src/hooks/import/index.tsx)。
关于
identifier的完整说明,参考 documentation/docs/core/refine-component/index.md 中的 identifier 一节。
mapData
在将数据发送给数据提供者方法之前,如果你想对解析出的每一行做变换,可以使用mapData:
useImport({ mapData: (data) => ({ ...data, category: { id: data.categoryId, }, }), });从源码看,mapData的默认值是恒等函数(item) => item as unknown as TVariables(见 packages/core/src/hooks/import/index.tsx)。它的完整签名是MapDataFn<TItem, TVariables>,调用时会被传入(item, index, array)三个参数(见 packages/core/src/definitions/helpers/importCSVMapper/index.ts),因此你还可以基于行号或整表数据进行变换。
paparseOptions
你可以把任意 Papa Parse 配置项 传给paparseOptions:
useImport({ paparseOptions: { header: true, }, });在 core 实现中,这些选项会原样展开传给papaparse.parse(file, { complete, ...paparseOptions })(见 packages/core/src/hooks/import/index.tsx)。其类型为papaparse.ParseConfig(见 packages/core/src/hooks/import/index.tsx),常见的用法还包括自定义分隔符(delimiter)、跳过空行(skipEmptyLines)、错误处理回调(error)等。
batchSize
batchSize控制请求的批处理方式,是理解 useImport 行为的关键配置:
batchSize === 1:对文件中每一行调用一次create方法;batchSize > 1:将数据按该大小切成多个块,对每个块调用一次createMany方法;- 默认值为
Number.MAX_SAFE_INTEGER(见 packages/core/src/hooks/import/index.tsx),即默认一次性把全部行塞进一次createMany调用。
useImport({ batchSize: 1, });在 core 实现中,batchSize === 1时会对每一行构造一个create.mutateAsync调用,然后通过sequentialPromises顺序执行(逐个发起请求并累计进度);否则使用lodash/chunk切块后,同样以顺序方式逐批调用createMany.mutateAsync(见 packages/core/src/hooks/import/index.tsx)。注意"顺序执行"意味着大批量文件不会并发打爆后端,但耗时也会线性增长。
如果batchSize > 1,你的数据提供者必须实现createMany,这一点在 core 的类型注释中也有明确说明(见 packages/core/src/hooks/import/index.tsx)。此外,batchSize取undefined时行为与默认一致(走createMany全量一次),这一点被测试用例专门覆盖(见 packages/core/src/hooks/import/index.spec.tsx)。
onFinish
在导入流程全部结束后触发,用于处理成功与失败的结果。回调参数是一个包含succeeded与errored两个数组的对象,分别存放成功与失败请求的响应:
useImport({ onFinish: (result) => { // success requests response result.succeeded.forEach((item) => { console.log(item); }); // failed requests response result.errored.forEach((item) => { console.log(item); }); }, });对应类型定义在 packages/core/src/hooks/import/index.tsx:
succeeded: ImportSuccessResult[],每个元素包含request(本次请求的原始值)、type: "success"与response(TData[]);errored: ImportErrorResult[],每个元素包含request、type: "error"与response(HttpError[])。
注意:使用onFinish时不要遗漏await,因为handleChange返回的是一个Promise。在测试中,onFinish也被用来断言传入数据提供者的create/createMany参数(见 packages/core/src/hooks/import/index.spec.tsx)。
meta
如果你想向create/createMany方法发送额外的元数据,可以使用meta:
useImport({ meta: { foo: "bar", }, });从 core 实现看,meta会先经过useMeta()与资源信息合并成combinedMeta,再作为meta参数传给create/createMany的 mutation(见 packages/core/src/hooks/import/index.tsx)。这在需要向 API 传递select、custom等数据提供者特定参数时很有用。
onProgress
导入进度变化时触发的回调,参数为{ totalAmount, processedAmount },分别表示总行数与已处理行数:
useImport({ onProgress: ({ totalAmount, processedAmount }) => { // progress percentage console.log((processedAmount / totalAmount) * 100); }, });core 版本通过useEffect在totalAmount/processedAmount状态变化时触发该回调(见 packages/core/src/hooks/import/index.tsx)。antd 版本默认会展示一个包含进度百分比的通知(环形Progress+ "Importing: x/y" 文案,完成 4.5 秒后自动关闭),你可以通过传入自定义onProgress覆盖这一默认行为(见 packages/antd/src/hooks/import/index.tsx)。在 packages/core/src/hooks/import/index.spec.tsx 中,onProgress被断言会收到{ totalAmount: 3, processedAmount: 3 }这样的最终值。
dataProviderName
当你配置了多个dataProvider时,用dataProviderName指定本次导入使用哪一个:
useImport({ dataProviderName: "second-data-provider", });它会被原样传给 mutation 的dataProviderName参数(见 packages/core/src/hooks/import/index.tsx),适用于不同资源由不同数据提供者服务的场景。
Return Values 返回值
useImport(antd 版本)返回uploadProps、buttonProps、isLoading与mutation。类型上,它去掉了 core 版本的handleChange/inputProps,替换为面向 Ant Design 的uploadProps/buttonProps(见 packages/antd/src/hooks/import/index.tsx)。
buttonProps
与 Ant Design<Button>组件兼容的按钮属性:
import { useImport } from "@refinedev/antd"; import { Button } from "antd"; export const PostList: React.FC = () => { const { buttonProps } = useImport(); return <Button {...buttonProps}>Import</Button>; };内部细节:
type:默认为"default";loading:导入进行中时为true,用于切换按钮的加载态。
(见 packages/antd/src/hooks/import/index.tsx。)
uploadProps
与 Ant Design<Upload>组件兼容的上传属性:
import { useImport } from "@refinedev/antd"; import { Upload } from "antd"; export const PostList: React.FC = () => { const { uploadProps } = useImport(); return <Upload {...uploadProps}>Import</Upload>; };内部细节(见 packages/antd/src/hooks/import/index.tsx):
| 属性 | 默认值 | 说明 |
|---|---|---|
onChange | handleChange | 处理文件上传,触发 CSV 解析与导入流程 |
beforeUpload | () => false | 阻止文件被自动上传,导入逻辑完全由本 Hook 接管 |
showUploadList | false | 隐藏上传文件列表 |
accept | ".csv" | 只允许选择 CSV 文件 |
isLoading
布尔值,表示导入是否正在进行(见 packages/antd/src/hooks/import/index.tsx)。core 实现中它由handleChange开始时置true、handleFinish与handleCleanup时复位(见 packages/core/src/hooks/import/index.tsx)。测试中也会通过waitFor等待isLoading回到false来确认流程结束(见 packages/core/src/hooks/import/index.spec.tsx)。
mutation
useCreate或useCreateMany的结果(取决于batchSize)。core 实现中会根据batchSize === 1选择useCreate,否则选择useCreateMany(见 packages/core/src/hooks/import/index.tsx)。类型上它是两种UseMutationResult的联合(见 packages/core/src/hooks/import/index.tsx):
UseMutationResult<{ data: TData }, TError, { resource: string; values: TVariables; }, unknown> | UseMutationResult<{ data: TData[] }, TError, { resource: string; values: TVariables[]; }, unknown>关于
useCreate/useCreateMany的细节,可参考 documentation/docs/data/hooks/use-create/index.md 与 documentation/docs/data/hooks/use-create-many/index.md。
FAQ:关系数据的处理
导入时经常遇到的一个场景是:CSV 中存放的是外键 ID,而你的后端 API 要求的是嵌套对象结构。此时可以用mapData完成"扁平 CSV → 嵌套 API 结构"的还原。
假设你的 CSV 文件内容如下:
"title","content","status","categoryId","userId" "dummy title 1","dummy content 1","rejected","3","8" "dummy title 2","dummy content 2","draft","44","8" "dummy title 3","cummy content 3","published","41","10"由于user和category是关系字段,导出时只保存了它们的 ID(userId、categoryId)。要基于该文件重建资源,需要把数据映射回后端 API 要求的格式:
useImport<IPostFile>({ mapData: (item) => { return { title: item.title, content: item.content, status: item.status, category: { id: item.categoryId, }, user: { id: item.userId, }, }; }, }); interface IPostFile { title: string; status: string; content: string; categoryId: string; userId: string; }通过mapData,解析出的每行数据在发送给数据提供者之前会被转换成{ title, content, status, category: { id }, user: { id } }结构,从而满足后端 API 的约束。泛型参数TItem(此处为IPostFile)让这一变换过程具备类型安全。
从底层看,CSV 解析结果先经过importCSVMapper处理:第一行作为表头,其余每行与表头通过zip+fromPairs组合成对象,再依次交给mapData变换(见 packages/core/src/definitions/helpers/importCSVMapper/index.ts)。所以mapData收到的item已经是以表头为 key 的对象。
API Reference 速览
完整属性表
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
resource | string | 从路由读取的资源名 | 指定调用create/createMany的资源 |
mapData | MapDataFn<TItem, TVariables> | (item) => item | 发送前对每行解析结果做变换 |
paparseOptions | papaparse.ParseConfig | - | 传给 Papa Parse 的解析选项 |
batchSize | number | Number.MAX_SAFE_INTEGER | 为 1 时逐行调用create,大于 1 时按批调用createMany |
onFinish | (results) => void | - | 全部请求结束后回调,含succeeded/errored |
meta | MetaQuery | - | 传给数据提供者方法的额外元数据 |
onProgress | (params) => void | antd 默认进度通知 | 进度回调,参数为{ totalAmount, processedAmount } |
dataProviderName | string | - | 多数据提供者时指定使用哪一个 |
返回值表
| 属性 | 说明 | 类型 |
|---|---|---|
buttonProps | 与 Ant Design<Button>兼容的属性 | ButtonProps |
uploadProps | 与 Ant Design<Upload>兼容的属性 | UploadProps |
isLoading | 导入进行中的 loading 状态 | boolean |
mutation | 创建导入资源的 mutation/mutations 结果 | UseMutationResult<{ data: TData }, ...>|UseMutationResult<{ data: TData[] }, ...>(见上文"mutation"一节) |
类型参数
| 类型参数 | 说明 | 默认值 |
|---|---|---|
TItem | 解析后的 CSV 数据接口 | any |
TData | 数据查询结果类型,继承BaseRecord | BaseRecord |
TError | 自定义错误对象,继承HttpError | HttpError |
TVariables | mutation 函数的入参类型 | any |
从源码看完整导入流程
把上面所有信息串起来,一次导入的完整生命周期如下(依据 packages/core/src/hooks/import/index.tsx 的handleChange实现):
- 触发:antd 的
<Upload>触发onChange(即handleChange),beforeUpload返回false阻止自动上传; - 重置状态:
handleCleanup将totalAmount/processedAmount归零、isLoading置true; - 解析:
papaparse.parse读取文件,complete回调拿到原始二维数组;importCSVMapper依据表头将其转换为对象数组,并应用mapData(见 packages/core/src/definitions/helpers/importCSVMapper/index.ts); - 分批:
totalAmount记录总行数;若batchSize === 1则每行一个请求(create),否则用lodash/chunk切块后每块一个请求(createMany),并通过sequentialPromises顺序执行,每完成一个请求/批次就更新processedAmount,从而驱动onProgress; - 收尾:所有请求完成后,按
type拆分为succeeded/errored传给onFinish,isLoading复位;antd 版本在完成 4.5 秒后关闭默认进度通知。
对应行为都有测试覆盖,例如 packages/core/src/hooks/import/index.spec.tsx 验证了batchSize: 2时createMany按两行一批被调用、batchSize: 1时create逐行被调用、mapData在请求前生效、onFinish能收到成功/失败结果、onProgress收到最终进度等。antd 侧的<ImportButton />测试则直接复用@refinedev/ui-tests的buttonImportTests(见 packages/antd/src/components/buttons/import/index.spec.tsx),保证组件与 Hook 的行为稳定。
小结
useImport将"选择 CSV → 解析 → 变换 → 分批写入数据提供者 → 展示进度与结果"这条完整链路封装在一个 Hook 中:配置层面有resource、mapData、paparseOptions、batchSize、onFinish、meta、onProgress、dataProviderName八个选项;返回层面有buttonProps、uploadProps、isLoading、mutation四个值,与 Ant Design 组件开箱即用。理解其"batchSize 决定 create / createMany 切换、sequentialPromises 顺序执行、mapData 在 importCSVMapper 内按行变换"这三条实现事实,能帮助你在处理大文件批导入、多数据提供者、关系数据还原等真实场景时做出正确的配置决策。
【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考