Logo
Section 1 / 5

文件上传

让我们在应用程序的业务逻辑中实现文件上传功能的前端部分。

导入

文件上传

让我们在应用程序的业务逻辑中实现文件上传功能的前端部分。

在本节课中,我们将继续实现任何应用程序所需的四项基本功能(CRUD 操作)。不过,这一次我们将完成所有必要的用户界面部分,使操作更加便捷。我们首先从最重要的功能——上传功能的 UI 开始。

上传服务的实现

我们已经创建了 services/user.service.js 文件,但至今尚未使用它。现在是时候在这个文件中编写第一个服务了。该服务将与后端的上传接口进行交互。

  1. 我们导入 axios 和 authHeader。

    • HTTP 客户端 axios 是我们在应用中使用的工具。

    • authHeader 是提供认证令牌的认证服务。

1// frontend/src/services/user.service.js 2import axios from "axios"; 3import authHeader from "./auth.header";
  1. 我们按如下方式编写上传服务:

    • data 包含上传文件的相关信息以及文件本身。

    • 服务器上的专用 /upload 接口使用 HTTP POST 方法上传文件。

    • "Content-Type": "multipart/form-data" 是 Multer 所需的属性,用于支持文件上传。

1// frontend/src/services/user.service.js 2import axios from "axios"; 3import authHeader from "./auth.header"; 4 5const upload = (data) => { 6 return axios.post(`/upload`, data, { 7 headers: { ...authHeader(), "Content-Type": "multipart/form-data" }, 8 }); 9};
  1. 我们导出该服务。
    • UserService 对象包含用户所有提供的服务。
1// frontend/src/services/user.service.js 2import axios from "axios"; 3import authHeader from "./auth.header"; 4 5const upload = (data) => { 6 return axios.post(`/upload`, data, { 7 headers: { ...authHeader(), "Content-Type": "multipart/form-data" }, 8 }); 9}; 10 11const UserService = { 12 upload, 13}; 14 15export default UserService;

上传功能的 UI 实现

  1. 我们首先确保上传操作的 UI 表单包含以下三个部分:

    • 一个上传区域,允许用户从本地设备选择文件并上传到服务器。

    • 一个文本区域,供用户为上传的文件添加描述。

    • 一个“提交”按钮,点击时触发上传服务。

1// frontend/src/pages/Upload.js 2import React, { useState } from "react"; 3 4const Upload = () => { 5 const [file, setFile] = useState(null); 6 const [description, setDescription] = useState(""); 7 const [filename, setFilename] = useState(""); 8 9 return ( 10 <> 11 <form 12 onSubmit={handleUpload} 13 className="m-10 flex flex-col items-center justify-center" 14 > 15 <label className="flex flex-col items-center justify-center h-full w-full border-4 border-blue-200 border-dashed hover:bg-gray-100 hover:border-gray-300"> 16 <div className="flex flex-col items-center justify-center pt-7"> 17 <svg 18 xmlns="http://www.w3.org/2000/svg" 19 className="w-60 h-60 text-gray-500 group-hover:text-gray-600" 20 fill="none" 21 viewBox="0 0 24 24" 22 stroke="currentColor" 23 > 24 <path 25 strokeLinecap="round" 26 strokeLinejoin="round" 27 strokeWidth="2" 28 d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" 29 /> 30 </svg> 31 <p className="pt-1 text-lg tracking-wider text-gray-500 group-hover:text-gray-600"> 32 {filename || "上传文件"} 33 </p> 34 </div> 35 <input 36 type="file" 37 className="opacity-0" 38 onChange={(e) => { 39 setFile(e.target.files[0] || null); 40 setFilename(e.target.files[0]?.name || "上传文件"); 41 }} 42 /> 43 </label> 44 45 <textarea 46 onChange={(e) => setDescription(e.target.value)} 47 value={description} 48 className="mt-10 form-control block w-full px-3 py-1.5 text-base font-normal text-gray-700 bg-white bg-clip-padding border border-solid border-gray-300 rounded transition ease-in-out m-0 focus:text-gray-700 focus:bg-white focus:border-blue-600 focus:outline-none" 49 rows="3" 50 placeholder="文件描述" 51 ></textarea> 52 53 <input 54 type="submit" 55 value="上传" 56 className="w-80 cursor-pointer mt-10 bg-transparent hover:bg-blue-500 text-blue-700 font-semibold hover:text-white py-2 px-4 border border-blue-500 hover:border-transparent rounded" 57 /> 58 </form> 59 </> 60 ); 61}; 62 63export default Upload;

在上述实现中,我们可以看到以下几点:

  • 上传区域是一个被 label 包裹的隐藏 input 元素,占据页面大部分区域。该区域中央有一个 SVG 图标,表示这是文件上传的位置。

  • 上传区域包含一段文字,如果用户选择了文件,则显示文件名;否则显示“上传文件”提示。

  • input 的透明度设置为 0,因为我们只需要它的功能,不需要显示其外观。

  • file 和 filename 状态分别用于存储用户选择的文件及其名称。

  • 文本区域默认有 3 行高度,这是一个合适的初始尺寸,用户可根据需要调整大小。

  • description 状态用于在用户输入时保存文件描述内容。

  • 最后的 input 用于提交表单,因此我们需要在表单上处理 submit 事件。

  1. 我们实现 handleUpload 函数来处理表单提交动作。
1// frontend/src/pages/Upload.js 2import React, { useState } from "react"; 3import UserService from "../services/user.service"; 4 5const Upload = () => { 6 const [file, setFile] = useState(null); 7 const [description, setDescription] = useState(""); 8 const [filename, setFilename] = useState(""); 9 10 const handleUpload = (e) => { 11 e.preventDefault(); 12 if (file === null || !description) { 13 return; 14 } 15 16 const data = new FormData(); 17 data.append("file", file); 18 data.append("name", file?.name || "filename"); 19 data.append("description", description); 20 21 UserService.upload(data) 22 .then((res) => { 23 console.log(res); 24 }) 25 .catch((err) => { 26 console.log(err); 27 }); 28 }; 29 30 // ...其余代码 31}; 32 33export default Upload;

在上述实现中,我们可以看到以下几点:

  • 我们阻止了表单提交的默认行为。默认行为是刷新页面,而我们并不需要这一行为。

  • 我们检查用户是否选择了文件,并为文件填写了描述。如果没有,则不执行任何操作。

  • 我们将数据转换为 FormData 格式,以便将文件发送到服务器,让 Multer 能够正确处理。

  • 我们使用之前创建的 UserService.upload 服务上传文件,并暂时在控制台打印返回结果。

  1. 我们使用 Alert 组件向用户展示上传结果,以便他们知道文件是否上传成功。
1// frontend/src/pages/Upload.js 2import React, { useState } from "react"; 3import UserService from "../services/user.service"; 4import Alert from "../components/Alert"; 5 6const Upload = () => { 7 // ...其余代码 8 9 const [alertState, setAlertState] = useState({ 10 show: false, 11 color: "green", 12 msg: "", 13 }); 14 15 const handleUpload = (e) => { 16 e.preventDefault(); 17 if (file === null || !description) { 18 setAlertState({ 19 show: true, 20 color: "red", 21 msg: "您必须选择一个文件并填写描述", 22 }); 23 24 return; 25 } 26 27 const data = new FormData(); 28 data.append("file", file); 29 data.append("name", file?.name || "filename"); 30 data.append("description", description); 31 32 UserService.upload(data) 33 .then((res) => { 34 console.log(res); 35 setProcessing(false); 36 setAlertState({ 37 ...alertState, 38 show: true, 39 color: "green", 40 msg: res.data.message || "文件上传成功", 41 }); 42 }) 43 .catch((err) => { 44 console.log(err); 45 setProcessing(false); 46 setAlertState({ 47 ...alertState, 48 show: true, 49 color: "red", 50 msg: err.response.data || "文件上传失败", 51 }); 52 }); 53 }; 54 55 return ( 56 <> 57 <form 58 onSubmit={handleUpload} 59 className="m-10 flex flex-col items-center justify-center" 60 > 61 <div className="flex justify-center mt-10"> 62 {alertState.show ? ( 63 <Alert 64 color={alertState.color} 65 msg={alertState.msg} 66 showAlert={alertState.show} 67 setShowAlert={(value) => 68 setAlertState({ ...alertState, show: value }) 69 } 70 /> 71 ) : null} 72 </div> 73 </form> 74 { 75 // ...其余代码 76 } 77 </> 78 ) 79}; 80 81export default Upload;

在上述实现中,我们可以看到以下几点:

  • 我们使用 alertState 状态来存储 Alert 的配置,并根据需要进行更新。

  • 如果未选择文件或未填写描述,则显示红色 Alert 提示用户必须填写。

  • 如果文件上传成功,则显示绿色 Alert 并展示返回的消息。

  • 如果上传失败,则显示红色 Alert 并展示错误信息。

  1. 我们使用 Loader 组件在文件上传过程中向用户提供交互反馈。
1// frontend/src/pages/Upload.js 2import React, { useState } from "react"; 3import UserService from "../services/user.service"; 4import Alert from "../components/Alert"; 5import Loader from "../components/Loader"; 6 7const Upload = () => { 8 // ...其余代码 9 10 const [processing, setProcessing] = useState(false); 11 12 13 const handleUpload = (e) => { 14 // ...其余代码 15 16 setProcessing(true); 17 18 // ...其余代码 19 20 UserService.upload(data) 21 .then((res) => { 22 // ...其余代码 23 setProcessing(false); 24 }) 25 .catch((err) => { 26 // ...其余代码 27 setProcessing(false); 28 }); 29 }; 30 31 return ( 32 <> 33 <form 34 onSubmit={handleUpload} 35 className="m-10 flex flex-col items-center justify-center" 36 > 37 { 38 // ...其余代码 39 } 40 41 <div className="flex justify-center mt-10"> 42 {processing ? <Loader /> : null} 43 </div> 44 </form> 45 </> 46 ); 47}; 48 49export default Upload;

在上述实现中,我们可以看到以下几点:

  • 我们使用 processing 状态来决定是否渲染 Loader 组件。

  • 在 handleUpload 函数开始时,将 processing 置为 true,表示当前正在处理任务。

  • 一旦从服务器接收到响应,无论成功或失败,都立即将 processing 置为 false。

以下是该功能的完整实现:

服务器端路由

获取文件