本篇文章基于Vue3前端项目详细讲解如何搞定文件上传下载等功能。一、文件上传用户选择文件前端拿到File对象使用FromData封装文件和额外参数通过axios/fetch发送multipart/from-data请求监听上传进度处理上传成功、失败、取消、大小限制、格式校验等逻辑二、上传代码示例1.原生inputtemplate div input typefile changeselectFile / button clickuploadFile()上传/button /div /template script setup import { ref } from vue import axios from axios const file ref(null) const selectFile(event) { file.value event.target.files[0] } const uploadFile async() { if(!file.value) { alert(请先选择文件) return } const formData new FormData() formData.append(file,file.value) try { const res await axios.post(/api/upload,formData, { headers: { Content-Type: multipart/form-data } }) conosle.log(上传成功,res.data) }catch(error) { console.log(上传失败,error) } /script一般来说使用FormData时可以不手动设置Content-Type,浏览器会自动带上boundary。更推荐await axios.post(/api/upload,formData)三、封装上传API1.vue中axios封装// src/utils/request.js import axios from axios const service axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL, timeout: 30000 }) service.interceptors.request.use( config { const token localStorage.getItem(token) if(token){ config.headers.Authorization Bearer${token} } return config }, error Promise.reject(error) ) service.interceptors.response.use( response response.data, error { return Promise.reject(error) } ) export default service2.上传接口// src/api/file.js import request from /utils/request export function uploadFileApi(data,onUploadProgress) { return request({ url: file/upload, method: post, data, onUploadPorgress }) }3.页面中使用template input typefile changehandleChange / button clicksubmit上传/button div v-ifprogress 0 上传进度{{ progress }}% /div /template script setup import { ref } from vue impirt { uploadFile } from /api/file const selectedFile ref(null) const progress ref(0) const handleChange e { selectedFile.value e.target.files[0] } const submit async () { if(!selectFile.value) return const formData new FormData() formData.append(file,selectedFile.value) formData.append(type,avatar) const res await uploadFileApi(formData, event { if(event.total) { progress.value Math.round((event.loaded*100)/event.total) } }) console.log(res) } /script四、使用Element Plus上传组件1.自动上传template el-upload action/api/file/upload namefile :headersheaders :on-sucesshandleSuccess :on-errorhandleError :befor-uploadbeforeUpload el-button typeprimary点击上传/el-button /el-upload /template script setup const headers { Authorization: Bearer ${localStorage.getItem(token)} } const beforeUpload file { const isLt10M file.size /1024/1024 10 if(isLt10M) { Elmessage.error(文件大小不能超过10MB) return false } return true } const handleSuccess response { console.log(上传成功,response) } const handlerError error { conosle.log(上传失败,error) } /script2.手动上传template el-upload refuploadRef :auto-uploadfalse :limit1 :on-changehandleChange :before-uploadbeforeUpload template #trigger el-button typeprimary选择文件el-button /template el-button typesuccess clicksubmitUpload 上传服务器 /el-button /el-upload /template script setup import { ref } from vue import { Elmessage } from element-plus import { uploadFileApi } from /api/file const uploadRef ref() const fileObj ref(null) const handleChange uploadFile { fileObj.value uploadFile.raw } const beforeUpload file { const allowTypes [image/png,image/jpeg,application/pdf] const isAllowed allowedTypes.includes(file.type) const isLt20M file.size /1024/1024 20 if(!isAllowed) { ElMessage.error(只支持png、jpg、pdf文件) return false } if(!isLt20M){ ElMessage.error(文件不能超过20MB) return false } return true } const submitUpload async () { if(!fileObj.value) { ElMessage.warning(请选择文件) return } const formData new FormData() formData.append(file,fileObj.value) try { const res await uploadFileApi(formData) ElMessage.success(上传成功) console.log(res) }catch{ ElMessage.error(上传失败) } } /script五、多文件上传template input typefile multiple changehangdleFiles / button clickuploadFiles批量上传/button /template script setup import { ref } from vue import { uploadFileApi } from /api/file const file ref([]) const handleFiles e { files.value Array.from(e.target.files) } const uploadFiles async() { const formData new FormData() files.value.forEach(file { formData.append(files,file) }) const res await uploadFileApi(formData) console.log(res) } /script后端常见接收方式:files: MultipartFile[]有些后端这样要求formData.append(fileList,file)字段名前后端要保持一致。六、上传时携带额外参数const formData new FormData() formData.append(file,file) formData.append(userId,userId) formData.append(bizType,constract) formData.append(remark,合同附件) await uploadFileApi(formData)后端可以同时接手文件和普通字段七、上传前校验常见校验包括文件大小文件类型文件数量文件名长度是否为空文件图片宽高视频时长Excel表头格式等1.校验文件大小和类型const validateFile file { const maxSize 10*1024*1024 const allowedTypes [ image/jpeg, image/png, application/pdf ] if(!allowedTypes.includes(file.type)) { throw new Error(文件格式不正确) } if(file.size maxSize) { throw new Error(文件大小不能超过10MB) } return true }2.检验图片宽高const checkImageSize file { return new Promise((resolve,reject) { const img new Image() const url URL.createObjectURL(file) img.onload () { URL.revokeObjectURL(url) if(img.width 300 img.height 300) { resolve(true) } else { reject(new Error(图片尺寸必须是300X300)) } } img.onerror () { URL.revokeObjectURL(url) reject(new Error(图片加载失败)) } img.src url }) }八、上传进度条Axios提供onUploadProgressawait axios.post(/api/upload,formData,{ onUploadProgress: progressEvent { const percent Math.round( progressEvent.loaded * 100 / progressEvent.total ) console.log(percent) } })页面展示el-progress :percentageprogress /完整示例template input typefile changehandleChange / el-button clickupload上传/el-button el-progress :percentageprogress / /template script setup import { ref } from vue import axios from axios const file ref(null) const progress ref(0) const handleChange e { file.value e.target.files[0] } const upload async () { const formData new FormData() formData.append(file,file.value) await axios.post(/api/upload,formData, { onUploadProgress: e { progress.value Math.round((e.loaded*100)/e.total) } }) } /script九、取消上传Axios新版本可以使用AbortController。template input typefile changehandleChange / button clickupload上传/button button clickcancelUpload取消上传/button /template script setup import { ref } from vue import axios from axios const file ref(null) const controller ref(null) const handleChange e { file.value e.target.files[0] } const upload async() { controller.value new AbortController() const formData new FormData() formData.append(file,file.value) try { await axios.post(/api/upload, formData, { signal: controller.value.signal }) }catch(error) { if(axios.isCancel?.(error) || error.name CanceledError) { console.log(上传已取消) }else{ console.log(error) } } } const cancelUpload () { controller.value?.abort() } /script十、大文件分片上传当文件较大比如视频、压缩包、安装包、直接上传容易失败需要分片上传1.分片上传流程前端选择大文件按固定大小切片例如每片5MB计算文件唯一标识例如hash调接口查询已上传分片实现妙传、断点续传上传未完成的分片所有分片上传完成后通知后端合并文件2.文件切片const createChunks (file,chunkSize5*1024*1024) { const chunks [] let cur 0 while (cur file.size) { chunks.push(file.slice(cur,cur chunkSize)) cur chunkSize } return chunks }3.上传分片const uploadChunks async file { const chunks createChunks(file) const fileName file.name const requests chunks.map((chunk,index) { const formData new FormData() formData.append(file,chunk) formData.append(fileName,fileName) formData.append(chunkIndex,index) formData.append(totalChunks,chunks.length) return axios.post(/api/file/upload-chunk,formData) }) await Promise.all(requests) await axios.post(/api/file/merge, { fileName, totalChunks: chunks.length }) }4.控制并发上传大文件分片不能一次性全部并发否则浏览器和服务器压力很大async function uploadWithConcurrency(task,limit 3) { const results [] const executing [] for(const task of tasks) { const p Promise.resolve().then(() task()) result.push(p) if(limit task.length) { const e p.then(() executing.splice(executing.indexOf(e),1)) executing.push(e) if(executing.length limit) { await Promise.race(executing) } } } return Promise.all(results) }使用const tasks chunks.map((chunk,index) { return () { const formData new FormData() formData.append(file,chunk) formData.append(chunkIndex,index) return axios.post(/api/file/upload-chunk,formData) } }) await iploadWithConcurrency(tasks,3)十一、断点续传和秒传1.前端一般需要计算文件hashnpm install spark-md5import SparkMD5 from spark-md5 const calculateHash file { return new Promise((resolve,reject) { const chunkSize 2*1024*1024 const chunks Math.ceil(file.size / chunkSize) let currentChunk 0 const spark new SparkMD5.ArrayBuffer() const fileReader new FileReader() fileReader.onload e { spark.append(e.target.result) currentChunk if(currentChunk chunks) { loadNext() }else{ resolve(spark.end()) } } fileReader.onerror reject function loadNext() { const start currentChunk * chunkSize const end Math.min(start chunkSize, file.size) fileReader.readAsArrayBuffer(file.slice(start,end)) } loadNext() }) }2.秒传逻辑const hash await calculateHash(file) const checkRes await axios.get(/api/file/check,{ params: { hash, fileName: file.name } }) if(checkRes.data.upload) { console.log(文件已存在秒传成功) return }3.跳过已上传分片const uploadedChunks checkRes.data.uploadedChunks || [] const tasks chunks.map((chunk,index) ({chunk,index})) .filter(item !uploadedChunks.includes(item.index)) .map(item (){ const formData new FormData() formData.append(file,item.chunk) formData.append(hash,hash) formData.append(chunkIndex,item.index) formData.append(fileName,file.name) return axios.post(/api/file/upload-chunk,formData) })十二、文件下载怎么做文件下载主要分以下几种情况后端返回文件流后端返回文件URL前端生成文件并下载下载需要鉴权下载Excel、PDF、图片、压缩包等十三、通过URL直接下载如果后端直接返回一个文件地址window.location.href fileUrl或者const a document.createElement(a) a.href fileUrl a.download 文件名.pdf a.click()封装export function downloadByUrl(url,filenme) { const a document.createElement(a) a.href url a.download filename || a.target _blank document.body.appendChild(a) a.click() document.body.removeChild(a) }注意如果是跨域URLdownload属性可能不生效浏览器可能直接打开文件十四、下载后返回的文件流1.API封装// src/api/file.js import request from /utils/request export function downloadFileApi(params) { return request({ url: file/download, method: get, params, responseType: blob // 关键点 }) }2.下载工具函数export function downloadBlob(blob,filename) { const url window.URL.createObjectURL(blob) const a document.createElement(a) a.href url a.dowmload filename document.body.appendChild(a) a.click() document.body.removeChild(a) window.URL.revokeObjectURL(url) }3.页面中下载import { downloadFileApi } from /api/file import { downloadBlob } from /utils/download const handleDownload async row { const blob await downloadFileApi({id: row.id }) downloadBlob(blob,row.fileName) }十五、Axios响应拦截器对blob的处理如果你封装了axios下载文件时可能出问题比如普通接口返回response response.data下载接口返回的是Blob可以正常返回service.interceptors.response.use( response { if(response.config.responseType blob){ return response.data } return response.data }, error Promise.reject(error) )更完整一点service.interceptors.response.use( async response { const { data, config } response if(config.responseType blob) { return data } return data }, error Promise.reject(error) )十六、从响应头获取文件名后端通常会在响应头中返回Content-Disposition: attachment;filename*UTF-8xxx.xlsx前端获取export function getFilenameFromDisposition(disposition) { if(!dispositon) return const utf8FilenameRegex filename\*UTF-8([^;])/i const asciiFilenameRegex /filename?([^;])?/i const utf8Matches disposition.match(utf8FilenameRegex) if(utf8Matches utf8Matches[1]) { return decodeURIComponent(utf8Matches[1]) } const asciiMatches disposition.match(asciiFilenameRegex) if(asciiMatches asciiMatches[1]) { return decodeURIComponent(asciiMatches[1]) } return }但是如果axios拦截器只返回response.data就拿不到headers。可以单独封装下载请求export function downloadFileApi(params) { return request({ url: /file/download, method: get, params, responseType: blob, returnFullResponse: true }) }拦截器service.interceptors.response.use( response { if(response.config.returnFullResponse) { return response } return response.data } error Promise.reject(error) )页面const res await downloadFileApi({id: row.id}) const disposition res.headers[content-disposition] const filename getFilenameFormDisposition(disposition) || row.fileName downloadBlob(res.data,filename)后端还要暴露响应头Access-Control-Expose-Headers: Content-Disposition否则跨域时前端读不到Content-Disposition十七、下载Excel文件export function exportExcel(params) { return request({ url: /user/export, method: get, params, responseType: blob }) }const handleExport async () { const blob await exportExcel(queryParams.value) downloadBlob(blob,用户列表_${Date.now()}.xlsx ) }如果需要指定MIMEconst excelBlob new Blob([blob],{ type:application/vnd.openxmlformats-officedocument.spreadsheetml.sheet }) downloadBlob(excelBlob, 用户列表.xlsx)十八、下载时后端返回错误JSON怎么办下载接口设置了responseType:blob即使后端报错前端拿到的也是Blob。需要判断Blob类型async function handleBlobResponse(blob) { if(blob.type application/json) { const text await bolb.text() const json JSON.parse(text) throw new Error(json.message || 下载失败) } return blob }使用try { const blob await downloadFileApi(params) await handleBlobRespones(blob) downloadBlob(blob,文件.xlsx) } catch(error) { ElMessage.error(error.message) }更严谨const blob await downloadFileApi(params) if(blob.type.includes(application/json)) { const text await blob.text() const data JSON.parse(text) ElMessage.error(data.message || 下载失败) return } downloadBlob(blob,导出.xlsx)十九、预览文件下载不一定总是保存是有些文件需要预览1.预览PDFconst previewFdf blob { const url URL.createObjectURL(blob) window.open(url) }2.预览图片template img :srcimageUrl / /template script setup import { ref } from vue const imageUrl ref() const previewImage blob { imageUrl.value URL.createObjectURL(blob) } /script组件卸载时释放import { onBeforeUnmount } from vue onBeforeUnmount(() { if(imageUrl.value){ URL.revokeObjectURL(imageUrl.value) } )二十、完整项目目录建议src ├─ api │ └─ file.js ├─ utils │ ├─ request.js │ └─ download.js ├─ views │ └─ file │ └─ FileManage.vue二十一、文件接口封装示例// src/api/file.js import request from /utils/request export function uploadFile(data,onUploadProgress) { return request({ url: /file/upload, method: post, data, onUploadProgress }) } export function uploadChunk(data) { return ({ url: /file/upload-chunk, methods: post, data }) } export function mergeChunks(data) { return request({ url: /file/merge, method: post, data }) } export function checkFile(params) { return request({ url: /file/check, method: get, params }) } export function downloadFile(params) { return request({ url: /file/download, method: get, params, responseType: blob }) } export function deleteFile(id) { return request({ url: /file/${id}, method: delete }) }二十二、下载工具完整封装// src/utils/download.js export function downloadBlob(blob, filename download) { const objectUrl window.URL.createObjectURL(blob) const link document.createElement(a) link.href objectUrl link.download filename link.style.display none document.body.appendChild(link) link.click() document.body.removeChild(link) window.URL.revokeObjectURL(objectUrl) } export function downloadByUrl(url, filename) { const link document.createElement(a) link.href url link.download filename || link.target _blank document.body.appendChild(link) link.click() document.body.removeChild(link) } export function getFilenameFromDisposition(disposition) { if (!disposition) return const filenameStar disposition.match(/filename\*UTF-8([^;])/i) if (filenameStar) { return decodeURIComponent(filenameStar[1]) } const filename disposition.match(/filename?([^;])?/i) if (filename) { return decodeURIComponent(filename[1]) } return } export async function parseBlobError(blob) { if (blob.type blob.type.includes(application/json)) { const text await blob.text() return JSON.parse(text) } return null }二十三、完整上传下载页面示例template div classfile-page el-card template #header文件上传/template el-upload :auto-uploadfalse :limit1 :on-changehandleFileChange el-button typeprimary选择文件/el-button /el-upload el-progress v-ifprogress 0 :percentageprogress stylemargin-top: 16px / el-button typesuccess stylemargin-top: 16px clickhandleUpload 上传 /el-button /el-card el-card stylemargin-top: 20px template #header文件列表/template el-table :datafileList el-table-column propfileName label文件名 / el-table-column propsize label大小 / el-table-column label操作 template #default{ row } el-button typeprimary link clickhandleDownload(row) 下载 /el-button /template /el-table-column /el-table /el-card /div /template script setup import { ref } from vue import { ElMessage } from element-plus import { uploadFile, downloadFile } from /api/file import { downloadBlob, parseBlobError } from /utils/download const currentFile ref(null) const progress ref(0) const fileList ref([ { id: 1, fileName: 测试文件.xlsx, size: 20KB } ]) const handleFileChange uploadFileItem { currentFile.value uploadFileItem.raw } const handleUpload async () { if (!currentFile.value) { ElMessage.warning(请选择文件) return } const formData new FormData() formData.append(file, currentFile.value) try { await uploadFile(formData, e { if (e.total) { progress.value Math.round((e.loaded * 100) / e.total) } }) ElMessage.success(上传成功) } catch (error) { ElMessage.error(上传失败) } } const handleDownload async row { try { const blob await downloadFile({ id: row.id }) const errorData await parseBlobError(blob) if (errorData) { ElMessage.error(errorData.message || 下载失败) return } downloadBlob(blob, row.fileName) } catch (error) { ElMessage.error(下载失败) } } /script二十四、常见问题总结1. 上传接口 415 Unsupported Media Type通常是请求头设置不对。建议使用const formData new FormData() formData.append(file, file) axios.post(/api/upload, formData)不要强行写错Content-Type。2. 后端拿不到文件检查字段名是否一致formData.append(file, file)后端也要用file接收。3. 下载文件乱码常见原因没有设置responseType: blob后端文件名编码不正确前端没有正确解析Content-Disposition跨域没有暴露Content-Disposition。4. 下载接口返回了 JSON但前端当文件下载了需要判断 Blob 类型if (blob.type.includes(application/json)) { const text await blob.text() const data JSON.parse(text) ElMessage.error(data.message) return }5. 跨域下载读不到文件名后端需要加Access-Control-Expose-Headers: Content-Disposition二十五、实际开发建议普通小文件上传FormData axios 即可大文件上传使用分片上传、断点续传、秒传下载文件流一定设置 responseType: blob下载文件名优先从 Content-Disposition 获取跨域下载后端暴露 Content-Disposition上传前要做大小、类型、数量校验文件上传接口建议统一封装下载工具函数建议统一封装上传时不要随便手动设置错误的 Content-Type后端返回错误时前端要能识别 Blob 中的 JSON。简单结论Vue 项目中文件上传的核心是const formData new FormData() formData.append(file, file) axios.post(/api/upload, formData)文件下载的核心是axios.get(/api/download, { responseType: blob })然后通过URL.createObjectURL(blob)配合a标签触发浏览器下载。本文章参考借鉴转载自CSDN博主「zqOrange」的原创文章。原文链接https://blog.csdn.net/2302_81102841/article/details/162604591