前端文件上传实战
Vue 3 + React 实现单文件和多文件上传
文件上传这个功能,说简单也简单,说复杂也真是能玩出不少花样。
记得我第一次写文件上传的时候,就是一个 <input type="file"> 配上 FormData,完事儿。但后来需求就开始不对劲了——要限制文件大小、要显示上传进度、要支持拖拽、要做文件类型校验…
得,那咱就一步步来,把常见的文件上传场景都覆盖一遍。
一、Vue 3 原生实现
1.1 单文件上传
先从最基础的开始,用原生方式实现单文件上传。
<template> <div> <!-- 文件选择框 --> <input type="file" @change="handleFile" /> <!-- 上传按钮 --> <button @click="upload" :disabled="!file">上传</button> <!-- 预览文件名 --> <p v-if="file">已选择: {{ file.name }}</p> </div> </template> <script setup> import { ref } from 'vue' const file = ref(null) // 监听文件选择事件 const handleFile = (e) => { file.value = e.target.files[0] } // 上传文件到服务器 const upload = async () => { if (!file.value) return const formData = new FormData() formData.append('file', file.value) await fetch('/api/upload', { method: 'POST', body: formData }) } </script> 这套代码看起来挺清爽的吧?
这里有几个小细节要注意一下:
e.target.files[0]获取的是第一个选中的文件,如果你想一次选多个,得把multiple属性加上FormData这个对象挺关键的,浏览器会自动帮你设置好Content-Type边界值,不用自己手动拼接
1.2 多文件上传
单文件搞定了,那多文件呢?其实改动不大。
<template> <div> <input type="file" multiple @change="handleFiles" /> <button @click="upload" :disabled="!files.length">批量上传</button> <ul> <li v-for="f in files" :key="f.name">{{ f.name }}</li> </ul> </div> </template> <script setup> import { ref } from 'vue' const files = ref([]) // 获取选中的多个文件 const handleFiles = (e) => { files.value = Array.from(e.target.files) } // 批量上传多个文件 const upload = async () => { if (!files.value.length) return const formData = new FormData() // 注意:这里要用相同的字段名 'files',后端才能用 List/MultipartFile[] 接收 files.value.forEach(f => formData.append('files', f)) await fetch('/api/upload/multiple', { method: 'POST', body: formData }) } </script> 这里有个坑我踩过,就是后端接收的时候,一定要保证前端 append 的字段名和后端的 @RequestParam("files") 或者 @ModelAttribute("files") 对得上,不然收不到文件。
二、Vue 3 + Element Plus 实现
原生写是能写,但样式得自己调,交互效果也得自己写。
说实话,一般项目我都会直接用组件库,省心。Element Plus 的 Upload 组件功能挺完善的,该有的都有。
2.1 单文件上传
用 Element Plus 写,代码确实少很多。
<template> <el-upload action="/api/upload" :on-success="handleSuccess" :on-error="handleError" :show-file-list="false" > <el-button type="primary">选择文件</el-button> </el-upload> </template> <script setup> // 上传成功回调 const handleSuccess = (res) => { ElMessage.success('上传成功') } // 上传失败回调 const handleError = () => { ElMessage.error('上传失败') } </script> action 是上传地址,这个是必填的。:show-file-list="false" 表示不显示已上传的文件列表,如果你只是想选个文件上传,这个挺合适的。
2.2 多文件上传
多文件其实就是加个 multiple 属性,再加个 limit 限制数量。
<template> <el-upload action="/api/upload" :file-list="fileList" :on-success="handleSuccess" :on-remove="handleRemove" :on-exceed="handleExceed" :limit="5" multiple > <el-button type="primary">选择文件</el-button> <template #tip> <div>支持 jpg、png、pdf 格式,单个文件不超过 10MB</div> </template> </el-upload> </template> <script setup> import { ref } from 'vue' const fileList = ref([]) // 文件数量超出限制了 const handleExceed = () => { ElMessage.warning('最多只能上传 5 个文件') } // 手动移除了某个文件 const handleRemove = (file) => { console.log('移除了:', file.name) } // 单个文件上传成功的回调 const handleSuccess = (res, file) => { ElMessage.success(`${file.name} 上传成功`) } </script> on-exceed 这个回调挺实用的,用户选了 10 个文件,你只让传 5 个,这时候就得提示一下用户。
2.3 拖拽上传
拖拽上传现在几乎是标配了,用户体验比点击选择好很多。
<template> <el-upload action="/api/upload" drag :auto-upload="false" :on-change="handleFileChange" > <el-icon><upload-filled /></el-icon> <div>将文件拖到此处,或<em>点击上传</em></div> </el-upload> </template> <script setup> import { UploadFilled } from '@element-plus/icons-vue' // 文件被添加或者被移除的时候触发 const handleFileChange = (file) => { console.log('选中文件:', file.name) } </script> :auto-upload="false" 这个很关键。默认情况下,文件一被选择就会立即上传。如果你用的是拖拽组件,通常的交互是用户把文件拖进来,等用户确认了再上传,这时候关掉自动上传就对了。
2.4 手动上传(上传前校验)
有时候我们需要在上传前做些校验,比如检查文件大小、格式之类的。
<template> <el-upload ref="uploadRef" action="/api/upload" :auto-upload="false" :before-upload="beforeUpload" :on-change="handleChange" > <el-button type="success">选取文件</el-button> <template #tip> <el-button type="primary" @click="submitUpload">开始上传</el-button> </template> </el-upload> </template> <script setup> import { ref } from 'vue' const uploadRef = ref(null) // 上传前的校验,返回 false 可以取消上传 const beforeUpload = (rawFile) => { // 检查文件大小,10MB const isLt10M = rawFile.size / 1024 / 1024 < 10 if (!isLt10M) { ElMessage.error('文件大小不能超过 10MB') return false } // 可以继续检查文件类型等等 return true } // 文件被添加或移除时的回调 const handleChange = (file) => { console.log('当前文件:', file.name) } // 手动调用上传 const submitUpload = () => { uploadRef.value.submit() } </script> beforeUpload 这个钩子非常有用,返回 false 就能阻止上传。用它来做文件校验,再合适不过了。
三、React + Ant Design 实现
说完 Vue,再来看看 React 那边。Ant Design 的 Upload 组件也很好用。
3.1 单文件上传
import { Upload, message } from 'antd' function SingleUpload() { // 上传成功 const handleSuccess = (res) => { message.success('上传成功') } // 上传失败 const handleError = () => { message.error('上传失败') } return ( <Upload action="/api/upload" onSuccess={handleSuccess} onError={handleError} showUploadList={false} > <button>选择文件</button> </Upload> ) } Ant Design 的写法跟 Element Plus 有点区别,它是 props 风格的回调,不过用起来都挺直观的。
3.2 多文件上传
import { Upload, message, Button } from 'antd' function MultiUpload() { // 超出数量限制了 const handleExceed = () => { message.warning('最多只能上传 5 个文件') } // 文件状态变化的时候 const handleChange = (info) => { // 如果手动删了一些文件,导致数量又合规了,这里其实不用特别处理 // Ant Design 会自动处理上传列表 if (info.fileList.length > 5) { handleExceed() return } // 可以在这里监听每个文件的上传状态 info.fileList.forEach(file => { if (file.status === 'done') { message.success(`${file.name} 上传成功`) } else if (file.status === 'error') { message.error(`${file.name} 上传失败`) } }) } return ( <Upload action="/api/upload" multiple maxCount={5} onChange={handleChange} > <Button>选择文件</Button> </Upload> ) } maxCount 这个属性很方便,直接限制最大数量,不用自己写回调去检查。
3.3 拖拽上传
Ant Design 专门提供了 Dragger 组件来做拖拽。
import { Upload, message } from 'antd' import { InboxOutlined } from '@ant-design/icons' function DragUpload() { // 拖拽释放后的回调 const handleDrop = ({ fileList }) => { console.log('拖拽进来的文件:', fileList) } return ( <Upload.Dragger action="/api/upload" multiple drag onDrop={handleDrop} > <p className="ant-upload-drag-icon"> <InboxOutlined /> </p> <p>点击或拖拽文件到此区域上传</p> </Upload.Dragger> ) } 拖拽区域的样式是封装好的,不用自己写 CSS,挺好的。
3.4 手动上传
Ant Design 默认是选中文件就上传,如果想手动上传,需要做点配置。
import { Upload, Button, message } from 'antd' import { UploadOutlined } from '@ant-design/icons' function ManualUpload() { const [fileList, setFileList] = useState([]) // 选完文件后,不触发上传,只更新列表 const handleChange = ({ fileList: newFileList }) => { setFileList(newFileList) } // 手动触上传 const handleUpload = () => { if (fileList.length === 0) { message.warning('请先选择文件') return } const formData = new FormData() fileList.forEach(file => { // originFileObj 才是原始的 File 对象 formData.append('files', file.originFileObj) }) fetch('/api/upload', { method: 'POST', body: formData }).then(() => { message.success('上传成功') setFileList([]) // 清空列表 }) } return ( <div> <Upload fileList={fileList} onChange={handleChange} // 返回 false 表示阻止自动上传 beforeUpload={() => false} multiple > <Button icon={<UploadOutlined />}>选择文件</Button> </Upload> <Button type="primary" onClick={handleUpload} style={{ marginTop: 16 }}> 开始上传 </Button> </div> ) } 这里有个小坑:fileList 里的每个 file 对象,它的 originFileObj 才是原始的 File 实例。直接用 file 可能会出问题。
四、进度监听
上传大文件的时候,用户肯定想知道上传进度,不然还以为页面卡死了。
Vue 3 进度监听
Element Plus 的 el-upload 组件没有直接提供进度回调,需要用原生 XMLHttpRequest 或者 axios 来实现。
<script setup> import { ref } from 'vue' const progress = ref(0) const uploading = ref(false) const uploadWithProgress = () => { const fileInput = document.querySelector('input[type="file"]') const file = fileInput.files[0] if (!file) return const formData = new FormData() formData.append('file', file) const xhr = new XMLHttpRequest() // 监听上传进度 xhr.upload.onprogress = (e) => { if (e.lengthComputable) { const percent = Math.round((e.loaded / e.total) * 100) progress.value = percent console.log(`上传进度: ${percent}%`) } } // 上传完成 xhr.onload = () => { if (xhr.status === 200) { ElMessage.success('上传成功') } uploading.value = false } xhr.open('POST', '/api/upload') xhr.send(formData) uploading.value = true } </script> <template> <div> <input type="file" @change="handleFile" /> <button @click="uploadWithProgress" :disabled="uploading"> {{ uploading ? `上传中 ${progress}%` : '上传' }} </button> <el-progress :percentage="progress" v-if="uploading" /> </div> </template> 如果你用的是 axios,就更简单了,axios 封装了进度事件。
import axios from'axios'constuploadWithAxios=(file)=>{const formData =newFormData() formData.append('file', file) axios.post('/api/upload', formData,{onUploadProgress:(progressEvent)=>{const percent = Math.round((progressEvent.loaded / progressEvent.total)*100) console.log(`进度: ${percent}%`)}})}React + Ant Design 进度属性
Ant Design 的 Upload 组件直接支持 onProgress 属性。
<Upload action="/api/upload" withCredentials // 带上 cookie 等凭证 onProgress={(event, file) => { // event.percent 是 0-100 的数字 const percent = Math.round(event.percent) console.log(`当前进度: ${percent}%`) }} > <Button>上传</Button> </Upload> withCredentials 这个属性记得加上,不然跨域上传的时候 cookie 之类的凭证不会带上。
五、总结对比
写了这么多,来张表总结一下各方案的特点:
| 功能 | Vue 原生 | Vue + Element Plus | React 原生 | React + Ant Design |
|---|---|---|---|---|
| 单文件上传 | input + fetch | el-upload | input + fetch | Upload |
| 多文件上传 | multiple + forEach | el-upload + multiple | multiple + forEach | Upload + multiple |
| 拖拽上传 | drag events | el-upload + drag | drag events | Upload.Dragger |
| 进度监听 | xhr.onprogress | 需二次封装 | xhr.onprogress | onProgress |
| 文件限制 | 手动校验 | :limit/:before-upload | 手动校验 | maxCount/beforeUpload |
| 手动上传 | 控制时机 | :auto-upload=“false” | 控制时机 | beforeUpload={() => false} |
| 上传样式 | 自己写 CSS | 开箱即用 | 自己写 CSS | 开箱即用 |
总的来说,如果你的项目已经用了 Element Plus 或 Ant Design,直接用它们的 Upload 组件是最省事的。如果你是原生开发,那就得自己多写点代码,但胜在依赖少、灵活性高。
文件上传这个功能,看起来简单,里面的门道还挺多的。实际项目中,根据业务需求选择合适的方案就好。