前端文件下载实战:从原理到最佳实践
引言
在现代 Web 应用开发中,文件下载是一个常见但容易出错的场景。本文将通过一个真实的订单导出功能案例,详细介绍前后端协作实现文件下载的完整方案,分析常见问题及解决方案,并提供经过生产验证的最佳实践。
前端文件下载涉及前后端协作,常见问题包括响应头访问错误、大文件内存溢出及文件名编码问题。解决方案包含后端流式响应改造、使用 SXSSFWorkbook 优化 Excel 生成、前端增强文件名解析及 Blob 处理。最佳实践涵盖浏览器兼容性处理、完善的错误捕获及安全控制措施,确保下载功能在生产环境稳定运行。

在现代 Web 应用开发中,文件下载是一个常见但容易出错的场景。本文将通过一个真实的订单导出功能案例,详细介绍前后端协作实现文件下载的完整方案,分析常见问题及解决方案,并提供经过生产验证的最佳实践。
我们需要实现一个订单数据导出功能,允许用户将查询结果下载为 Excel 文件。具体要求包括:
@ApiOperation(value = "下载订单列表", notes = "根据条件导出订单数据为 Excel 文件")
@PostMapping("/order-list/download")
public Result<?> downloadTaskOrderExcel(@RequestBody TaskDownLoadRequest taskDownLoadRequest, HttpServletRequest httpRequest) {
try {
// 获取用户 ID 并记录日志
Integer userId = getUserId(taskDownLoadRequest.getTaskId());
logDownloadStart(userId, taskDownLoadRequest.getTaskId());
// 查询订单数据
List<CustomerOrder> orders = queryOrders(taskDownLoadRequest.getTaskId());
if (orders.isEmpty()) {
return Result.error("没有找到符合条件的订单数据");
}
// 生成 Excel 文件
ByteArrayResource resource = generateExcel(orders);
// 构建响应数据
Map<String, Object> data = buildResponseData(resource);
return Result.ok(data);
} catch (Exception e) {
log.error("下载订单列表失败", e);
return Result.error(500, "下载订单数据失败");
}
}
const download = async (row) => {
const loading = ElLoading.service({ text: "正在下载..." });
try {
const response = await commonApi.taskOrderListDownload(
{ taskId: row.id },
{ responseType: "blob" }
);
// 文件名解析逻辑
let filename = "订单导出.xlsx";
const disposition = response.headers['content-disposition'];
if (disposition) {
const match = disposition.match(/filename="?([^"]+)"?/);
if (match) filename = decodeURIComponent(match[1]);
}
// 创建下载链接
const blob = new Blob([response.data], {
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
});
const link = document.createElement("a");
link.href = window.URL.createObjectURL(blob);
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
ElMessage.success("下载成功");
} catch (e) {
ElMessage.error("下载失败");
} finally {
loading.close();
}
};
Cannot read properties of undefined (reading 'content-disposition')@PostMapping("/order-list/download")
public void downloadTaskOrderExcel(@RequestBody TaskDownLoadRequest taskDownLoadRequest, HttpServletResponse response) throws IOException {
// 设置响应头
String filename = "订单导出_" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss")) + ".xlsx";
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename*=UTF-8''" + URLEncoder.encode(filename, "UTF-8").replace("+", "%20"));
// 流式生成 Excel
try (OutputStream out = response.getOutputStream()) {
orderService.generateExcelToStream(queryOrders(taskDownLoadRequest.getTaskId()), out);
}
}
public void generateExcelToStream(List<CustomerOrder> orders, OutputStream out) throws IOException {
try (Workbook workbook = new SXSSFWorkbook(100)) {
// 使用流式 Workbook
Sheet sheet = workbook.createSheet("订单数据");
// 创建标题行
String[] headers = {"订单 ID", "客户姓名", "运单号"};
Row headerRow = sheet.createRow(0);
for (int i = 0; i < headers.length; i++) {
headerRow.createCell(i).setCellValue(headers[i]);
}
// 填充数据
int rowNum = 1;
for (CustomerOrder order : orders) {
Row row = sheet.createRow(rowNum++);
row.createCell(0).setCellValue(order.getId());
// 其他字段...
}
workbook.write(out);
}
}
function getFilenameFromHeaders(headers) {
let filename = "订单导出_" + new Date().toISOString().slice(0, 10) + ".xlsx";
const disposition = headers['content-disposition'] || headers['Content-Disposition'];
if (!disposition) return filename;
// 支持 RFC 5987 编码
const utf8Match = disposition.match(/filename\*=UTF-8''([^;]+)/i);
if (utf8Match && utf8Match[1]) {
return decodeURIComponent(utf8Match[1]);
}
// 支持普通文件名
const filenameMatch = disposition.match(/filename="?([^"]+)"?/i);
if (filenameMatch && filenameMatch[1]) {
return filenameMatch[1].replace(/["']/g, '');
}
return filename;
}
const downloadFile = async (params, apiMethod, defaultFilename) => {
try {
const response = await apiMethod(params, {
responseType: 'blob',
headers: {'Accept': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'}
});
// 解析文件名
const filename = getFilenameFromHeaders(response.headers) || defaultFilename;
// 创建下载链接
const blob = new Blob([response.data], {
type: response.headers['content-type'] || 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
});
if (window.navigator.msSaveOrOpenBlob) {
// IE 专用方法
window.navigator.msSaveOrOpenBlob(blob, filename);
} else {
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename;
link.style.display = 'none';
document.body.appendChild(link);
link.click();
// 延迟清理
setTimeout(() => {
document.body.removeChild(link);
URL.revokeObjectURL(url);
}, 100);
}
return { success: true, filename };
} catch (error) {
// 尝试解析错误信息
if (error.response?.data instanceof Blob) {
try {
const errorText = await error.response.data.text();
const errorJson = JSON.parse(errorText);
throw new Error(errorJson.message || '下载失败');
} catch {
throw new Error('文件下载失败');
}
}
throw error;
}
};
try (Workbook workbook = new SXSSFWorkbook(100)) {
// 只保留 100 行在内存中
}
response.setHeader("Content-Disposition", "attachment; filename*=UTF-8''" + URLEncoder.encode(filename, "UTF-8"));
// IE 浏览器兼容
if (window.navigator.msSaveOrOpenBlob) {
window.navigator.msSaveOrOpenBlob(blob, filename);
} else {
// 标准浏览器实现
}
try {
// 下载逻辑
} catch (error) {
if (error.response?.status === 404) {
showError("文件不存在");
} else if (error.response?.status === 403) {
showError("无下载权限");
} else {
showError("下载失败:" + (error.message || "未知错误"));
}
}
const blob = new Blob([response.data], {
type: response.headers['content-type'] || 'application/octet-stream'
});
文件下载功能看似简单,实则涉及前后端多个技术点的紧密配合。本文通过实际案例详细分析了常见问题及其解决方案,提供了经过生产验证的实现方案。希望这些经验能帮助开发者避免常见陷阱,构建更健壮的文件下载功能。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 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