文件上传下载以及大文件分片上传,断点续传,秒传
2026/9/12 19:17:17 网站建设 项目流程

本篇文章基于Vue3前端项目,详细讲解如何搞定文件上传,下载等功能。

一、文件上传

  1. 用户选择文件
  2. 前端拿到File对象
  3. 使用FromData封装文件和额外参数
  4. 通过axios/fetch发送multipart/from-data请求
  5. 监听上传进度
  6. 处理上传成功、失败、取消、大小限制、格式校验等逻辑

二、上传代码示例

1.原生input

<template> <div> <input type="file" @change="selectFile" /> <button @click="uploadFile()">上传</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)

三、封装上传API

1.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 service

2.上传接口

// 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 type="file" @change="handleChange" /> <button @click="submit">上传</button> <div v-if="progress > 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" name="file" :headers="headers" :on-sucess="handleSuccess" :on-error="handleError" :befor-upload="beforeUpload" > <el-button type="primary">点击上传</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) } </script>

2.手动上传

<template> <el-upload ref="uploadRef" :auto-upload="false" :limit="1" :on-change="handleChange" :before-upload="beforeUpload" > <template #trigger> <el-button type="primary">选择文件<el-button> </template> <el-button type="success" @click="submitUpload"> 上传服务器 </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 type="file" multiple @change="hangdleFiles" /> <button @click="uploadFiles">批量上传</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提供onUploadProgress

await axios.post('/api/upload',formData,{ onUploadProgress: progressEvent => { const percent = Math.round( progressEvent.loaded * 100 / progressEvent.total ) console.log(percent) } })

页面展示:

<el-progress :percentage="progress" />

完整示例:

<template> <input type="file" @change="handleChange" /> <el-button @click="upload">上传</el-button> <el-progress :percentage="progress" /> </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 type="file" @change="handleChange" /> <button @click="upload">上传</button> <button @click="cancelUpload">取消上传</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,chunkSize=5*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.前端一般需要计算文件hash

npm install spark-md5
import 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) })

十二、文件下载怎么做

文件下载主要分以下几种情况

  1. 后端返回文件流
  2. 后端返回文件URL
  3. 前端生成文件并下载
  4. 下载需要鉴权
  5. 下载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) }

注意:如果是跨域URL,download属性可能不生效,浏览器可能直接打开文件

十四、下载后返回的文件流

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-8''xxx.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` ) }

如果需要指定MIME:

const 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.预览PDF

const previewFdf = blob => { const url = URL.createObjectURL(blob) window.open(url) }

2.预览图片

<template> <img :src="imageUrl" /> </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 class="file-page"> <el-card> <template #header>文件上传</template> <el-upload :auto-upload="false" :limit="1" :on-change="handleFileChange" > <el-button type="primary">选择文件</el-button> </el-upload> <el-progress v-if="progress > 0" :percentage="progress" style="margin-top: 16px" /> <el-button type="success" style="margin-top: 16px" @click="handleUpload" > 上传 </el-button> </el-card> <el-card style="margin-top: 20px"> <template #header>文件列表</template> <el-table :data="fileList"> <el-table-column prop="fileName" label="文件名" /> <el-table-column prop="size" label="大小" /> <el-table-column label="操作"> <template #default="{ row }"> <el-button type="primary" link @click="handleDownload(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

二十五、实际开发建议

  1. 普通小文件上传:FormData + axios 即可;

  2. 大文件上传:使用分片上传、断点续传、秒传;

  3. 下载文件流:一定设置 responseType: 'blob';

  4. 下载文件名:优先从 Content-Disposition 获取;

  5. 跨域下载:后端暴露 Content-Disposition;

  6. 上传前要做大小、类型、数量校验;

  7. 文件上传接口建议统一封装;

  8. 下载工具函数建议统一封装;

  9. 上传时不要随便手动设置错误的 Content-Type;

  10. 后端返回错误时,前端要能识别 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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询