refine v3 Base64 文件上传:在表单提交前将文件转换为 Base64 字符串
【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine
本篇技术指南介绍 refine(v3)Advanced Tutorials 中 "Base64 Upload" 的完整实现:利用 Ant Design<Form>组件的onFinish属性,在表单提交前把Upload控件中的文件逐一转换为 Base64 字符串,再连同表单数据一起交给 refine 的useForm提交流程。读完后,你将掌握file2Base64与getValueFromEvent两个工具函数的用法、beforeUpload={() => false}的拦截机制,以及如何在 Create / Edit 两种页面中复用同一套 Base64 上传逻辑。
为什么需要 Base64 上传
很多后端接口并不直接接收multipart/form-data中的二进制文件,而是期望在 JSON 请求体中携带文件的 Base64 编码字符串(例如data:image/png;base64,iVBORw0KGgo...)。典型场景包括:
- 后端通过 JSON 字段直接落库或推送到对象存储(如某些 Strapi、Supabase 集成场景);
- 网关或反序列化层只接受 JSON,不接受混合内容的 multipart 请求;
- 头像、附件等小体积文件,开发者希望在一次
POST中完成"业务数据 + 文件"的整体提交。
refine 的解决方案是:在表单onFinish触发时、真正调用 mutation 之前,把Form收集到的UploadFile[]转换为携带base64String字段的对象数组。官方文档documentation/versioned_docs/version-3.xx.xx/advanced-tutorials/upload/base64-upload.md明确给出了这一思路:"By encoding your files and images from your forms to Base64 you can change all files needed for the upload to Base64 format before the submit"。
完整示例:Create 页面中的 Base64 上传
以下代码完整继承自官方文档,上传字段为avatar(多文件拖拽上传):
// pages/users/create.tsx import { //highlight-start file2Base64, //highlight-end } from "@pankod/refine-core"; import { Create, Form, Upload, Input, useForm, // highlight-start getValueFromEvent, // highlight-end } from "@pankod/refine-antd"; export const UserCreate: React.FC = () => { const { form, formProps, saveButtonProps } = useForm<IUser>(); return ( <Create saveButtonProps={saveButtonProps}> <Form {...formProps} layout="vertical" // highlight-start onFinish={async (values) => { const base64Files = []; // @ts-ignore const { avatar } = values; for (const file of avatar) { if (file.originFileObj) { const base64String = await file2Base64(file); base64Files.push({ ...file, base64String, }); } else { base64Files.push(file); } } return ( formProps.onFinish && formProps.onFinish({ ...values, avatar: base64Files, }) ); }} // highlight-end > <Form.Item label="First Name" name="firstName" rules={[ { required: true, }, ]} > <Input /> </Form.Item> <Form.Item label="Avatar"> <Form.Item name="avatar" valuePropName="fileList" // highlight-start getValueFromEvent={getValueFromEvent} noStyle rules={[ { required: true, }, ]} > <Upload.Dragger listType="picture" multiple // highlight-start beforeUpload={() => false} > <p className="ant-upload-text"> Drag & drop a file in this area </p> </Upload.Dragger> </Form.Item> </Form.Item> </Form> </Create> ); }; interface IUser { id: number; firstName: string; avatar: [ { uid: string; name: string; url: string; status: "error" | "success" | "done" | "uploading" | "removed"; }, ]; }核心代码逐段解析
1. 用onFinish包裹并替换提交流程
关键点在于<Form {...formProps}>:useForm返回的formProps中已经内置了一个onFinish,它负责把校验通过的表单值交给useForm内部的 mutation(onMutate)。这里的做法是在展开formProps之后再显式传入自定义onFinish,让它覆盖(wrapper)掉默认实现:
- 遍历
values.avatar(即 antd 的UploadFile[]); - 对每个文件调用
await file2Base64(file),得到 DataURL 形式的 Base64 字符串; - 生成
base64Files数组,其中每个元素在原UploadFile基础上追加base64String字段; - 最后调用
formProps.onFinish?.({...values, avatar: base64Files}),把转换后的完整表单值交还 refine 的提交流程。
这样dataProvider最终收到的variables中,avatar就是带base64String的对象数组,后端可直接从 JSON 中解码保存。
2.originFileObj:区分"新文件"与"已有文件"
if (file.originFileObj) { const base64String = await file2Base64(file); base64Files.push({ ...file, base64String }); } else { base64Files.push(file); }originFileObj是 antdUpload为"本次会话中用户新选择的原始File对象"保留的引用。file2Base64内部依赖FileReader.readAsDataURL(file.originFileObj as Blob),只有持有原始Blob才能读取二进制内容。对于从后端加载回来的已有文件(例如编辑页回显的 URL),没有originFileObj,也无需重新编码——直接透传即可。这也是为什么示例在 Edit 场景下可以"不改剩余代码"工作(见下文"Create 与 Edit 复用"一节)。
3.getValueFromEvent与valuePropName="fileList"
内层Form.Item上的两个属性是 antd 上传控件接入 Form 的标准姿势:
valuePropName="fileList":告诉 Form 用fileList属性而非默认的value绑定子组件;getValueFromEvent={getValueFromEvent}:把Upload的onChange事件参数(UploadChangeParam)归一化为UploadFile[]存进表单值。
refine-antd 包对该函数的实现非常简洁,可以直接在源码中确认(packages/antd/src/definitions/upload/index.ts):
import type { UploadFile, UploadChangeParam } from "antd/lib/upload/interface"; export const getValueFromEvent = (event: UploadChangeParam): UploadFile[] => { const { fileList } = event; return [...fileList]; };即:从事件参数中取出fileList并浅拷贝后作为表单值返回,保证Form拿到的是不可变的新数组引用,从而正确触发受控更新。
4.beforeUpload={() => false}:拦截自动上传
<Upload.Dragger>上的beforeUpload={() => false}是 antd 的约定:返回false会阻止控件自身发起任何网络上传请求。这很重要——文件只停留在前端内存中,真正的"提交"由 refine 的onFinish统一完成。若漏写这一行,antd 会尝试按其默认action单独上传文件,导致提交行为失控。
源码级原理:file2Base64是怎么工作的
file2Base64定义在 core 包的upload定义目录中,实现如下(packages/core/src/definitions/upload/file2Base64/index.ts):
export const file2Base64 = (file: any): Promise<string> => { return new Promise((resolve, reject) => { const reader = new FileReader(); const resultHandler = () => { if (reader.result) { reader.removeEventListener("load", resultHandler, false); resolve(reader.result as string); } }; reader.addEventListener("load", resultHandler, false); reader.readAsDataURL(file.originFileObj as Blob); reader.onerror = (error) => { reader.removeEventListener("load", resultHandler, false); return reject(error); }; }); };从源码结构看,有三个值得注意的实现细节:
- 基于
FileReader.readAsDataURL:reader.result是完整的 DataURL 字符串(含data:<mime>;base64,前缀),因此后端拿到的base64String可直接解析出 MIME 类型与编码内容,无需额外传输 MIME; - 输入约定是 antd 风格的
UploadFile:函数签名虽然是file: any,但实现里直接读取file.originFileObj。这与示例中传入选中的 antd 文件对象保持一致——如果你传入原生File对象,originFileObj为undefined,readAsDataURL(undefined)将无法正常读取; - 事件监听的生命周期管理:
load成功后会removeEventListener,错误分支同样移除监听并reject,避免 Promise 悬挂或监听器泄漏。
该函数的单元测试位于 packages/core/src/definitions/upload/file2Base64/index.spec.ts,通过 mockwindow.FileReader验证了成功路径:readAsDataURL触发addEventListener注册的回调后,file2Base64正确 resolve 出reader.result(测试断言expect(content).toBe("file content"))。
值得一提的是,@pankod/refine-antd也内置了一份等价实现(packages/antd/src/definitions/upload/index.ts),只是参数类型标注为UploadFile。文档示例选择从 core 包导入,保持了对具体 UI 框架包的最小依赖面。
Create 与 Edit 复用:改一个组件即可
官方文档特别提示:把<Create>换成<Edit>,其余代码一行不改即可得到编辑页的 Base64 上传。原因在于:
useForm在 Edit 模式下会自动query当前记录并把返回值作为表单初始值注入;- 回显出来的已有文件对象没有
originFileObj,走else分支原样透传; - 用户在编辑页新添加的文件带有
originFileObj,会被转换为base64String一并提交。
仓库中的示例工程 examples/upload-antd-base64 完整演示了这一模式:create.tsx 与同目录下的 edit.tsx 使用了完全相同的onFinish转换逻辑。该示例中还定义了表单变量的类型约束(examples/upload-antd-base64/src/interfaces/index.d.ts):
export interface IUserVariable { id: number; email: string; firstName: string; lastName: string; avatar: UploadFile[]; }将avatar显式标注为UploadFile[]后,useForm<IUser, HttpError, IUserVariable>的第三个泛型即可精确描述提交变量结构,避免文档示例中// @ts-ignore的权宜写法。
注意事项与适用边界
- Base64 体积膨胀:Base64 编码会使数据体积增加约 33%(每 3 字节变 4 字符)。该方案适合头像、图标等中小体积文件;大文件或批量文件建议改用 multipart/form-data 上传,refine 在 multipart-upload.md 中提供了对应的进阶教程;
- 浏览器 API 依赖:
file2Base64依赖FileReader,只能在浏览器(或具备 DOM 环境的测试运行时)中使用; - 包名适用前提:本文对应 refine v3 文档(
versioned_docs/version-3.xx.xx),包名为@pankod/refine-core/@pankod/refine-antd;仓库中的examples/upload-antd-base64示例已升级到@refinedev/*命名,逻辑完全一致,迁移时只需替换导入路径; - 请求体大小限制:Base64 字符串走 JSON 请求体,注意后端网关、Nginx 等中间件对
Content-Length的限制配置。
小结
Base64 上传的本质是一次"提交前的数据整形":借助 antdForm的onFinish覆盖机制,把Upload组件收集到的UploadFile[]逐个通过file2Base64转换为携带base64String的纯数据,再交还useForm的 mutation 流程。整套方案只需要一个异步onFinish、两个 refine 工具函数(file2Base64、getValueFromEvent)和一个beforeUpload={() => false}拦截,即可让 Create / Edit 两种页面以同一套代码完成 Base64 文件提交。
【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考