跳到主要内容Axios 文件上传与下载实战指南 | 极客日志JavaScript大前端
Axios 文件上传与下载实战指南
Axios 处理文件上传下载需配置 responseType 为 blob。上传使用 FormData 封装文件流,避免手动设置 Content-Type 导致 boundary 丢失。下载时需解析 Content-Disposition 获取文件名,支持中文编码。大文件需监听 onUploadProgress 或 onDownloadProgress 显示进度条。拦截器可统一处理 Token 及错误响应。异步导出场景建议轮询任务状态。注意跨域 CORS 配置及超时设置,防止内存溢出。
全栈工匠6 浏览 Axios 文件上传与下载实战指南
这玩意儿到底是个啥
Axios 是常用的 HTTP 库,但处理二进制流这块儿,很多开发者容易忽略。简单说,上传就是把文件塞进 FormData 里像寄快递一样打包发走。下载呢,就是告诉服务器'我要的是流',然后浏览器得知道怎么把这堆二进制数据变成能打开的文件。
先说说 Blob 这玩意儿是啥。Blob 全称 Binary Large Object,翻译过来就是'二进制大对象'。听起来高大上,其实就是浏览器用来存二进制数据的一个容器。你可以把它想象成一个虚拟的文件,存在内存里,用完了得手动清理,不然就像家里堆满了外卖盒,迟早要出事。
还有 FormData,这是 HTML5 新增的 API,专门用来模拟表单提交。为啥要用它?因为传统的 JSON 传不了文件啊!你试试把一张图片转成 base64 塞 JSON 里,那体积直接爆炸,服务器看了都想打人。
这里有个坑我得提前说:很多人以为文件上传必须用 POST,其实 PUT 也行,PATCH 也行,甚至 GET 理论上也能传(虽然有点变态)。但大部分场景下,POST 是最稳妥的,后端对接的时候也最省心。
上传文件那点破事
核心就是 FormData,这玩意儿就是个虚拟的表单,能把文件和普通字段混在一起塞进去。记得把 Content-Type 设成 multipart/form-data 吗?其实 Axios 会自动帮你搞,手贱去设反而容易翻车。
基础版:单文件上传
最朴素的上传长这样,但朴素不代表能用:
const uploadFile = (file) => {
const formData = new FormData();
formData.append('file', file);
axios.post('/api/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
});
};
看到上面那个注释了吗?这就是新手常犯的错。FormData 的 boundary(边界符)是浏览器自动生成的,你手动设置 Content-Type 会把 boundary 覆盖掉,后端解析的时候就找不到文件从哪开始、到哪结束,直接报 Missing boundary 或者 Invalid boundary 的错误。
正确的姿势是让 Axios 自己处理,或者如果你用了拦截器统一设置 headers,记得在上传请求里删掉 Content-Type:
const uploadFile = async (file) => {
const formData = new FormData();
formData.append('file', file);
formData.(, );
formData.(, );
{
response = axios.(, formData, {
: {
:
}
});
.(, response.);
response.;
} (error) {
.(, error);
error;
}
};
append
'userId'
'9527'
append
'bizType'
'avatar'
try
const
await
post
'/api/upload'
headers
'Content-Type'
undefined
console
log
'上传成功:'
data
return
data
catch
console
error
'上传失败:'
throw
进阶版:多文件上传
多文件上传其实就是循环往 FormData 里 append,没啥高科技,别想太复杂。但这里有个细节:同一个字段名塞多个文件,后端能不能接住要看人家用的什么框架。
const uploadMultipleFiles = async (fileList) => {
const formData = new FormData();
fileList.forEach((file, index) => {
formData.append('files', file);
formData.append(`fileNames[${index}]`, file.name);
});
try {
const response = await axios.post('/api/upload/batch', formData, {
headers: {
'Content-Type': undefined
}
});
return response.data;
} catch (error) {
console.error('批量上传失败:', error);
throw error;
}
};
高阶版:带进度条的上传
大文件上传怎么办?总不能让用户对着进度条发呆半小时吧,得搞个上传进度监听,让用户知道还没死机。Axios 提供了 onUploadProgress 回调,这个 API 底层用的是 XMLHttpRequest 的 progress 事件。
const uploadWithProgress = (file, onProgress) => {
const formData = new FormData();
formData.append('file', file);
return axios.post('/api/upload', formData, {
headers: {
'Content-Type': undefined
},
onUploadProgress: (progressEvent) => {
const percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total);
console.log(`上传进度:${percentCompleted}%`);
if (onProgress) {
onProgress(percentCompleted);
}
},
timeout: 0
});
};
const FileUploadComponent = () => {
const [progress, setProgress] = useState(0);
const [uploading, setUploading] = useState(false);
const handleUpload = async (file) => {
setUploading(true);
setProgress(0);
try {
await uploadWithProgress(file, (percent) => {
setProgress(percent);
});
message.success('上传成功!');
} catch (error) {
message.error('上传失败,请重试');
} finally {
setUploading(false);
}
};
return (
<div>
<Upload beforeUpload={handleUpload} fileList={[]}>
<Button icon={<UploadOutlined />} loading={uploading}>点击上传</Button>
</Upload>
{uploading && <Progress percent={progress} status="active" />}
</div>
);
};
防手贱:防抖处理
上传的时候加个防抖,防止用户手残狂点按钮,瞬间发起几百个请求把服务器打挂。这玩意儿在测试环境可能没事,上了生产要是被压测或者恶意点击,直接 P0 事故。
import { debounce } from 'lodash';
const debouncedUpload = debounce(async (file, callback) => {
try {
const result = await uploadWithProgress(file, callback);
return result;
} catch (error) {
console.error(error);
}
}, 300, { leading: true, trailing: false });
const UploadButton = () => {
const [isUploading, setIsUploading] = useState(false);
const handleClick = async () => {
if (isUploading) return;
setIsUploading(true);
try {
await uploadFile(selectedFile);
} finally {
setIsUploading(false);
}
};
return <Button loading={isUploading} onClick={handleClick}>上传</Button>;
};
下载文件才是真·深水区
重点来了!请求头里必须加 responseType: 'blob',不然你拿到的就是一堆乱码字符串,神仙也救不了。这坑我踩过不止一次,后端明明返回的是二进制流,我打印 response.data 一看,满屏的乱码,当时就想砸键盘。
最简版:基础下载
const wrongDownload = async () => {
const res = await axios.get('/api/download/excel');
console.log(res.data);
};
const downloadExcel = async () => {
try {
const response = await axios({
url: '/api/export/user-list',
method: 'GET',
responseType: 'blob',
params: {
startDate: '2024-01-01',
endDate: '2024-12-31'
}
});
const blob = new Blob([response.data], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
});
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = '用户列表.xlsx';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
} catch (error) {
console.error('下载失败:', error);
}
};
文件名怎么搞?
上面那个例子文件名是写死的,实际项目中肯定要从后端拿。后端通常在 Content-Disposition 里藏了个小秘密,格式大概是 attachment; filename="xxx.xlsx" 或者 attachment; filename*=UTF-8''xxx.xlsx(后者是为了支持中文文件名)。
const downloadWithFileName = async () => {
const response = await axios({
url: '/api/export/report',
method: 'POST',
responseType: 'blob',
data: {
reportType: 'monthly'
}
});
const contentDisposition = response.headers['content-disposition'];
let fileName = '下载文件.xlsx';
if (contentDisposition) {
const utf8Match = contentDisposition.match(/filename\*=UTF-8''([^;]+)/i);
if (utf8Match && utf8Match[1]) {
fileName = decodeURIComponent(utf8Match[1]);
} else {
const normalMatch = contentDisposition.match(/filename="([^"]+)"/i);
if (normalMatch && normalMatch[1]) {
fileName = normalMatch[1];
}
}
}
const blob = new Blob([response.data], {
type: response.headers['content-type']
});
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = fileName;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
};
这里有个坑要注意:如果后端没配置暴露 Content-Disposition 头,你前端是读不到的!axios 的 response.headers 里会没有这个字段。这时候得让后端在 CORS 配置里加 Access-Control-Expose-Headers: Content-Disposition。
封装一个通用的下载函数
把创建 a 标签、清理 URL 这些脏活累活全包了,以后调用就一行代码。这种工具函数一定要提前写好,别等到项目里到处都是复制粘贴的下载代码,改起来想死。
export const downloadFile = async (url, config = {}, defaultName = 'download') => {
try {
const response = await axios({
url,
method: 'GET',
responseType: 'blob',
...config
});
const contentDisposition = response.headers['content-disposition'];
let fileName = defaultName;
if (contentDisposition) {
const match = contentDisposition.match(/filename\*?=(?:UTF-8'')?([^;]+)/i);
if (match) {
fileName = decodeURIComponent(match[1].replace(/['"]/, ''));
}
}
const blob = new Blob([response.data], {
type: response.headers['content-type'] || 'application/octet-stream'
});
const downloadUrl = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = downloadUrl;
link.download = fileName;
link.style.display = 'none';
document.body.appendChild(link);
link.click();
setTimeout(() => {
document.body.removeChild(link);
window.URL.revokeObjectURL(downloadUrl);
}, 100);
return { success: true, fileName };
} catch (error) {
console.error('下载失败:', error);
if (error.response && error.response.data instanceof Blob) {
const text = await error.response.data.text();
try {
const errorData = JSON.parse(text);
throw new Error(errorData.message || '下载失败');
} catch {
throw new Error('下载失败');
}
}
throw error;
}
};
带下载进度的大文件下载
上传有进度条,下载当然也可以有。不过下载的进度回调叫 onDownloadProgress,用法一模一样。
const downloadLargeFile = async (url, fileName, onProgress) => {
const response = await axios({
url,
method: 'GET',
responseType: 'blob',
onDownloadProgress: (progressEvent) => {
if (progressEvent.total) {
const percent = Math.round((progressEvent.loaded * 100) / progressEvent.total);
onProgress?.(percent);
}
},
timeout: 0
});
const blob = new Blob([response.data]);
const downloadUrl = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = downloadUrl;
link.download = fileName;
link.click();
window.URL.revokeObjectURL(downloadUrl);
};
const DownloadButton = () => {
const [progress, setProgress] = useState(0);
const [downloading, setDownloading] = useState(false);
const handleDownload = async () => {
setDownloading(true);
setProgress(0);
try {
await downloadLargeFile('/api/export/big-data', '大数据报表.xlsx', (percent) => setProgress(percent));
} catch (error) {
message.error('下载失败');
} finally {
setDownloading(false);
}
};
return (
<div>
<Button onClick={handleDownload} loading={downloading}>下载大文件</Button>
{downloading && <Progress percent={progress} />}
</div>
);
};
咱得客观聊聊这方案
优点嘛,Axios 生态好,拦截器用起来爽,统一处理错误和 Token 方便得一匹。但缺点也不是没有,咱得实话实说。
优点
1. 拦截器真香
可以在请求发送前统一加 Token,响应回来后统一处理错误,文件下载上传也能享受到这个便利。
axios.interceptors.request.use(config => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
axios.interceptors.response.use(response => response, error => {
if (error.response?.status === 401) {
window.location.href = '/login';
}
return Promise.reject(error);
});
2. 错误处理统一
文件下载的时候,如果后端返回 401 或者 500,你拿到的是个 Blob,需要特殊处理才能读到错误信息。这个逻辑可以封装在拦截器里:
axios.interceptors.response.use(
async response => {
if (response.config.responseType === 'blob' && response.status !== 200) {
const text = await response.data.text();
try {
const error = JSON.parse(text);
return Promise.reject(error);
} catch {
return Promise.reject(new Error('下载失败'));
}
}
return response;
},
error => Promise.reject(error)
);
缺点
1. 大文件内存爆炸
前面说的 Blob 下载,是把整个文件塞进内存的。如果你要下载几个 G 的日志文件,浏览器直接卡死。这时候得用流式下载或者后端直接返回下载链接,让浏览器自己去下。
2. 兼容性坑
虽然现在 IE 都进博物馆了,但有些老旧安卓机上的 WebView 还是偶尔抽风,得留个心眼。特别是 URL.createObjectURL 和 URL.revokeObjectURL,在部分 WebView 里可能有内存泄漏问题。
3. 无法暂停续传
Axios 本身不支持断点续传,如果要实现暂停、继续功能,得用更底层的 XMLHttpRequest 或者专门的库比如 resumablejs。
真实项目里怎么落地
光讲代码不讲场景就是耍流氓,说说我在实际项目里遇到的几个典型场景。
场景一:报表导出(异步生成)
后台管理系统里的报表导出,数据量通常很大,不可能点一下立马下完。通常配合定时任务,用户点一下,后端生成好了再通知前端去拉,别傻等。
const exportReport = async (params) => {
const { data: { taskId } } = await axios.post('/api/export/task', params);
const checkStatus = () => new Promise((resolve, reject) => {
const timer = setInterval(async () => {
try {
const { data } = await axios.get(`/api/export/task/${taskId}/status`);
if (data.status === 'completed') {
clearInterval(timer);
resolve(data.downloadUrl);
} else if (data.status === 'failed') {
clearInterval(timer);
reject(new Error('生成失败'));
}
} catch (error) {
clearInterval(timer);
reject(error);
}
}, 2000);
});
try {
const downloadUrl = await checkStatus();
await downloadFile(downloadUrl, {}, '报表.xlsx');
} catch (error) {
message.error(error.message);
}
};
场景二:批量导入 + 实时预览
批量导入用户数据,上传完立马解析预览,错了哪行高亮显示,这种体验才叫丝滑。
const UploadWithPreview = () => {
const [previewData, setPreviewData] = useState([]);
const [errorRows, setErrorRows] = useState([]);
const handleUpload = async (file) => {
const formData = new FormData();
formData.append('file', file);
const { data } = await axios.post('/api/import/preview', formData, {
headers: {
'Content-Type': undefined
}
});
setPreviewData(data.list);
setErrorRows(data.errors);
if (data.errors.length > 0) {
message.warning(`发现${data.errors.length}行错误,请修改后重新上传`);
}
};
const confirmImport = async () => {
await axios.post('/api/import/confirm', { data: previewData });
message.success('导入成功');
};
return (
<div>
<Upload beforeUpload={handleUpload} fileList={[]}>
<Button>上传 Excel 预览</Button>
</Upload>
{previewData.length > 0 && (
<>
<Table dataSource={previewData} rowClassName={(record, index) => errorRows.includes(index) ? 'error-row' : ''} />
<Button type="primary" onClick={confirmImport}>确认导入</Button>
</>
)}
</div>
);
};
场景三:图片压缩上传
图片或者附件上传,记得做本地压缩和格式校验,别等传到服务器报错了再让用户重传,那是友尽的节奏。
import Compressor from 'compressorjs';
const compressAndUpload = (file) => {
if (!['image/jpeg', 'image/png'].includes(file.type)) {
message.error('只支持 jpg/png 格式');
return;
}
if (file.size > 10 * 1024 * 1024) {
message.error('图片不能超过 10MB');
return;
}
new Compressor(file, {
quality: 0.6,
maxWidth: 1920,
maxHeight: 1080,
success(result) {
const compressedFile = new File([result], file.name, {
type: result.type,
lastModified: Date.now()
});
uploadFile(compressedFile);
},
error(err) {
message.error('压缩失败');
}
});
};
遇到报错别只会重启
最常见就是下载下来文件打不开,99% 是 responseType 没设对,或者后端返回的不是流而是 JSON 报错信息,得先判断一下。
下载下来是乱码或打不开
axios.get('/api/download', { responseType: 'blob' })
const safeDownload = async () => {
const response = await axios.get('/api/download', {
responseType: 'blob',
transformResponse: []
});
const contentType = response.headers['content-type'];
if (contentType.includes('application/json')) {
const text = await response.data.text();
const error = JSON.parse(text);
throw new Error(error.message);
}
};
跨域问题
OPTIONS 预检请求要是挂了,文件根本发不出去,赶紧找后端查 CORS 配置。文件上传的 CORS 配置要特别注意:
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With
Access-Control-Expose-Headers: Content-Disposition
超时问题
大文件上传下载一定要把 timeout 调大点,或者干脆设成 0 无限等待(当然最好还是做进度条)。
const api = axios.create({
baseURL: '/api',
timeout: 10000
});
api.post('/upload', formData, {
timeout: 300000
});
api.get('/download/big-file', {
responseType: 'blob',
timeout: 0
});
几个让同事喊 666 的骚操作
1. 全局上传下载管理器
搞个全局的上传下载管理器,显示所有进行中的任务,暂停、取消功能安排上,逼格满满。
const DownloadManager = () => {
const [tasks, setTasks] = useState([]);
const addTask = (url, fileName) => {
const taskId = Date.now();
const newTask = {
id: taskId,
fileName,
progress: 0,
status: 'pending',
controller: new AbortController()
};
setTasks(prev => [...prev, newTask]);
axios({
url,
responseType: 'blob',
signal: newTask.controller.signal,
onDownloadProgress: (e) => {
const percent = Math.round((e.loaded * 100) / e.total);
updateTaskProgress(taskId, percent);
}
}).then(response => {
updateTaskStatus(taskId, 'completed');
}).catch(error => {
if (error.name === 'CanceledError') {
updateTaskStatus(taskId, 'cancelled');
} else {
updateTaskStatus(taskId, 'error');
}
});
return taskId;
};
const cancelTask = (taskId) => {
const task = tasks.find(t => t.id === taskId);
if (task && task.controller) {
task.controller.abort();
}
};
return (
<div className="download-manager">
{tasks.map(task => (
<div key={task.id} className="task-item">
<span>{task.fileName}</span>
<Progress percent={task.progress} />
{task.status === 'downloading' && (
<Button onClick={() => cancelTask(task.id)}>取消</Button>
)}
</div>
))}
</div>
);
};
2. 利用拦截器统一处理
axios.interceptors.request.use(config => {
if (config.data instanceof FormData) {
config.headers['X-Upload-Request'] = 'true';
config.timeout = 0;
}
return config;
});
axios.interceptors.response.use(response => {
if (response.config.responseType === 'blob') {
const disposition = response.headers['content-disposition'];
if (disposition) {
response.fileName = extractFileName(disposition);
}
}
return response;
});
3. 文件类型校验工具函数
const FILE_TYPES = {
image: ['image/jpeg', 'image/png', 'image/gif'],
excel: ['application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'],
word: ['application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document']
};
const validateFile = (file, type, maxSize = 10 * 1024 * 1024) => {
const errors = [];
if (!FILE_TYPES[type].includes(file.type)) {
errors.push(`文件类型错误,请上传${type}格式文件`);
}
if (file.size > maxSize) {
errors.push(`文件大小不能超过${maxSize / 1024 / 1024}MB`);
}
return {
valid: errors.length === 0,
errors
};
};
const { valid, errors } = validateFile(file, 'excel', 5 * 1024 * 1024);
if (!valid) {
message.error(errors.join(','));
}
总结
技术这东西,有时候真不是越新越好,Axios 这老家伙在文件处理上依然稳如老狗。别光收藏不吃灰,赶紧去项目里试两把,万一明天产品经理就提了这个需求呢?到时候你能淡定地说'这个简单,半天搞定',而不是'我研究研究',那逼格完全不一样。
要是真踩了坑也别骂街,毕竟咱们前端就是在填坑和造坑之间反复横跳,痛并快乐着嘛。行了,不多哔哔,你们要是试的过程中遇到啥奇葩问题,欢迎在群里吐槽,咱们一起研究。记住,能准时干饭的前端才是好前端,共勉!
相关免费在线工具
- Keycode 信息
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
- Escape 与 Native 编解码
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
- JavaScript / HTML 格式化
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
- JavaScript 压缩与混淆
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online