【背景介绍】

项目中涉及到需要对导入的excel文件进行模板的下载,涉及到了下载的相关实现,那么我们话不多说,直接开讲:

【项目涉及到的技术栈】

Vue3+Arco-design

【开发过程中碰到的问题】

1.后端返回的数据是直接在response里面的

2.后端接收过来的数据一直显示是string类型,但转换成blob下载下来的文件又打不开

3.后端在postman中可以直接下载到文件,并且能够正常的打开;前端通过接口获取到的数据,在浏览器下载下来的文件大小与实际的文件大小不符,且无法正常的打开文件

总结下来,核心的问题就是打不开!打不开!打不开!!!!!!

 【关于产生原因的思考】

在浏览器、豆包、deepseek中查找了很多相关的帖子,总结下来基本就是:

1.要确认请求的接口对不对(都能下载到文件,证明接口肯定是对的啊)

2.要设置responseType;(可以很确定的说,我已经在请求的时候加上了)

3.要设置Content-Type,值要根据要下载的文档类型需要,选择对应的值

例如:vnd.openxmlformats-officedocument.spreadsheetml.sheet excel表格使用的类型

4.一堆杂七杂八的属性(防止xss攻击的啊、限制必须下载设置的type类型的啊等等,反正没有一个解决了我的问题)

后来又自己思考了一下可能造成这样情况的原因:

1.按照网上描述,接口返回数据不应该直接在response里面,而是在response.data里面,是否是拦截器有问题

2.接到数据之后对数据进行了转换,是否是在转换的途中出现了问题

【最终如何解决的】

在cursor中,让cursor解决了,并让它封装了一个下载的函数,可以看到有相应的注释

import { Message } from '@arco-design/web-vue';
import { getToken } from '@/utils/auth';

/**
 * 通用文件下载工具
 * 
 * 使用fetch API下载文件,支持各种文件类型
 * 自动处理身份验证、错误处理和资源清理
 */

/**
 * 下载配置接口
 */
interface DownloadConfig {
  /** 下载地址 */
  url: string;
  /** 文件名(包含扩展名) */
  filename: string;
  /** 请求方法,默认GET */
  method?: 'GET' | 'POST';
  /** 额外的请求头 */
  headers?: Record<string, string>;
  /** 请求体数据(POST请求时使用) */
  data?: any;
  /** 是否显示成功提示,默认true */
  showSuccessMessage?: boolean;
  /** 是否显示错误提示,默认true */
  showErrorMessage?: boolean;
}

/**
 * 使用fetch下载文件
 * 
 * @param config 下载配置
 * @returns Promise<void>
 */
export async function downloadFile(config: DownloadConfig): Promise<void> {
  const {
    url,
    filename,
    method = 'GET',
    headers = {},
    data,
    showSuccessMessage = true,
    showErrorMessage = true
  } = config;

  try {
    console.log(`开始下载文件: ${filename}`);

    // 1. 获取API基础地址和用户token
    const baseURL = import.meta.env.VITE_API_URL;
    const token = getToken();

    // 2. 准备请求配置
    const requestConfig: RequestInit = {
      method,
      headers: {
        // 默认请求头
        'Authorization': `Bearer ${token}`,
        'RSC': import.meta.env.VITE_CLIENT_RSC || '',
        // 用户自定义请求头
        ...headers
      }
    };

    // 3. 如果是POST请求且有数据,添加请求体
    if (method === 'POST' && data) {
      requestConfig.body = JSON.stringify(data);
      requestConfig.headers = {
        ...requestConfig.headers,
        'Content-Type': 'application/json'
      };
    }

    // 4. 发起下载请求
    const fullUrl = url.startsWith('http') ? url : `${baseURL}${url}`;
    const response = await fetch(fullUrl, requestConfig);

    // 5. 检查响应状态
    if (!response.ok) {
      throw new Error(`下载失败: HTTP ${response.status} ${response.statusText}`);
    }

    // 6. 将响应转换为Blob
    const blob = await response.blob();

    // 7. 验证文件数据
    if (blob.size === 0) {
      throw new Error('下载的文件为空');
    }

    console.log(`文件下载完成:`, {
      文件名: filename,
      文件大小: `${(blob.size / 1024).toFixed(2)} KB`,
      文件类型: blob.type || '未知类型'
    });

    // 8. 创建下载链接并触发下载
    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();
    document.body.removeChild(link);

    // 9. 清理临时URL,防止内存泄漏
    window.URL.revokeObjectURL(downloadUrl);

    // 10. 显示成功提示
    if (showSuccessMessage) {
      Message.success(`${filename} 下载成功`);
    }

  } catch (error: any) {
    // 错误处理
    console.error('文件下载失败:', error);
    
    if (showErrorMessage) {
      const errorMessage = error?.message || '未知错误';
      Message.error(`下载失败: ${errorMessage}`);
    }
    
    // 重新抛出错误,让调用方可以进一步处理
    throw error;
  }
}

/**
 * 下载Excel文件的便捷方法
 * 
 * @param url 下载地址
 * @param filename 文件名
 * @param data 请求数据(可选)
 */
export async function downloadExcel(
  url: string, 
  filename: string, 
  data?: any
): Promise<void> {
  return downloadFile({
    url,
    filename,
    method: data ? 'POST' : 'GET',
    data,
    headers: {
      'Accept': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
    }
  });
}

/**
 * 下载PDF文件的便捷方法
 */
export async function downloadPDF(
  url: string, 
  filename: string, 
  data?: any
): Promise<void> {
  return downloadFile({
    url,
    filename,
    method: data ? 'POST' : 'GET',
    data,
    headers: {
      'Accept': 'application/pdf'
    }
  });
} 
/**
 * 下载测试报告 - 使用GET请求
 * 
 * @param params 下载参数
 * @returns Promise<void>
 */
export const downloadReport = async (params) => {
  const { downloadExcel } = await import('@/utils/download');
  // 构建GET请求的URL参数
  const searchParams = new URLSearchParams({
    要传入的参数
  });
  
  return downloadExcel(
    `后端接口的url`,
    `文件名称.xlsx`,
  );
};

【鉴于有小伙伴不知道如何调用这个定义的接口,现补充调用接口相关的板块代码】

/**
 * 下载报告 - 使用GET请求
 * 
 * @param record 任务记录数据
 */
const handleDownload = async (record) => {
  try {
    // 调用API下载报告(GET请求)
    await downloadReport({
      要传入到接口的参数
    });
  } catch (error) {
    console.error('下载测试报告失败:', error);
    message.error('下载测试报告失败');
  }
};

总结:

以上就是我对该问题的解决,就我目前的理解来讲, 我认为应该是我之前传入给window.URL.createObjectURL函数的参数不对,之前传入的参数是blob开头,而正常传入的应该是携带有项目请求转接使用的请求头才对,写这篇文章的目的主要就是为了记录碰到的下载问题,以便后续如果碰到类似问题,可以解决,欢迎来评论探讨呀

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐