前端文件下载与错误处理实战指南

一、背景

在前端开发中,文件下载是一个非常常见的功能需求,比如导出 Excel、下载 PDF 等。通常我们会使用 axiosfetch 设置 responseType: 'blob' 来处理文件流。但实际开发中,我们常常会遇到一个棘手的问题:

当后端返回错误信息时(如参数校验失败),前端无法正确解析和展示错误提示。

典型场景

以最近开发的「导出诊疗项目明细」功能为例:

javascript 复制代码
// API 定义
export const exportTreatmentList = (search) =>
  request({
    url: "/report/costItem/export",
    method: "post",
    data: search,
    responseType: "blob",  // 设置为 blob
  });

调用时:

javascript 复制代码
exportTreatmentList(params).then((res) => {
  console.log(res);  // Blob {size: 56, type: 'application/json'}
  // 无法通过 res.code 判断错误,因为 res 不是预期的 JSON 对象
});

问题 :后端返回了 { code: 12000, msg: "请选择起始日期" },但前端收到的却是 Blob 对象,导致错误信息无法被正确处理。


二、问题分析

2.1 为什么会出现这个问题?

当设置 responseType: 'blob' 后,无论后端返回的是什么内容,浏览器都会将其包装为 Blob 对象:

后端返回 前端收到的 Blob
Excel 文件流 Blob { type: 'application/octet-stream' }
JSON 错误信息 Blob { type: 'application/json' }
纯文本错误信息 Blob { type: 'text/plain' }

2.2 解决方案

通过 判断 Blob 的 type 属性 来区分成功还是失败:

  • type === 'application/json' → 后端返回了错误信息
  • 其他类型(如 application/octet-stream)→ 正常的文件流

三、解决方案实现

3.1 基础实现

javascript 复制代码
exportExcel() {
  this.loadingExcel = true;
  exportTreatmentList(params)
    .then((res) => {
      this.loadingExcel = false;
      
      // 通过 Blob.type 判断返回内容类型
      if (res.type === "application/json") {
        // 错误信息:用 FileReader 读取文本内容
        const reader = new FileReader();
        reader.onload = (e) => {
          try {
            const result = JSON.parse(e.target.result);
            this.$message.error(result.msg || "导出失败");
          } catch (err) {
            this.$message.error("导出失败");
          }
        };
        reader.readAsText(res);
      } else {
        // 正常文件流:触发下载
        const blob = new Blob([res], {
          type: "application/octet-stream",
        });
        const url = window.URL.createObjectURL(blob);
        const link = document.createElement("a");
        link.href = url;
        link.setAttribute("download", "诊疗项目明细.xlsx");
        document.body.appendChild(link);
        link.click();
        document.body.removeChild(link);
      }
    })
    .catch((error) => {
      this.loadingExcel = false;
      this.$message.error("导出失败");
    });
}

3.2 核心流程

复制代码
接口返回 → 判断 Blob.type
  ├── 'application/json' → FileReader 读取 → JSON.parse → 显示错误
  └── 其他类型 → 触发文件下载

四、拓展延伸

4.1 通用文件下载工具函数

在多个页面都有导出需求时,可以封装一个通用工具函数:

javascript 复制代码
// utils/download.js

/**
 * 通用文件下载方法
 * @param {Promise} requestPromise - 返回 Blob 的请求 Promise
 * @param {string} filename - 下载文件名
 * @param {function} onError - 错误回调
 */
export function downloadFile(requestPromise, filename = "download.xlsx", onError) {
  return requestPromise.then((res) => {
    // 判断是否为 JSON 错误信息
    if (res.type === "application/json") {
      return new Promise((resolve, reject) => {
        const reader = new FileReader();
        reader.onload = (e) => {
          try {
            const error = JSON.parse(e.target.result);
            if (onError) {
              onError(error);
            } else {
              ElMessage.error(error.msg || "下载失败");
            }
            reject(error);
          } catch (err) {
            ElMessage.error("下载失败");
            reject(err);
          }
        };
        reader.readAsText(res);
      });
    }

    // 正常下载
    const url = window.URL.createObjectURL(new Blob([res]));
    const link = document.createElement("a");
    link.href = url;
    link.setAttribute("download", filename);
    document.body.appendChild(link);
    link.click();
    document.body.removeChild(link);
    window.URL.revokeObjectURL(url);
  });
}

使用示例

javascript 复制代码
import { downloadFile } from "@/utils/download";
import { exportTreatmentList } from "@/api/toll/treatmentList";

exportExcel() {
  const params = { /* ... */ };
  downloadFile(
    exportTreatmentList(params),
    "诊疗项目明细.xlsx"
  ).catch(() => {
    // 错误已在 downloadFile 内部处理
  });
}

4.2 支持自定义错误处理

javascript 复制代码
// 在 downloadFile 基础上支持自定义错误处理
export function downloadFile(config) {
  const { request, filename, onSuccess, onError } = config;
  
  return request.then((res) => {
    if (res.type === "application/json") {
      const reader = new FileReader();
      reader.onload = (e) => {
        try {
          const error = JSON.parse(e.target.result);
          if (onError) {
            onError(error);
          }
        } catch {
          ElMessage.error("下载失败");
        }
      };
      reader.readAsText(res);
      return;
    }

    // 成功下载
    if (onSuccess) {
      onSuccess(res);
    }
    
    // 触发下载
    const url = window.URL.createObjectURL(new Blob([res]));
    const link = document.createElement("a");
    link.href = url;
    link.download = filename;
    link.click();
    window.URL.revokeObjectURL(url);
  });
}

4.3 进度条支持

对于大文件下载,可以显示进度条:

javascript 复制代码
import axios from "axios";

export function downloadWithProgress(url, filename, onProgress) {
  return axios({
    url,
    method: "get",
    responseType: "blob",
    onDownloadProgress: (progressEvent) => {
      if (onProgress && progressEvent.total) {
        const percentCompleted = Math.round(
          (progressEvent.loaded * 100) / progressEvent.total
        );
        onProgress(percentCompleted);
      }
    },
  }).then((res) => {
    // 处理下载...
  });
}

4.4 大文件分片下载

对于超大文件,可以考虑分片下载:

javascript 复制代码
async function downloadInChunks(url, filename, chunkSize = 5 * 1024 * 1024) {
  // 1. 获取文件大小
  const headResponse = await fetch(url, { method: "HEAD" });
  const fileSize = parseInt(headResponse.headers.get("Content-Length"));
  
  // 2. 分片下载
  const chunks = [];
  let offset = 0;
  
  while (offset < fileSize) {
    const end = Math.min(offset + chunkSize, fileSize - 1);
    const response = await fetch(url, {
      headers: { Range: `bytes=${offset}-${end}` }
    });
    const blob = await response.blob();
    chunks.push(blob);
    offset = end + 1;
  }
  
  // 3. 合并分片
  const finalBlob = new Blob(chunks, { type: "application/octet-stream" });
  
  // 4. 触发下载
  const link = document.createElement("a");
  link.href = URL.createObjectURL(finalBlob);
  link.download = filename;
  link.click();
}

五、相关技术点

5.1 Blob 对象

Blob(Binary Large Object)是表示二进制数据的对象:

javascript 复制代码
const blob = new Blob([data], { type: "application/octet-stream" });

// Blob 属性
blob.size      // 大小(字节)
blob.type      // MIME 类型

常见 MIME 类型

类型 说明
application/octet-stream 二进制文件流
application/json JSON 数据
text/plain 纯文本
text/html HTML 文档
image/png PNG 图片
application/pdf PDF 文件

5.2 FileReader 对象

FileReader 用于读取 Blob 或 File 对象的内容:

javascript 复制代码
const reader = new FileReader();

reader.onload = (e) => {
  console.log(e.target.result);  // 读取结果
};

// 读取方式
reader.readAsText(blob);        // 读取为文本
reader.readAsDataURL(blob);     // 读取为 DataURL (base64)
reader.readAsArrayBuffer(blob); // 读取为 ArrayBuffer

5.3 URL.createObjectURL

创建临时的 Blob URL:

javascript 复制代码
const url = URL.createObjectURL(blob);
// 使用 url...
URL.revokeObjectURL(url);  // 释放内存

注意 :使用后需要调用 revokeObjectURL 释放内存。


六、最佳实践

6.1 统一错误处理

javascript 复制代码
// 在 axios 拦截器中统一处理
axios.interceptors.response.use(
  (response) => {
    if (response.config.responseType === "blob") {
      return response.data;  // 返回 Blob
    }
    return response.data;
  },
  (error) => {
    // 处理网络错误
    return Promise.reject(error);
  }
);

6.2 类型判断工具函数

javascript 复制代码
/**
 * 判断 Blob 是否为 JSON 错误
 */
export function isJsonError(blob) {
  return blob.type === "application/json";
}

/**
 * 解析 JSON 错误信息
 */
export function parseBlobError(blob) {
  return new Promise((resolve) => {
    const reader = new FileReader();
    reader.onload = (e) => {
      try {
        resolve(JSON.parse(e.target.result));
      } catch {
        resolve({ msg: "未知错误" });
      }
    };
    reader.readAsText(blob);
  });
}

6.3 完整示例

javascript 复制代码
async function handleExport(requestPromise, filename) {
  try {
    const res = await requestPromise;
    
    if (isJsonError(res)) {
      const error = await parseBlobError(res);
      ElMessage.error(error.msg || "导出失败");
      return;
    }
    
    // 触发下载
    const url = URL.createObjectURL(res);
    const link = document.createElement("a");
    link.href = url;
    link.download = filename;
    link.click();
    URL.revokeObjectURL(url);
    
    ElMessage.success("导出成功");
  } catch (err) {
    ElMessage.error("网络错误,请重试");
  }
}

七、总结

核心要点

  1. 问题根源responseType: 'blob' 会将所有响应包装为 Blob
  2. 判断方法 :通过 Blob.type 区分成功/失败
  3. 错误解析 :使用 FileReader 读取 Blob 内容
  4. 通用封装:抽离工具函数,统一处理逻辑

注意事项

  • ✅ 使用后及时调用 URL.revokeObjectURL() 释放内存
  • ✅ 处理 FileReader 的异步读取
  • ✅ 兼容不同 Babel 版本(catch (err) 而非 catch {}
  • ✅ 考虑大文件下载进度显示
  • ✅ 支持多种文件格式(Excel、PDF、图片等)

希望这篇文章能帮助你更好地处理前端文件下载和错误处理的问题!

相关推荐
Yao8061 小时前
MinIO自建对象存储,省OSS费用的完整方案
前端·后端
2501_933923252 小时前
设计网页的时候加载不出来怎么办?认识常见的状态码
前端·spring
only-lucky2 小时前
QML深入学习五(Controls模块)
前端·javascript·学习
程序员黑豆2 小时前
鸿蒙应用开发之路由:Router 页面路由使用教程
前端·harmonyos
gyx_这个杀手不太冷静2 小时前
Agent开发进阶指南(第 2 章):Agent 运行全流程拆解、上下文窗口、流式输出、记忆系统与 Function Call 实战
前端·架构·agent
anyup2 小时前
像这种问题千万别自己动手,否则你可太看不起 AI 了
前端·架构·trae
hunterandroid3 小时前
[鸿蒙从零到一] HarmonyOS Web 组件与 JSBridge 通信实战:从页面加载到安全协议
前端
xingren3 小时前
「拍摄器」UI 特效 - 在 Winform/WPF/WinUI3/Avalonia/Web 的实现
前端
leslie1183 小时前
npm常用命令
前端·npm