ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

使用 React Hook Form 与 FormData 实现 Multipart 文件上传:Refine 实战指南

使用 React Hook Form 与 FormData 实现 Multipart 文件上传:Refine 实战指南 使用 React Hook Form 与 FormData 实现 Multipart 文件上传Refine 实战指南【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine在 React 应用中向服务器上传图片或文件是最常见的需求之一而正确的方式是使用multipart/form-data编码类型通过FormData构造请求体。本篇指南基于 Refine 仓库中的官方博客教程 documentation/blog/2022-03-29-react-hook-form-upload.md先带你在纯 React React Hook Form 场景下从零搭建 Express 上传服务器并完成表单上传再深入展示如何借助 Refine 的refinedev/react-hook-form适配器把文件上传与标题、分类等业务字段一起通过onFinish自动写入数据库。读完本文你将掌握从原生 React Hook Form 手动构造 FormData到Refine headless 表单托管上传的完整进阶路径。一、什么是 Multipart 文件上传Multipart 文件上传指客户端把文件连同其他表单字段以multipart/form-data的Content-Type编码后发送到服务端的网络请求。浏览器原生提供的FormData接口正是为此设计的它可以容纳多个键值对其中值既可以是普通字符串也可以是File/Blob二进制对象浏览器在发送时会自动完成分块boundary编码。const formData new FormData(); formData.append(file, file);这种方式的典型应用是把图片、文档等资源传送到后端存储再由后端返回可访问的 URL前端随后仅把该 URL 作为业务字段例如文章的封面图thumbnail随其他数据一起提交保存。二、创建 Express 上传服务器先从服务端开始。文件要有一个接收方这里用 Express 搭建一个最小可用的上传接口用于验证整条链路的正确性。2.1 安装依赖首先安装 Express 框架本身npm i express接着安装两个配套包cors用于允许跨域上传请求前端通常跑在另一个端口express-fileupload用于接收并落盘上传的文件npm i cors express-fileuploadexpress-fileupload会解析请求中的 multipart 数据把上传的文件挂到req.files上并提供file.mv()方法把文件移动到指定路径。2.2 编写上传接口创建一个服务端入口文件对指定端点暴露POST请求返回上传成功或失败的 JSON 响应import express from express; import fileupload from express-fileupload; import cors from cors; const app express(); app.use( fileupload({ createParentPath: true, }), ); app.use(cors()); app.use(express.json()); app.use(express.urlencoded({ extended: true })); app.post(/upload-file, async (req, res) { try { if (!req.files) { res.send({ status: failed, message: No file uploaded, }); } else { let file req.files.file; console.log(req.files); file.mv(./uploads/ file.name); res.send({ status: success, message: File is uploaded, data: { name: file.name, mimetype: file.mimetype, size: file.size, }, }); } } catch (err) { res.status(500).send(err); } }); const port process.env.PORT || 5000; app.listen(port, () console.log(Server started on port ${port}));配置要点fileupload({ createParentPath: true })当目标目录如./uploads/不存在时自动创建父级路径避免file.mv()报目录不存在的错误。请求体被成功解析后字段名为file的文件会以req.files.file暴露包含name、mimetype、size等元信息可通过file.mv(destination)落盘保存。app.use(cors())允许来自其他源例如http://localhost:3000的前端页面跨域提交文件。若没有文件被上传!req.files返回status: failed与提示信息若服务端逻辑异常则以 HTTP 500 返回错误对象。启动后终端会输出Server started on port 5000服务端即就绪等待接收来自前端表单的 multipart 请求。三、在纯 React React Hook Form 中上传文件3.1 初始化项目并安装依赖使用 Create React App 创建前端工程npx create-react-app react-hook-form-multipart-upload进入项目目录后安装 React Hook Form 并启动开发服务器cd react-hook-form-multipart-upload npm install react-hook-form npm run start3.2 用 useForm 管理文件输入React Hook Form 的核心思路是用register把表单控件注册到表单状态中用handleSubmit统一处理提交。先把基础表单搭起来import React from react; import { useForm } from react-hook-form; function App() { const { register, handleSubmit } useForm(); const onSubmit () {}; return ( div classNameApp form onSubmit{handleSubmit(onSubmit)} input typefile {...register(file)} / input typesubmit / /form /div ); } export default App;注意input typefile /与普通文本框的区别文件选择框的值是一个FileList即使只选一个文件也是如此因此register(file)注册后在提交数据里拿到的是data.file数组对象需要取data.file[0]才是用户选中的那个File实例。3.3 构造 FormData 并发送到服务器在onSubmit中把选中的文件放入FormData再通过fetch以 POST 方式发送。这里的关键是不要把文件对象直接放进 JSON 请求体也不要手动设置Content-Type头交给浏览器根据FormDatabody 自动生成带 boundary 的multipart/form-dataimport React from react; import { useForm } from react-hook-form; function App() { const { register, handleSubmit } useForm(); const onSubmit async (data) { const formData new FormData(); formData.append(file, data.file[0]); const res await fetch(http://localhost:5000/upload-file, { method: POST, body: formData, }).then((res) res.json()); alert(JSON.stringify(${res.message}, status: ${res.status})); }; return ( div classNameApp form onSubmit{handleSubmit(onSubmit)} input typefile {...register(file)} / input typesubmit / /form /div ); } export default App;浏览器会依次执行用户选择文件 → 点击提交 → React Hook Form 校验并回调onSubmit→ 构造FormData并append(file, data.file[0])→fetchPOST 到http://localhost:5000/upload-file→ 服务端把文件存入uploads/目录并返回 JSON → 前端用alert展示服务端返回的message与status。选中一张图片提交后如果服务端返回File is uploaded说明整条 multipart 链路已经打通。四、升级到 Refineheadless 表单自动对接数据层纯 React Hook Form 方案要求你手动管理接口地址、提交逻辑与数据持久化而在 Refine 这类 CRUD 框架中表单只是数据流的入口。Refine 以 headless 方式工作可搭配任意 UI同时内置路由、网络、状态管理等能力并通过官方适配器refinedev/react-hook-form让 React Hook Form 的全部能力与 Refine 的 data provider 无缝协同。适配器的安装与使用说明可参考 packages/react-hook-form/README.md。4.1 适配器如何打通 useForm 与 Refine 核心从源码看refinedev/react-hook-form的useForm返回类型在原生 React Hook Form 的返回值基础上扩展了refineCore与saveButtonProps两个成员见 useForm/index.ts 的类型定义实现层面则同时调用了两个底层 hook见 useForm/index.tsuseHookForm来自react-hook-form负责字段注册、校验规则、register/setValue/formState.errors等表单状态管理useFormCore来自refinedev/core负责按resource拉取数据、通过onFinish触发 data provider 的create/update并暴露formLoading、query等状态。两者通过refineCore命名空间合体返回同时外层useForm会捕获 data provider 抛出的HttpError把服务端校验错误逐字段映射回 React Hook Form 的errors见 useForm/index.ts。这意味着表单校验、提交、错误展示都由适配器统一衔接你只需要把文件上传的产物放进字段值里即可。仓库中用于验证这套行为的组件级测试见 packages/react-hook-form/src/useForm/index.spec.tsx其中覆盖了refineCoreProps.resource、字段注册与formState.errors的联动场景完整可运行的示例工程位于 examples/form-react-hook-form-use-form采用refinedev/simple-restrefinedev/react-hook-form React Router 组成依赖清单见 examples/form-react-hook-form-use-form/package.json。4.2 典型的 Refine 上传流程在 Refine React Hook Form 的 headless 场景下Multipart 上传通常采用两步提交模式用户选择图片后立即把文件以multipart/form-data的形式独立上传到媒体服务/media/upload这类端点服务端返回该文件的可访问 URL前端用setValue把它写入表单隐藏字段如thumbnail表单整体提交时handleSubmit(onFinish)会连同标题、分类、状态等业务字段一起交给 data provider最终调用创建接口持久化。这种先传文件、再存引用的方式让表单提交体保持简洁的 JSON 结构是 Refine 官方示例中的标准做法同样思路在 UI 集成文档中也有体现可对照 documentation/docs/advanced-tutorials/upload/multipart-upload.md 中 Ant Design Upload 组件的上传状态处理。4.3 完整示例CreatePost 页面以下代码完整取自仓库中的真实示例页 examples/form-react-hook-form-use-form/src/pages/posts/create.tsx原博客中的版本与之同构。这是一个使用 Refine headless 功能创建的 CMS 风格的新建文章页面包含文本字段、下拉框与图片上传import { useState } from react; import { useForm } from refinedev/react-hook-form; import { useSelect, useApiUrl, useBack } from refinedev/core; import axios from axios; export const PostCreate: React.FC () { const [isUploading, setIsUploading] useStateboolean(false); const back useBack(); const { refineCore: { onFinish, formLoading }, register, handleSubmit, formState: { errors }, setValue, } useForm(); const apiURL useApiUrl(); const { options } useSelect({ resource: categories, pagination: { mode: server }, }); const onSubmitFile async () { setIsUploading(true); const inputFile document.getElementById(fileInput) as HTMLInputElement; const formData new FormData(); formData.append(file, inputFile?.files?.item(0) as File); const res await axios.post{ url: string }( ${apiURL}/media/upload, formData, { withCredentials: false, headers: { Access-Control-Allow-Origin: *, }, }, ); setValue(thumbnail, res.data.url); setIsUploading(false); }; return ( form onSubmit{handleSubmit(onFinish)} labelTitle: /label input idtitle {...register(title, { required: true })} / {errors.title span idtitle-errorThis field is required/span} br / labelStatus: /label select idstatus {...register(status)} option valuepublishedpublished/option option valuedraftdraft/option option valuerejectedrejected/option /select br / labelCategory: /label select idcategory defaultValue{} {...register(category.id, { required: true })} option value{} disabled Please select /option {options?.map((category) ( option key{category.value} value{category.value} {category.label} /option ))} /select {errors.category span idcategory-errorThis field is required/span} br / labelContent: /label br / textarea idcontent {...register(content, { required: true })} rows{10} cols{50} / {errors.content span idcontent-errorThis field is required/span} br / br / labelImage: /label input idfileInput typefile onChange{onSubmitFile} / input typehidden {...register(thumbnail)} / {errors.thumbnail spanThis field is required/span} br / br / button onClick{back}Cancel/button input typesubmit disabled{isUploading} valueSubmit / {formLoading pLoading/p} /form ); };代码中的几个关键动作需要拆开理解useForm()解构出的refineCore.onFinish取代了原生 React Hook Form 中手写的提交逻辑。提交时表单整体走handleSubmit(onFinish)data provider 会自动完成记录创建。useApiUrl()从 Refine 上下文中取出 data provider 配置的 API 地址本示例工程在 examples/form-react-hook-form-use-form/src/App.tsx 中通过dataProvider{dataProvider(API_URL)}注入API_URL指向https://api.fake-rest.refine.dev上传端点拼接为${apiURL}/media/upload。useSelect({ resource: categories })以 headless 方式拉取关联资源categories作为下拉选项渲染为option同时通过register(category.id)将选中的分类 id 写入嵌套字段。onSubmitFile在文件选择框的onChange触发将#fileInput中选中的第一个文件files?.item(0)放入FormData用axios.post以 multipart 方式上传到/media/upload成功后用setValue(thumbnail, res.data.url)把返回的图片 URL 写入隐藏字段。isUploading状态用于在上传过程中禁用提交按钮防止用户拿到过期数据抢先提交。4.4 关联的列表与编辑页面同样的适配器可用于编辑页编辑记录时refineCore.query会拉取现有记录回填字段包括已保存的thumbnail缩略图setValue与defaultValue用来同步关联字段保存时handleSubmit(onFinish)触发更新完整实现见 examples/form-react-hook-form-use-form/src/pages/posts/edit.tsx。配套的路由资源声明与列表页分别位于 examples/form-react-hook-form-use-form/src/App.tsx 和 examples/form-react-hook-form-use-form/src/pages/posts/list.tsx。五、运行示例工程仓库中该示例是一个 Vite 工程使用 Refine CLI 驱动脚本见 examples/form-react-hook-form-use-form/package.json其 READMEexamples/form-react-hook-form-use-form/README.md给出了标准初始化命令npm create refine-applatest -- --example form-react-hook-form-use-form进入生成的项目目录后即可本地启动npm install npm run dev启动后访问应用的新建文章路由填写标题、选择分类、选取一张图片图片会先上传到/media/upload获得 URL提交后整个记录含thumbnail引用由refinedev/react-hook-form适配器配合refinedev/simple-rest写入接口。运行示例即可直观看到普通字段 文件引用一次表单提交全部落库的完整效果。六、小结本篇完整走通了 Multipart 文件上传的两条技术路线原生 React Hook Form用register注册文件输入、handleSubmit收集数据、FormData承载二进制fetch直发 Express 上传接口理解data.file[0]与 multipart 编码的本质Refine refinedev/react-hook-form适配器复用全部 React Hook Form 能力同时把提交动作交给onFinish、接口地址交给useApiUrl、关联选项交给useSelect实现文件独立上传拿 URL、业务字段整表提交的 CRUD 实践。两种方案并非互斥——前者帮你理解 HTTP 层面的上传原理后者在真实 CRUD 应用中帮你省去大量样板代码。若需进一步了解 Refine 在其他 UI 集成中的上传方案可继续阅读 documentation/docs/advanced-tutorials/upload/base64-upload.md 与 documentation/docs/advanced-tutorials/upload/multipart-upload.md以及本仓库中对应的一站式表单示例工程form-react-hook-form-use-form。【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表