RefineuseImportHook 全解析:从 CSV 文件到数据落库的完整导入链路
【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine
useImport是 Refine 提供的数据导入 Hook,它让开发者只需几行代码就能把 CSV 文件中的内容解析并批量写入数据源。本文以版本化文档中针对 Ant Design 集成的useImport指南为核心,结合本仓库中@refinedev/core与@refinedev/antd的真实源码与测试用例,完整讲解其配置属性、返回值、关联数据处理实战以及底层实现原理,帮助你掌握在 Refine 管理后台中构建"一键导入"能力的完整方案。
一、Hook 定位:从文件到数据源的桥梁
useImport允许你从CSV文件中导入数据:对于文件中的每一行,它会根据你的配置调用 data provider 的create或createMany方法。内部它使用 Papa Parse 解析文件内容,并返回与 Ant Design<Upload>和<Button>组件兼容的属性。
该 Hook 是从@pankod/refine-core(当前仓库中为 @refinedev/core)的useImportHook 扩展而来,因此你可以使用 core 版本的全部能力。core 版本文档见 useImport(core),而 Ant Design 集成版的完整实现位于 packages/antd/src/hooks/import/index.tsx。
二、快速上手:两种基础用法
1. 与<ImportButton>组合使用
最简单的用法是直接将useImport的返回值展开到<ImportButton>上:
import { useImport, ImportButton } from "@pankod/refine-antd"; export const PostList: React.FC = () => { const importProps = useImport(); return <ImportButton {...importProps}>Import</ImportButton>; };关于<ImportButton>的完整接口,可参考 ImportButton 文档。在源码层面,ImportButton 内部就是用 Ant Design 的<Upload>包裹<Button>,并带上了ImportOutlined图标,其label文本由useImportButton()提供,hideText属性可控制是否隐藏文字。
2. 不使用<ImportButton>,自行组合组件
如果你需要更多定制,可以直接使用buttonProps与uploadProps手动组合:
import { useImport, Upload, Button } from "@pankod/refine-antd"; export const PostList: React.FC = () => { const { buttonProps, uploadProps } = useImport(); return ( <Upload {...uploadProps}> <Button {...buttonProps}>Import</Button> </Upload> ); };三、配置属性详解
resourceName
默认值:从当前 URL 读取
resource值
决定将哪个资源传递给 data provider 的create或createMany方法:
useImport({ resourceName: "posts", });需要说明的是,文档中以resourceName命名该参数;在 packages/core/src/hooks/import/index.tsx 的ImportOptions类型与实现中,该配置项的字段名是resource,最终通过useResourceParams({ resource })解析出资源名与标识符(identifier),再传给create/createMany。core 的测试用例 index.spec.tsx 验证了传入resource: "tests"时,createMany会以resource: "tests"发起请求。
mapData
在数据发送给 data provider 之前对其进行映射:
useImport({ mapData: (data) => ({ ...data, category: { id: data.categoryId, }, }), });paparseOptions
可以向paparseOptions传入任意 Papa Parse 配置项(类型为papaparse.ParseConfig),例如header、delimiter、skipEmptyLines等:
useImport({ paparseOptions: { header: true, }, });在 core 实现中,这些选项通过...paparseOptions展开传入papaparse.parse(file, { complete, ...paparseOptions })(见 packages/core/src/hooks/import/index.tsx)。
batchSize
默认值:
Number.MAX_SAFE_INTEGER
批量发送数据的大小。当batchSize为 1 时,对文件中的每一行调用 data provider 的create方法;当batchSize大于 1 时,对每个批次调用createMany方法:
useImport({ batchSize: 1, });重要前提:当batchSize大于 1 时,你的 data provider 必须实现了createMany方法(core 源码中的注释也明确说明了这一点,见 packages/core/src/hooks/import/index.tsx)。此外,默认值Number.MAX_SAFE_INTEGER意味着默认情况下所有数据会一次性全部提交(即调用一次createMany)。
onFinish
导入全部结束后触发的回调,返回包含succeeded与errored两个数组的对象,分别存放成功与失败请求的响应:
useImport({ onFinish: (result) => { // 成功请求的响应 result.succeeded.forEach((item) => { console.log(item); }); // 失败请求的响应 result.errored.forEach((item) => { console.log(item); }); }, });从源码看,succeeded与errored的元素类型为ImportSuccessResult/ImportErrorResult,其中request保存本次请求发送的原始数据、response保存响应数据(成功时为TData[],失败时为HttpError[]),类型定义见 packages/core/src/hooks/import/index.tsx。
metaData
向 data provider 的create或createMany方法发送额外的元数据(如关系字段、自定义请求头等):
useImport({ metaData: { foo: "bar", }, });在 core 实现 中,meta(即文档中的metaData)会经useMeta()与资源自身的 meta 合并为combinedMeta,随后传递给create/createMany的meta参数。
onProgress
导入进度变化时的回调,返回totalAmount(总行数)与processedAmount(已处理行数):
useImport({ onProgress: ({ totalAmount, processedAmount }) => { // 进度百分比 console.log((processedAmount / totalAmount) * 100); }, });默认行为:如果不传onProgress,Ant Design 版本默认会弹出一个带圆形进度条的通知(notification.open),提示Importing: processedAmount/totalAmount,进度完成后约 4.5 秒自动销毁。该默认逻辑实现在 packages/antd/src/hooks/import/index.tsx,通知的key为${resource}-import。传入自定义onProgress即可覆盖此行为。
dataProviderName
当存在多个 data provider 时,指定使用哪一个。当你为不同资源配置了不同 data provider 时非常有用:
useImport({ dataProviderName: "second-data-provider", });四、返回值详解
buttonProps
与 Ant Design<Button>组件兼容的按钮属性:
import { useImport, Button } from "@pankod/refine-antd"; export const PostList: React.FC = () => { const { buttonProps } = useImport(); return <Button {...buttonProps}>Import</Button>; };type:默认为default。loading:导入进行中时,按钮会进入 loading 状态。
uploadProps
与 Ant Design<Upload>组件兼容的上传属性:
import { useImport, Upload } from "@pankod/refine-antd"; export const PostList: React.FC = () => { const { uploadProps } = useImport(); return <Upload {...uploadProps}>Import</Upload>; };onChange:处理文件上传事件。beforeUpload:默认为() => false,阻止文件被自动上传(CSV 由 hook 内部读取解析,而非直接上传到服务器)。showUploadList:默认为false,隐藏上传文件列表。accept:默认为".csv",仅接受 CSV 文件。
这些默认值在 packages/antd/src/hooks/import/index.tsx 中有明确实现,antd 的测试用例也专门验证了beforeUpload返回false(见 packages/antd/src/hooks/import/index.spec.ts)。
isLoading
布尔值,表示导入是否正在进行中。
mutationResult
useCreate或useCreateMany方法的结果。具体调用哪个取决于batchSize:batchSize === 1时使用useCreate,否则使用useCreateMany(见 packages/core/src/hooks/import/index.tsx)。对应 Hook 文档可参考 useCreateMany。
五、实战:处理关联数据(Relational Data)
有时候,解析出的 CSV 数据需要进一步处理——例如数据中包含关联字段、引用其他数据,或后端 API 要求特定的数据格式。此时可以用mapData来自定义处理过程。
例如,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 要求的格式。mapData可以做到这一点:
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; }执行这段代码后,解析出的数据会被映射为符合 API 要求的格式再提交创建。
这一模式在本仓库的官方示例 import-export-antd 中有完整落地:列表页通过useImport<IPostFile>配合mapData将扁平的categoryId/userId还原为嵌套的category: { id }/user: { id }结构,再配合<ImportButton>与<ExportButton>(useExport)形成导入导出的闭环。示例中IPostFile与IPost的接口定义可参考 examples/import-export-antd/src/interfaces/index.d.ts。
六、底层实现原理:数据是如何一步步写入数据源的
1. 解析:Papa Parse +importCSVMapper
当用户选择 CSV 文件后,antd 版本的uploadProps.onChange会调用 core 返回的handleChange(见 packages/antd/src/hooks/import/index.tsx)。在 core 内部,handleChange首先调用papaparse.parse(file, { complete, ...paparseOptions }),随后通过importCSVMapper(data, mapData)把解析出的二维数组转换为对象数组:
export const importCSVMapper = <TItem = any, TVariables = any>( data: any[][], mapData: MapDataFn<TItem, TVariables> = (item) => item as any, ): TVariables[] => { const [headers, ...body] = data; return body .map((entry) => fromPairs(zip(headers, entry))) .map((item: any, index, array: any) => mapData.call(undefined, item, index, array), ); };实现位于 packages/core/src/definitions/helpers/importCSVMapper/index.ts:第一行作为表头(headers),用zip将表头与每一行配对、再用fromPairs转为对象,最后对每个对象调用mapData(回调还能拿到index与完整数组)。
2. 分批与提交:batchSize决定走create还是createMany
解析完成后,core 会setTotalAmount(values.length),然后按batchSize分支处理:
batchSize === 1:为每一行构造一个create.mutateAsync({ resource, values, successNotification: false, errorNotification: false, dataProviderName, meta: combinedMeta })任务;batchSize > 1:用 lodash 的chunk(values, batchSize)把数据切块,为每个块构造一个createMany.mutateAsync(...)任务。
注意这里显式关闭了successNotification与errorNotification(防止每个批次都弹出成功/失败通知),并顺序执行所有任务,每完成一个任务就把processedAmount增加对应数量(单行模式 +1,批量模式 +当前批次长度)。
3. 顺序执行:sequentialPromises
无论单行还是批量,请求都是**顺序(串行)**发起的,而不是并发。这依赖 sequentialPromises:
for (const [index, promise] of promises.entries()) { try { const result = await promise(); results.push(onEachResolve(result, index)); } catch (error) { results.push(onEachReject(error as TReject, index)); } }它逐个await每个任务,成功与失败都会被收集为统一的结果数组,从而实现对大规模数据导入的节奏控制,也能精确统计成功与失败。
4. 收尾:进度通知与onFinish
processedAmount/totalAmount的变化会通过useEffect触发onProgress回调;所有任务执行完毕后,handleFinish将结果按type === "success"/"error"过滤为{ succeeded, errored },调用onFinish并复位isLoading(见 packages/core/src/hooks/import/index.tsx)。
5. 测试用例佐证
core 的测试(packages/core/src/hooks/import/index.spec.tsx)覆盖了关键行为:
batchSize: 1时,create被按行调用 3 次,且参数与解析出的每行数据一致;batchSize: 2时,createMany按两行一批被调用,variables为对应分块;batchSize未设置(undefined)时走createMany,且onFinish中succeeded[0].request等于全部解析数据;- data provider 返回 rejected 时,
onFinish的errored[0].response[0]能拿到HttpError(如statusCode: 500); mapData会在提交前完成字段映射;onProgress最终以{ totalAmount: 3, processedAmount: 3 }收尾。
antd 的测试(packages/antd/src/hooks/import/index.spec.ts)则验证了uploadProps.beforeUpload返回false,以及导入过程中会打开进度通知并在结束后关闭。
七、API Reference
Properties
| 属性 | 类型/默认值 | 说明 |
|---|---|---|
| resourceName | string(默认读当前 URL) | 传递给create/createMany的资源名 |
| mapData | MapDataFn<TItem, TVariables> | 每条解析记录的映射函数 |
| paparseOptions | papaparse.ParseConfig | 透传给 Papa Parse 的解析配置 |
| batchSize | number(默认Number.MAX_SAFE_INTEGER) | 1 时逐行create,>1 时按批createMany |
| onFinish | (results) => void | 全部请求结束后回调,返回{ succeeded, errored } |
| metaData | MetaQuery | 附加元数据,随请求传给 data provider |
| onProgress | ({ totalAmount, processedAmount }) => void | 进度回调,默认弹进度通知 |
| dataProviderName | string | 指定使用哪个 data provider(多 provider 场景) |
Return Values
| 属性 | 说明 | 类型 |
|---|---|---|
| buttonProps | 与 Ant Design<Button>组件兼容的属性 | ButtonProps |
| uploadProps | 与 Ant Design<Upload>组件兼容的属性 | UploadProps |
| isLoading | 可用于处理 Import 操作的 loading 状态 | boolean |
| mutationResult | 创建导入资源的 mutation/mutations 的结果 | UseMutationResult<{ data: TData }, TError, { resource: string; values: TVariables }, unknown>|UseMutationResult<{ data: TData[] }, TError, { resource: string; values: TVariables[] }, unknown> |
Type Parameters
| 属性 | 说明 | 默认值 |
|---|---|---|
| TItem | 解析后的 CSV 数据接口 | any |
| TData | 继承BaseRecord的查询结果类型 | BaseRecord |
| TError | 继承HttpError的自定义错误对象 | HttpError |
| TVariables | mutation 函数的参数类型 | any |
相关的核心类型与概念可进一步参考 data provider 文档,以及 core 版的 useImport 文档 中关于BaseRecord、HttpError的说明。
结语
通过useImport,Refine 将"CSV 解析 → 字段映射 → 分批写入 → 进度反馈 → 结果汇总"这一完整链路封装成了一个声明式 Hook:只需把它接入 Ant Design 的<Upload>/<Button>或现成的<ImportButton>,再配合mapData处理关联数据、batchSize控制写入粒度、onFinish汇总成败结果,即可在管理后台快速交付稳定可靠的批量数据导入功能。深入理解其底层对 Papa Parse、importCSVMapper与sequentialPromises的运用,也能帮助你在面对大文件、复杂关系映射或自定义数据源时,做出更合理的取舍与扩展。
【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考