React 技巧与窍门:使用进度条上传文件
使用 React 实现表单通常比较棘手。虽然有像formik或React Final Form这样的优秀库可以帮我们处理繁重的工作,但处理文件上传仍然并非总是那么简单。
在今天的 React 技巧和窍门中,我们将了解如何处理和提交文件数据,以及如何显示进度条!
基本形式
假设我们需要构建一个表单来创建博客文章,其中作为input
标题,作为textarea
正文。
以下是此类表单的简单实现,使用Material UI作为基本组件:
import React, { useState } from "react"; import Box from "@mui/material/Box";
import TextField from "@mui/material/TextField";
import Button from "@mui/material/Button";
interface PostData {
title: string;
body: string;
}
const Form: React.FunctionComponent = () => {
const [formValues, setFormValues] = useState<PostData>({
title: "",
body: "",
});
// Handlers for the input
const handleTitleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setFormValues((prevFormValues) => ({
...prevFormValues,
title: event.target.value,
}));
};
const handleBodyChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setFormValues((prevFormValues) => ({
...prevFormValues,
body: event.target.value,
}));
};
return (
<Box
display="flex"
height="100%"
flexDirection="column"
justifyContent="center"
alignItems="center"
>
<Box marginY={2}>
<TextField
onChange={handleTitleChange}
value={formValues.title}
label="Post Title"
name="title"
/>
</Box>
<Box marginY={2}>
<TextField
onChange={handleBodyChange}
multiline
minRows={5}
label="Post Body"
name="body"
/>
</Box>
<Box marginY={3}>
<Button onClick={() => console.log("submit")}>Submit Post </Button>
</Box>
</Box>
);
};
export default Form;
注意:这里我没有使用任何表单库,因为我想专注于文件处理。在生产环境中,我强烈建议使用类似Formik这样的工具,以避免重复造轮子!
这非常有效,并呈现以下输出:
太棒了!但现在假设我们除了标题和正文之外,还想提交一张图片作为文章的封面。这会稍微复杂一些,因为我们不再只是操作字符串了。
在帖子中添加图片
为了能够提交图像,我们需要在表单中添加 3 项内容:
- 从客户端计算机上传文件的按钮;
- 处理文件并将其存储在状态中的方法;
- 提交表单的处理程序;
让我们开始吧!
添加按钮
要向表单添加文件上传按钮,我们使用input
类型为file
,并包装在Button
组件中:
//Form.tsx
const Form: React.FunctionComponent = () => {
...
return (
...
<Box marginY={2}>
<TextField
onChange={handleBodyChange}
multiline
minRows={5}
label="Post Body"
name="body"
/>
</Box>
<Button variant="contained" component="label">
<input type="file" hidden />
</Button>
<Box marginY={3}>
<Button onClick={() => console.log("submit")}>Submit Post </Button>
</Box>
)
}
这里我们利用了标签(此处渲染为按钮)与其输入框以编程方式关联的特性。这意味着,任何“按钮”组件上的点击事件都将传递给隐藏的输入框。这个技巧使我们能够向用户显示任何我们想要的组件,同时仍然受益于内置的文件处理系统。
控制组件
目前,我们的输入是不受控制的:它没有链接到任何状态变量,因此我们无法在提交表单时声明性地使用它的值。我们需要改变这一点:
为了控制我们的输入,就像普通输入一样,我们需要传递一个处理程序。该处理程序使用File API来检索我们感兴趣的文件数据:
interface PostData {
title: string;
body: string;
image: File | null;
}
const Form: React.FunctionComponent = () => {
// Add an image attribute
// to our formData
const [formValues, setFormValues] = useState<PostData>({
title: "",
body: "",
image: null,
});
...
// Set up the handler
const handleImageChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setFormValues((prevFormValues) => ({
...prevFormValues,
image: event.target.files ? event.target.files[0] : null,
}));
};
...
return (
...
<Button variant="contained" component="label">
{formValues.image?.name ?? "Upload File"}
{/* Bind the handler to the input */}
<input onChange={handleImageChange} type="file" hidden />
</Button>
...
)
}
现在,当用户使用我们的按钮上传图片时,该image
属性将被填充一个 File 对象。这个对象有很多有用的属性,比如文件的名称和类型。我们可以用它们在按钮内显示用户当前选择的文件名称。另请注意,它target.files
是一个数组。这里我们只对第一个值感兴趣,因为我们只上传一个文件,但同样的方法可以用于多个文件!
表单提交
最后,我们需要一种提交数据的方法。为了测试,我在 Flask 中创建了一个小型 API,您可以在本文的仓库中找到它。它只是一个监听 POST 请求并返回 201 的端点。
现在,我们无法将数据以 JSON 格式 POST,因为我们想要发送文件,而 JSON 无法处理二进制数据。我们需要发送表单数据。我们将使用axios发送请求,因为它在显示进度方面非常方便,我们将在下一节中看到。
注意:或者,我们可以将图像用 BASE64 编码,然后将其作为 JSON 负载中的字符串发送。当然,在这种情况下,我们还需要在后端对其进行解码。
const handleSubmit = async () => {
const formData = new FormData();
formData.append("title", formValues.title);
formData.append("body", formValues.body);
formValues.image && formData.append("image", formValues.image);
const response = await axios.post(<YOUR-API-ENDPOINT>, formData, {
headers: {
"Content-Type": "multipart/form-data",
},
});
return response.data
};
这里发生了几件事:
- 首先我们创建一个新
FormData
对象; - 然后我们将我们的 fomvalues 添加到数据中;
- 最后,我们使用正确的内容标头将其发布到我们的端点
显示进度
我们的表单提交成功了!但还没完!
也许用户发布的图片会比较大,而且服务器端的处理速度也可能会比较慢。
由于处理请求可能需要一些时间,我们希望显示一个进度条。
这时候 Axios 就派上用场了!它内置了两个回调钩子来处理进度数据:
onUploadProgress
:上传阶段发送事件;onDownloadProgress
:在下载阶段;
现在我们要做的就是创建一个新的状态变量来存储进度值并监控请求状态!不妨把这段逻辑写在自定义钩子里,因为
我们以后可能会用到它。(这样也更容易阅读)。代码如下:
// hooks.ts
import { useState } from "react";
import axios from "axios";
export const useUploadForm = (url: string) => {
const [isSuccess, setIsSuccess] = useState(false);
const [progress, setProgress] = useState(0);
const uploadForm = async (formData: FormData) => {
setIsLoading(true);
await axios.post(url, formData, {
headers: {
"Content-Type": "multipart/form-data",
},
onUploadProgress: (progressEvent) => {
const progress = (progressEvent.loaded / progressEvent.total) * 50;
setProgress(progress);
},
onDownloadProgress: (progressEvent) => {
const progress = 50 + (progressEvent.loaded / progressEvent.total) * 50;
console.log(progress);
setProgress(progress);
},
});
setIsSuccess(true)
};
return { uploadForm, isSuccess, progress };
};
这里我选择将上传和下载步骤的进度均匀分布,但您可以随意选择!这完全取决于您
想向用户显示什么。我还添加了success
一个布尔值,可以用来进行一些条件渲染。
现在我们要做的就是使用自定义钩子提交表单,并以某种方式显示进度值!我在这里使用 Material UI 的线性进度。
const Form: React.FunctionComponent = () => {
const { isSuccess, uploadForm, progress } = useUploadForm(
"http://localhost:5000/post"
);
...
const handleSubmit = async () => {
const formData = new FormData();
formData.append("title", formValues.title);
formData.append("body", formValues.body);
formValues.image && formData.append("image", formValues.image);
return await uploadForm(formData);
};
}
...
const Form: React.FunctionComponent = () => {
return (
...
<Box marginY={3}>
<Button onClick={handleSubmit}>Submit Post </Button>
<LinearProgress variant="determinate" value={progress} />
</Box>
)
}
它看起来是这样的:
非常整洁!
奖励回合!
我认为展示如何在进度条达到 100% 后显示一条小成功消息会是一个很好的补充。
为此,我们将使用isSuccess
指示器。但首先,我们会在请求完成后人为地添加一个暂停,以便用户
欣赏进度条达到 100% 的效果。否则,React 会合并状态更新,并在进度条动画完成之前显示成功消息。
//hooks.ts
const uploadForm = async (formData: FormData) => {
...
await new Promise((resolve) => {
setTimeout(() => resolve("success"), 500);
});
setIsSuccess(true);
setProgress(0);
};
现在isSuccess
我们可以有条件地呈现成功消息:
{ isSuccess ? (
<Box color="success.main" display="flex">
<CheckIcon color="success" />
<Typography>Success</Typography>
</Box>
) : (
<>
<Button onClick={handleSubmit}>Submit Post </Button>
<LinearProgress variant="determinate" value={progress} />
</>
)}
谢谢阅读!
今天就到这里,希望你有所收获!React 中的表单处理并不容易,因为处理方式太多,出错的可能性也很大。这更有理由
继续尝试和学习!
想要了解更多 React 技巧? ➡️在 Twitter 上关注我!