Axios 实现 Excel 文件上传与下载方案详解
使用 Axios 进行文件上传与下载的技术方案。内容包括 FormData 的使用、单文件及多文件上传、上传进度监听与防抖处理。重点讲解了 Blob 对象处理、responseType 配置、文件名解析(Content-Disposition)及通用下载函数封装。此外,还涵盖了跨域问题、超时设置、拦截器统一处理错误与 Token,以及报表异步导出、批量导入预览等实际应用场景的解决方案。

使用 Axios 进行文件上传与下载的技术方案。内容包括 FormData 的使用、单文件及多文件上传、上传进度监听与防抖处理。重点讲解了 Blob 对象处理、responseType 配置、文件名解析(Content-Disposition)及通用下载函数封装。此外,还涵盖了跨域问题、超时设置、拦截器统一处理错误与 Token,以及报表异步导出、批量导入预览等实际应用场景的解决方案。

前端在处理文件上传与下载时,常遇到二进制流处理、跨域、进度监听等问题。本文基于 Axios 库,详细讲解如何高效解决这些技术难点。
Blob (Binary Large Object)
浏览器用于存储二进制数据的容器,可视为内存中的虚拟文件。使用 window.URL.createObjectURL 可生成临时链接触发下载,使用后需调用 revokeObjectURL 释放内存。
FormData
HTML5 API,用于模拟表单提交。由于 JSON 无法直接传输文件,上传时需将文件放入 FormData 对象中。注意:不要手动设置 Content-Type: multipart/form-data,否则会导致 boundary 丢失,后端无法解析。
const uploadFile = async (file) => {
const formData = new FormData();
// 字段名需与后端约定
formData.append('file', file);
formData.append('userId', '9527');
formData.append('bizType', 'avatar');
try {
const response = await axios.post('/api/upload', formData,
// 不设置 Content-Type,让 Axios 自动处理
{ headers: { 'Content-Type': undefined } }
);
console.log('上传成功:', response.data);
return response.data;
} catch (error) {
console.error('上传失败:', error);
throw error;
}
};
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;
}
};
利用 onUploadProgress 回调监听 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);
if (onProgress) onProgress(percentCompleted);
},
timeout: 0 // 大文件建议不设超时或设较长时间
}
);
};
防止用户重复点击导致并发请求过多。
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 });
必须设置 responseType: 'blob',否则返回乱码字符串。
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 中提取文件名,支持中文编码。
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) {
// 尝试匹配 filename*=UTF-8''xxx (RFC 5987)
const utf8Match = contentDisposition.match(/filename\*=UTF-8''([^;]+)/i);
if (utf8Match && utf8Match[1]) {
fileName = decodeURIComponent(utf8Match[1]);
} else {
// 尝试普通 filename="xxx"
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);
link = .();
link. = url;
link. = fileName;
..(link);
link.();
..(link);
..(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';
..(link);
link.();
( {
..(link);
..(downloadUrl);
}, );
{ : , fileName };
} (error) {
.(, error);
error;
}
};
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);
};
// 请求拦截器
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);
});
当下载接口返回非 200 状态码时,可能返回 JSON 错误信息而非文件流。
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));
大数据量导出通常采用异步任务模式。
const exportReport = async (params) => {
// 1. 发起导出任务
const { data: { taskId } } = await axios.post('/api/export/task', params);
// 2. 轮询任务状态
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 (downloadUrl, {}, );
} (error) {
message.(error.);
}
};
先上传到预览接口,解析后展示错误行,确认无误后再正式入库。
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);
};
const confirmImport = async () => {
await axios.post('/api/import/confirm', { data: previewData });
message.success('导入成功');
};
return (
<div>
<Upload beforeUpload={handleUpload} fileList={[]}>
<Button>上传 Excel 预览</Button>
</Upload>
{/* 表格展示逻辑略 */}
</>
);
};
在上传前进行本地压缩和格式校验。
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 });
uploadFile(compressedFile);
},
error(err) {
message.error('压缩失败');
}
});
};
responseType: 'blob',以及后端是否返回了正确的流。Access-Control-Expose-Headers: Content-Disposition。timeout 或设为 0。X-Upload-Request 等自定义 Header 区分请求类型。Axios 配合 Blob 与 FormData 是前端处理文件交互的标准方案。通过合理配置响应类型、利用拦截器统一处理鉴权与错误、并结合实际业务场景(如异步导出、断点续传需求),可以构建稳定高效的文件服务模块。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online