Refine v5 中的 Base64 图片上传实战:基于 Mantine useForm 与 Dropzone 的完整实现
【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine
本文以 Refine 官方示例 examples/upload-mantine-base64 为蓝本,讲解如何在 Refine v5 + Mantine 技术栈下,通过@refinedev/mantine的useForm配合@mantine/dropzone的Dropzone组件,实现"选择图片 → 转换为 Base64 → 存入表单字段 → 随表单一并提交"的完整上传流程。读完本文,你将掌握 Base64 上传的核心原理(FileReader.readAsDataURL)、创建/编辑页面的落地写法、图片预览的实现方式,以及useForm在 Refine 内部的封装机制,可直接复用到自己的管理后台项目中。
示例概览与技术栈
该示例是一个完整的"文章(posts)"CRUD 管理页面,其中创建(Create)与编辑(Edit)页面都集成了图片上传能力。示例的完整运行说明见 examples/upload-mantine-base64/README.md,可通过如下命令在本地启动:
npm create refine-app@latest -- --example upload-mantine-base64从 package.json 可以看到示例的核心依赖:
| 依赖包 | 版本 | 职责 |
|---|---|---|
@refinedev/core | ^5.0.12 | Refine 核心(hooks、数据提供者、路由等) |
@refinedev/mantine | ^3.0.2 | Mantine 集成层:useForm、Create/Edit页面组件、RefineThemes |
@mantine/dropzone | ^5.4.1 | 拖拽/点击选择文件的 Dropzone 组件 |
@mantine/form | ^5.10.4 | Mantine 原生表单库,被 RefineuseForm包装 |
@mantine/core | ^5.10.4 | Mantine 基础组件库 |
@refinedev/simple-rest | ^6.0.1 | REST 数据提供者 |
@refinedev/react-router | ^2.0.4 | 路由提供者 |
在 App.tsx 中,应用以MantineProvider(使用RefineThemes.Blue主题)包裹<Refine>,配置了simple-rest数据提供者(API 地址为https://api.fake-rest.refine.dev),并注册了posts资源的list、create、edit、show四个页面路由。
Base64 上传的核心思路
与传统的multipart/form-data文件上传不同,Base64 上传将文件内容编码为 Data URL 字符串(形如data:image/png;base64,iVBORw0KGgo...),并作为普通字符串字段存放在表单数据中。当用户点击保存时,这个字符串会随其他字段一起通过数据提供者发送到后端。后端解析字符串后即可还原为图片二进制内容。
这种方案的优点是实现简单、不依赖任何文件上传专用组件或服务端中间件,适合图片体积小、数量少、后端仅需单次请求即可完成 CRUD 提交的场景。需要留意的是,Base64 编码会使数据体积膨胀约 33%,且图片以字符串形式保存在内存与请求体中,因此不适合大图或批量上传(此类场景更适合本仓库中的 multipart 方案,见 multipart.md)。
通用工具函数:convertBase64
示例将文件转 Base64 的逻辑抽离为可复用的工具函数,见 src/utils/convertBase64.ts:
export const convertBase64 = (file: File): Promise<string> => { return new Promise((resolve, reject) => { const fileReader = new FileReader(); fileReader.readAsDataURL(file); fileReader.onload = () => { if (typeof fileReader.result === "string") { resolve(fileReader.result); } }; fileReader.onerror = (error) => { reject(error); }; }); };该函数基于浏览器内置的FileReaderAPI:
fileReader.readAsDataURL(file)以异步方式将文件读取为 Data URL;onload回调中,fileReader.result即为data:...;base64,...字符串,通过resolve返回;onerror回调将读取失败的错误透传给调用方,便于上层做异常处理;- 返回类型为
Promise<string>,配合async/await使用非常顺手。
创建页面:Dropzone + setFieldValue 组合
创建页面的完整实现位于 src/pages/posts/create.tsx,其核心是useForm与Dropzone的配合。
1. 定义表单结构与校验规则
useForm接受泛型FormValues,明确表单字段类型,其中images为string[](存放 Base64 字符串数组):
interface FormValues { title: string; status: string; category: { id: string }; content: string; images: string[]; } const { saveButtonProps, getInputProps, setFieldValue, values, errors } = useForm<IPost, HttpError, FormValues>({ initialValues: { title: "", status: "", category: { id: "" }, content: "", images: [], }, validate: { title: (value) => (value.length < 2 ? "Too short title" : null), status: (value) => (value.length <= 0 ? "Status is required" : null), category: { id: (value) => (value.length <= 0 ? "Category is required" : null) }, content: (value) => (value.length < 10 ? "Too short content" : null), }, });这里可以看到 RefineuseForm的三个关键返回值:
saveButtonProps:直接透传给<Create>页面的保存按钮,自动接管提交逻辑(含 loading 状态与提交处理);getInputProps("fieldName"):将字段值与变更处理绑定到任意输入组件(如TextInput、Select、MDEditor),配合 Mantine 的受控表单体系;setFieldValue/values:用于程序化读写字段值——这正是 Base64 上传与表单联动的关键。
validate对象采用 Mantine 表单风格的字段级校验函数,客户端校验不通过时errors会携带对应错误信息,可在界面上展示(示例中对content字段做了红色错误提示渲染)。
2. Dropzone 拖拽上传与 Base64 转换
表单中嵌入@mantine/dropzone的Dropzone组件,通过accept={IMAGE_MIME_TYPE}限定仅接受图片类型文件:
<Dropzone accept={IMAGE_MIME_TYPE} onDrop={handleOnDrop} loading={isUploadLoading} > <Text align="center">Drop images here</Text> </Dropzone>handleOnDrop是上传逻辑的核心(create.tsx 第 57-75 行):
const handleOnDrop = (files: FileWithPath[]) => { try { setIsUploadLoading(true); files.map(async (file) => { const base64 = await convertBase64(file); if (values.images) { setFieldValue("images", [...values.images, base64]); } else { setFieldValue("images", [base64]); } }); setIsUploadLoading(false); } catch (error) { setIsUploadLoading(false); } };这段代码值得逐行解读:
Dropzone的onDrop回调接收FileWithPath[](继承自浏览器File);- 先通过
setIsUploadLoading(true)打开 Dropzone 的loading动画,避免用户重复操作; - 对每个文件调用
convertBase64(file)得到 Base64 字符串; - 通过
setFieldValue("images", [...values.images, base64])将新 Base64追加到images数组——注意这里使用展开语法保留了已上传的图片,支持一次拖入多张图片; - 转换或追加过程出错时进入
catch,关闭 loading 并保持表单原状。
3. 实时图片预览
由于images字段本身就存的是 Data URL,预览无需任何额外请求:
const previews = values.images?.map((base64, index) => { return <Image key={index} src={base64} />; }); <SimpleGrid cols={4} breakpoints={[{ maxWidth: "sm", cols: 2 }]} mt={previews?.length > 0 ? "xl" : 0} > {previews} </SimpleGrid>values.images一旦变化,React 即重新渲染,<Image src={base64}>直接以 Data URL 作为图片来源渲染缩略图,并用SimpleGrid做 4 列(小屏 2 列)的网格布局。整个流程完全在客户端完成,所见即所得。
4. 保存提交
页面根节点使用@refinedev/mantine的<Create saveButtonProps={saveButtonProps}>包装表单,点击保存时,images数组会作为普通字段随POST /posts请求发送到 API。对后端而言,它只是收到一个包含 Base64 字符串数组的 JSON 对象,无需处理文件流。
编辑页面:数据回填与增量追加
编辑页面 src/pages/posts/edit.tsx 与创建页面几乎一致,差异点在于已有数据的回填:
const { saveButtonProps, getInputProps, setFieldValue, values, errors, refineCore: { query: queryResult }, } = useForm<IPost, HttpError, FormValues>({ initialValues: { /* 与创建页一致 */ }, validate: { /* 与创建页一致 */ }, });refineCore.query暴露了 Refine 内部通过数据提供者拉取当前记录的结果。useForm在编辑模式下会自动将查询到的记录填充进表单(包括images字段),从而保证:
- 进入编辑页时,已上传图片的 Base64 会作为初始预览显示;
useSelect通过defaultValue: queryResult?.data?.data.category.id正确选中当前分类;- 用户追加新图片时,
handleOnDrop中的展开语法会保留原有图片,实现"旧图 + 新图"共存。
深入原理:Refine 的 Mantine useForm 是如何工作的
要真正理解示例,需要知道@refinedev/mantine的useForm并不只是简单封装。查看源码 packages/mantine/src/hooks/form/useForm/index.ts,可以看到它的实现层次:
useForm (@refinedev/mantine) ├── useMantineForm (@mantine/form) → 表单状态、校验、getInputProps/setFieldValue └── useFormCore (@refinedev/core) → 数据获取与提交(getOne/create/update)几个关键实现细节:
1. 双重参数通道。顶层useForm接收refineCoreProps与其余参数(源码第 93-113 行):前者透传给useFormCore(负责数据请求、提交),后者透传给useMantineForm(负责表单状态),因此一套 hooks 同时管理了"表单 UI 状态"与"服务端数据状态"。
2. 表单状态委托给 Mantine。源码第 127-141 行 将除refineCoreProps外的配置(initialValues、validate等)原样交给@mantine/form的useForm,并从其结果中解构出setValues、onSubmit、isDirty、resetDirty、setFieldError、values——示例中使用的setFieldValue与values正是来自这一层。
3. 服务端错误自动映射到字段。源码第 152-164 行 在onMutationError中读取error.errors,逐字段调用setFieldError将后端返回的字段级错误映射到对应表单字段上(可通过disableServerSideValidation关闭)。这意味着 Base64 图片字段若被后端校验拒绝,同样能以表单错误的形式反馈给用户。
4. 返回值的组装。源码第 253-259 行 最终返回{ ...useMantineFormResult, onSubmit, refineCore: useFormCoreResult }——这就是为什么示例中既能直接拿到getInputProps、setFieldValue、errors,又能通过refineCore.query访问数据查询结果。
类型定义与资源结构
示例的类型定义见 src/interfaces/index.d.ts:
export interface ICategory { id: number; title: string; } export interface IPost { id: number; title: string; content: string; status: "published" | "draft" | "rejected"; category: { id: number }; }注意IPost中并未声明images字段——表单层通过FormValues类型承载images: string[],体现了"数据模型与表单模型分离"的写法:数据库记录关心业务字段,表单可以携带额外的提交数据(图片、确认密码、临时状态等)。列表页 list.tsx 与详情页 show.tsx 仍按IPost渲染标准字段,不受影响。
实战注意事项
- 体积膨胀与内存占用:Base64 会使数据体积增加约 33%,且整个字符串常驻表单状态中,多张大图可能导致页面卡顿与请求体过大,建议限制文件数量与大小;
IMAGE_MIME_TYPE约束:Dropzone 的accept属性只做客户端过滤,服务端仍需校验 MIME 与解码合法性;key稳定性:预览使用index作为key,若需支持删除中间图片,建议改为稳定唯一标识;- 后端解码:后端需能解析 Data URL(截取
base64,之后的部分再atob/Buffer.from解码),或直接以字符串形式存入数据库,具体取决于业务设计; - 增量追加语义:
handleOnDrop中的[...values.images, base64]依赖闭包中的最新values,多文件拖拽时逐次追加,逻辑上安全;若追求更严格的并发一致性,可改用setValues基于函数式更新。
总结
Base64 上传是 Refine 管理后台中最轻量的文件上传方案之一:前端仅需FileReader.readAsDataURL完成编码、setFieldValue写入表单、<Image>完成预览,后端无需任何上传中间件即可随 CRUD 请求一并处理。结合 examples/upload-mantine-base64 示例与 useForm 源码 可以看出,Refine 的useForm通过"Mantine 表单层 + Refine 数据层"的双层架构,让开发者可以用最小的胶水代码把任意组件(包括文件选择组件)接入完整的 CRUD 数据流。对于大文件与批量上传场景,则建议参考同目录下的 multipart 方案 与仓库中的 upload-mantine-multipart 示例,按业务需求权衡选择。
【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考