news 2026/9/13 2:48:33

Refine 中实现 Multipart 文件上传:Ant Design Upload 集成、上传端点设计与 useFileUploadState 源码解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Refine 中实现 Multipart 文件上传:Ant Design Upload 集成、上传端点设计与 useFileUploadState 源码解析

Refine 中实现 Multipart 文件上传:Ant Design Upload 集成、上传端点设计与 useFileUploadState 源码解析

【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine

本篇技术文章围绕 Refine 官方文档中的 Multipart Upload 指南展开,讲解如何在 Refine + Ant Design 管理后台中实现文件的多部件(multipart/form-data)上传:包括在创建/编辑表单中接入Upload.Dragger、服务端上传端点的请求与响应契约、表单提交时图片数据的流转方式,以及利用useFileUploadStateHook 在上传过程中禁用保存按钮。读完本文,你将掌握一套可直接落地的 Refine 文件上传方案,并能从源码层面理解getValueFromEventuseFileUploadStateuseApiUrl三个关键符号的实际实现。

什么是 Multipart 上传

Multipart 请求是 HTTP 客户端用来向服务器发送文件和数据的一类请求,浏览器和各类 HTTP 客户端上传文件时普遍采用这种机制。与 Base64 内联上传不同,multipart 上传把二进制文件以multipart/form-data的编码形式单独发送到上传专用端点,文件不经过业务数据的 CRUD 请求通道,而是先落到媒体存储(或对象存储)中拿到一个可访问 URL,业务表单中只保存 URL 等元数据。

Refine 官方给出的完整指南位于 multipart-upload.md,配套的可运行示例工程位于 examples/upload-antd-multipart,下文将完整继承该文档的操作步骤,并结合当前仓库的包源码进行纵深扩充。

整体流程分为三段:

  1. 创建/编辑表单:通过 Ant Design 的Upload.Dragger组件接收文件,并以 multipart/form-data 方式把文件直接 POST 到上传端点;
  2. 上传端点:服务端接收file二进制字段,保存文件后返回{ "url": "..." }
  3. 表单提交useForm提交时,把UploadFile对象数组(含 uid、name、url、status 等字段)作为普通 JSON 字段随业务数据一起发送。

第一步:在创建表单中加入图片上传字段

以"创建文章(post)"页面为例,需要在标题字段之外增加一个图片字段。文档给出的完整代码如下(源自版本 3 文档,包名为@pankod/refine-core@pankod/refine-antd,对应章节后文会给出当前仓库中的包名映射说明):

import { // highlight-start useApiUrl, // highlight-end } from "@pankod/refine-core"; import { // highlight-start Upload, getValueFromEvent, // highlight-end Create, Form, Input, useForm, } from "@pankod/refine-antd"; export const PostCreate: React.FC = () => { const { formProps, saveButtonProps } = useForm<IPost>(); // highlight-next-line const apiUrl = useApiUrl(); return ( <Create saveButtonProps={saveButtonProps}> <Form {...formProps} layout="vertical"> <Form.Item label="Title" name="title" rules={[ { required: true, }, ]} > <Input /> </Form.Item> <Form.Item label="Image"> <Form.Item name="image" valuePropName="fileList" // highlight-next-line getValueFromEvent={getValueFromEvent} noStyle > // highlight-start <Upload.Dragger name="file" action={`${apiUrl}/media/upload`} listType="picture" maxCount={5} multiple > <p className="ant-upload-text"> Drag & drop a file in this area </p> </Upload.Dragger> // highlight-end </Form.Item> </Form.Item> </Form> </Create> ); }; interface IPost { id: number; title: string; image: [ { uid: string; name: string; url: string; status: "error" | "success" | "done" | "uploading" | "removed"; }, ]; }

关键属性逐项说明

  • useApiUrl:Refine core 提供的 Hook,用于拿到当前 dataProvider 配置的 API 基础地址,从而拼出上传端点${apiUrl}/media/upload。文档提示"可以用useApiUrlHook 获取 API URL"。从当前仓库源码看,它的实现非常薄:useApiUrl.ts 中先通过useDataProvider拿到 dataProvider 实例,再调用其getApiUrl()方法返回基础 URL,并支持按resource.meta.dataProviderName选择命名 dataProvider。这意味着上传端点地址会自动跟随你在<Refine dataProvider={...}>中配置的 API 域名变化。
  • Upload.Draggeraction:这是 multipart 上传的核心——文件不经过表单提交逻辑,而是由 Ant Design Upload 在文件选择后立即向该地址发起multipart/form-data的 POST 请求。action中填写的正是后文要定义的上传端点地址。
  • name="file":指定 multipart 请求体中文件字段的字段名,服务端按此字段名解析二进制内容。
  • listType="picture":以图片墙形式展示已上传文件,配合图片类资源更直观。
  • maxCount={5}multiple:限制最多 5 个文件并允许多选。
  • valuePropName="fileList"+getValueFromEventForm.Item通过这两个配置把 Ant Design Upload 的受控属性对齐到表单值上,并把组件的 change 事件参数转换成UploadFile[]存入表单字段image

getValueFromEvent 的源码实现

文档特别提醒:必须使用getValueFromEvent方法把上传得到的文件转换为 Antd 的UploadFile对象。当前仓库中它的实现位于 upload/index.ts,逻辑一目了然:

import type { UploadFile, UploadChangeParam } from "antd/lib/upload/interface"; export const getValueFromEvent = (event: UploadChangeParam): UploadFile[] => { const { fileList } = event; return [...fileList]; };

即直接取 change 事件参数中的fileList并拷贝为数组返回,保证表单拿到的是 Antd 定义的UploadFile结构而不是原始File对象。同一文件里还导出了file2Base64工具函数(基于FileReader.readAsDataURL),它服务于另一种上传方式——base64 上传,可作为了解 multipart 与 base64 两条路线差异的对照参考。

当前仓库中配套的完整示例工程 examples/upload-antd-multipart/src/pages/posts/create.tsx 与该文档代码结构一致(额外带上了 category、status、content 字段),其图片字段部分同样是valuePropName="fileList"+getValueFromEvent+Upload.Dragger的组合,且使用当前包名@refinedev/core@refinedev/antdantd包。

第二步:设计服务端上传端点

表单中action指向的地址需要一个真实存在的上传端点来承接 multipart 请求。文档给出的端点契约如下。

请求形态:

{ "file": "binary" }

:::caution 该端点必须接受Content-Type: multipart/form-data,且文件字段名为Form Data: file: binary,与Upload.Dragger上的name="file"一一对应。 :::

服务端处理完成后,端点应返回一个包含下载 URL 的对象:

{ "url": "https://example.com/uploaded-file.jpeg" }

也就是说,端点的职责是:接收 multipart 二进制文件 -> 持久化(磁盘或对象存储)-> 返回{ url }。Ant Design 的 Upload 组件拿到响应后会据此补全对应UploadFileurl字段,并更新为status: "done"。示例工程 examples/upload-antd-multipart/src/App.tsx 中配置的API_URL = "https://api.fake-rest.refine.dev"正是文档中该端点的宿主,dataProvider(API_URL)通过@refinedev/simple-rest注入,useApiUrl返回的就是这个域名。

第三步:理解表单提交时的数据结构

当用户在创建页点击保存、表单真正提交时,useForm会把整个表单值 POST 到业务资源端点,其中image字段就是由getValueFromEvent维护的UploadFile对象数组:

{ "title": "Test", "image": [ { "uid": "rc-upload-1620630541327-7", "name": "greg-bulla-6RD0mcpY8f8-unsplash.jpg", "url": "https://refine.ams3.digitaloceanspaces.com/78c82c0b2203e670d77372f4c20fc0e2", "type": "image/jpeg", "size": 70922, "percent": 100, "status": "done" } ] }

文档明确要求:以下 Antd Upload 组件的字段是必需的,保存时必须全部落库

PropertyDescription
uid唯一标识(Unique id)
name文件名(File Name)
url下载 URL(Download URL)
status取值:error, success, done, uploading, removed

这份字段定义与当前仓库中的类型声明完全对应。packages/antd/src/interfaces/upload.ts 中定义了UploadedFile接口:

export interface UploadedFile { uid: string; name: string; url: string; type: string; size: number; percent: number; status: "error" | "success" | "done" | "uploading" | "removed"; }

示例工程的实体类型 examples/upload-antd-multipart/src/interfaces/index.d.ts 也按image: UploadFile[]声明了IPost.image,与文档中的interface IPost语义一致。status的五个枚举值覆盖了上传全生命周期:uploading(传输中)、done/success(完成)、error(失败)、removed(被用户移除),这也是下一步"上传状态"功能能感知进度的基础。

编辑表单:预填充已有图片并回写

编辑页的逻辑与创建页基本相同,区别在于表单初始值来自GET请求。文档给出的编辑页代码如下:

import { // highlight-start useApiUrl, // highlight-end } from "@pankod/refine-core"; import { // highlight-start Upload, getValueFromEvent, // highlight-end Edit, Form, Input, useForm, } from "@pankod/refine-antd"; export const PostEdit: React.FC = () => { const { formProps, saveButtonProps } = useForm<IPost>(); // highlight-next-line const apiUrl = useApiUrl(); return ( <Edit saveButtonProps={saveButtonProps}> <Form {...formProps} layout="vertical"> <Form.Item label="Title" name="title" rules={[ { required: true, }, ]} > <Input /> </Form.Item> <Form.Item label="Image"> <Form.Item name="image" valuePropName="fileList" getValueFromEvent={getValueFromEvent} noStyle > // highlight-start <Upload.Dragger name="file" action={`${apiUrl}/media/upload`} listType="picture" maxCount={5} multiple > <p className="ant-upload-text"> Drag & drop a file in this area </p> </Upload.Dragger> // highlight-end </Form.Item> </Form.Item> </Form> </Edit> ); };

编辑场景下的数据流是:

  1. 读取useForm自动发起GET /posts/1,返回体中携带既有的image数组(结构与上文的UploadFile相同):
{ "id": 1, "title": "Test", "image": [ { "uid": "rc-upload-1620630541327-7", "name": "greg-bulla-6RD0mcpY8f8-unsplash.jpg", "url": "https://refine.ams3.digitaloceanspaces.com/78c82c0b2203e670d77372f4c20fc0e2", "type": "image/jpeg", "size": 70922, "percent": 100, "status": "done" } ] }
  1. 预填充:由于image字段本身就是UploadFile[]结构,Antd Upload 可以直接根据urlnamestatus: "done"渲染出既有图片的缩略图,用户可以继续追加或删除。
  2. 回写:表单提交时以PUT /posts/1发送完整的image数组:
{ "title": "Test", "image": [ { "uid": "rc-upload-1620630541327-7", "name": "greg-bulla-6RD0mcpY8f8-unsplash.jpg", "url": "https://refine.ams3.digitaloceanspaces.com/78c82c0b2203e670d77372f4c20fc0e2", "type": "image/jpeg", "size": 70922, "percent": 100, "status": "done" } ] }

正因为创建、编辑两端的数据结构完全同构(都是UploadFile[]),同一套valuePropName="fileList"+getValueFromEvent配置无需任何改动即可复用到编辑页——这也是把UploadFile全量字段落库(而非只存 url 字符串)的收益所在。

进阶:上传进行中禁用保存按钮(useFileUploadState)

文档的 "Uploading State" 一节指出:你很可能希望在文件还在上传时禁用表单的"保存"按钮,以避免把status: "uploading"url尚未补全的数据提交给后端。Refine antd 包为此提供了useFileUploadStateHook:

import { useApiUrl } from "@pankod/refine-core"; import { Upload, getValueFromEvent, // highlight-next-line useFileUploadState, Create, Form, Input, useForm, } from "@pankod/refine-antd"; export const PostCreate: React.FC = () => { const { formProps, saveButtonProps } = useForm<IPost>(); // highlight-next-line const { isLoading, onChange } = useFileUploadState(); const apiUrl = useApiUrl(); return ( <Create // highlight-start saveButtonProps={{ ...saveButtonProps, disabled: isLoading, }} // highlight-end > <Form {...formProps} layout="vertical"> <Form.Item label="Title" name="title" rules={[ { required: true, }, ]} > <Input /> </Form.Item> <Form.Item label="Image"> <Form.Item name="image" valuePropName="fileList" getValueFromEvent={getValueFromEvent} noStyle > <Upload.Dragger name="file" action={`${apiUrl}/media/upload`} listType="picture" maxCount={5} multiple // highlight-next-line onChange={onChange} > <p className="ant-upload-text"> Drag & drop a file in this area </p> </Upload.Dragger> </Form.Item> </Form.Item> </Form> </Create> ); };

用法上只有两处接入点:把onChange挂到Upload.Dragger上监听上传进度变化,再把isLoading合并进saveButtonProps.disabled

Hook 的源码实现

该 Hook 的实现位于 useFileUploadState/index.ts,核心逻辑是把 Antd 的UploadChangeParam中每个文件的status映射为布尔值:

const mapStatusToLoading = (files: UploadChangeParam["fileList"]) => { return files.map((file) => { switch (file.status) { case "uploading": return true; default: return false; } }); };

只要文件列表中任意一个文件处于uploading状态,isLoading即为true;全部到达终态(done/success/error/removed)后恢复为false。Hook 用useCallback稳定onChange引用、用useMemo缓存返回值,避免不必要的重渲染。

它的行为由单元测试固化:useFileUploadState/index.spec.ts 验证了两条路径——初始状态下isLoadingfalse;当onChange收到包含status: "uploading"fileList时,isLoading变为true

包名与版本注意事项

本文继承的原始指南位于版本 3 文档目录(version-3.xx.xx/advanced-tutorials/upload/multipart-upload.md),其中代码使用@pankod/refine-core@pankod/refine-antd包名,且Upload组件由 antd 集成包导出。从当前仓库结构看,packages 目录下的 antd 集成包为 packages/antd,示例工程 examples/upload-antd-multipart 已迁移到@refinedev/core@refinedev/antd包名,并从antd包直接引入FormInputUpload。从源码结构看,两种写法对应的组件能力一致:getValueFromEventuseFileUploadState等符号在 packages/antd/src/definitions/upload/index.ts 与 packages/antd/src/hooks/useFileUploadState/index.ts 中均可定位到实现。若你在新项目中按当前版本搭架,应使用示例工程中的@refinedev/*包名;若维护 v3 老项目,则按原文档的@pankod/*包名即可。

小结与延伸阅读

  • 上传组件侧:Upload.Draggeraction指向 multipart 端点,name="file"定义字段名,valuePropName="fileList"+getValueFromEventUploadFile[]同步进表单;
  • 服务端侧:端点必须声明Content-Type: multipart/form-data、按字段名file接收二进制,并返回{ "url": "..." }
  • 数据契约:uidnameurlstatus四个字段(连同 type/size/percent)需要全量落库,以保证编辑页能正确预渲染图片墙;
  • 体验细节:用useFileUploadStateisLoading在上传未完成时禁用保存按钮,防止半完成状态被提交。

相关仓库资源:

  • 原始文档:multipart-upload.md
  • 可运行示例工程:examples/upload-antd-multipart/src/pages/posts/create.tsx、examples/upload-antd-multipart/src/pages/posts/edit.tsx、examples/upload-antd-multipart/src/App.tsx
  • 核心实现:packages/antd/src/definitions/upload/index.ts、packages/antd/src/hooks/useFileUploadState/index.ts、packages/antd/src/interfaces/upload.ts、packages/core/src/hooks/data/useApiUrl.ts
  • 测试用例:packages/antd/src/hooks/useFileUploadState/index.spec.ts

【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/13 2:48:06

Python招聘数据爬虫与可视化分析实战指南

简介&#xff1a;本资源是一份面向Python初学者与数据方向学习者的综合性爬虫实战项目&#xff0c;聚焦拉勾网Python岗位招聘信息的采集、清洗、分析与可视化全流程&#xff0c;解决从零构建可运行数据分析作业的实际需求。压缩包共22个文件&#xff0c;含8个CSV格式的分城市岗…

作者头像 李华
网站建设 2026/9/13 2:47:17

Google Colab实战指南:零基础在云端跑通大模型实验

很多人第一次接触大模型实战&#xff0c;卡住的地方不是算法&#xff0c;不是代码&#xff0c;而是“我没有一张像样的显卡”。我特别能理解这个痛点&#xff0c;因为我自己刚入坑时&#xff0c;手里只有一台旧笔记本&#xff0c;跑个稍微像样点的模型&#xff0c;风扇能响成直…

作者头像 李华
网站建设 2026/9/13 2:46:30

Java+Swing+MySQL学生选课成绩管理系统:从表设计到并发控制

简介&#xff1a;基于Java语言、Swing界面工具包以及MySQL关系型数据库构建的学生选课及成绩管理系统&#xff0c;是一份面向Java课程设计的完整项目源码包&#xff0c;主要服务正在学习图形界面编程和数据库设计的课设学生&#xff0c;能够有效覆盖选课、成绩管理等常见需求。…

作者头像 李华