和 chatGPT 一起解决 excel 文件下载问题

最近有些迷上了各种人工智能类产品不能自拔,感慨之余,发现这个「全知全能神」确实可以解决一些问题, 比如我这个问题。

问题

我最近需要写一个从后端传输生成的电子表格文件到前端的功能,之前写过一个类似功能, 但是当时不清楚请求异常时如何传递错误信息给前端,于是全部粗暴处理为全部返回 403。 这次想优雅一点处理,返回具体的报错信息。

先预警一下,这个解决问题的过程其实花费了不少时间(两天),不过我会尽量控制篇幅, 如果你的问题很紧急,我建议你能找专家的情况下还是优先找专家咨询。

分析过程

首先我觉得以下内容其实并不重要,因为每次 chatGPT 告诉我们的可能都是不同内容, 如果您需要的是知道如何保存服务端发送的文件,可以直接翻到结论,我把过程列出来, 是想向大家分享我是如何使用 chatGPT 解决特定问题的。

返回报错信息

上面提到了,之前由于不知道如何返回错误信息,所有报错都处理为内容的 403, 其实主要就是不知道怎么把报错信息塞入 ResponseEntity, 之前我返回的对象是 ResponseEntity<Resource>,不过 ChatGPT 告诉我, 其实返回 ResponseEntity<byte[]> 就可以了。 那如果是这样,我们把报错信息的 ByteArray 传过去就行了。

现在第一个问题解决了,不过好戏才刚刚开始。

使用 js 接收文件

之前我是做了一个链接,设置好下载地址来进行下载的。这次我也想变一变, 于是我问 ChatGPT 如何用 jQuery 来实现下载,它的回答是这样的:

function downloadExcel() {
  // 创建 XMLHttpRequest 对象
  var xhr = new XMLHttpRequest();
  
  // 设置响应类型为二进制流
  xhr.responseType = 'blob';
  
  // 发送 GET 请求
  xhr.open('GET', '/downloadExcel');
  xhr.send();
  
  // 注册 onload 回调函数,当请求成功完成时调用
  xhr.onload = function() {
    // 如果响应状态码为 200
    if (xhr.status === 200) {
      // 创建一个新的 Blob 对象,并获取 URL
      var blob = new Blob([xhr.response], {type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'});
      var url = window.URL.createObjectURL(blob);
      
      // 创建一个链接元素,设置下载链接,点击链接下载文件
      var a = document.createElement('a');
      a.href = url;
      a.download = 'example.xlsx';
      document.body.appendChild(a);
      a.click();
      
      // 释放 URL 对象
      window.URL.revokeObjectURL(url);
    }
  };
}

很好懂的代码,也很清楚,于是我照做了,不过试了一下我发现, 接收到的文件大小要远大于实际文件的大小(大概是两倍)。于是我向 chatGPT 反馈, 接收到的文件比实际文件要大,chatGPT 认为是两个问题,首先,我可能没有正确的 blob 类型:

var blob = new Blob([byteArray], { type: 'application/octet-stream' });

或者是使用 fetchAPI 再试试:

function downloadExcel() {
  // 发送 GET 请求,获取工作簿文件
  fetch('/downloadExcel')
    .then(response => response.blob())
    .then(blob => {
      // 创建一个链接元素,设置下载链接,点击链接下载文件
      var a = document.createElement('a');
      a.href = URL.createObjectURL(blob);
      a.download = 'example.xlsx';
      document.body.appendChild(a);
      a.click();
    });
}

我觉得其实没什么变化,不过还是试了下,依然是相同问题,于是 chatGPT 开始怀疑服务端, 它建议我去改后端了:

@GetMapping("/downloadExcel")
public void downloadExcel(HttpServletResponse response) throws IOException {
    // 设置响应的头信息
    response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
    response.setHeader("Content-Disposition", "attachment; filename=example.xlsx");

    // 使用 StreamingResponseBody 发送文件流
    Workbook workbook = createWorkbook();
    StreamingResponseBody responseBody = outputStream -> {
        workbook.write(outputStream);
        workbook.close();
    };
    OutputStream out = response.getOutputStream();
    responseBody.writeTo(out);
    out.flush();
}

我觉得不靠谱,并没有采纳,于是我开始把后端生成的工作簿保存到本地,是可以打开的, 而从 blob 保存的文件,打开时就提示已损坏,而且文件也远大于正常文件, 那么我觉得,文件内容没有问题,但是前端收到的内容有问题,所以大概率是编码问题。

这其实也不好查,有可能是 SpringBoot 发送时做了转换,有可能是浏览器接收时做了转换, 那么我觉得首先我们可以观测下前端收到的内容,如果前端收到的内容正常, 那么我们基本可以撇清服务器的问题。浏览器的开发者模式有一个贴心的功能, 我们可以把网络请求复制为 curl 命令,然后我们只要把 curl 输出的内容保存到文件就好了。

打开 curl 保存的文件,我发现表格的大小和服务端大小相同,而且可以正常打开,这下可以确定, 后端没有问题,问题在前端,于是我继续问 chatGPT:「我试了一下,发送到客户端的数据没有问题, 但是前端生成的blob要比发送的数据要大」,结果 chatGPT 还是认为问题出在 MIME 类型上, 所以我们要想办法如何引导他换个思路。

我去试了试以前的做法,就是写一个超链接去下载文件,实际测试是没有问题的,然后我就又告诉了他: 「我又试了一下,因为这是一个 get 请求,所以如果我通过点击超链接下载就没有问题, 如何使用 js 达到相同的效果?」chatGPT 给了我一段 fetchAPI 的写法:

function downloadExcel() {
  fetch('/downloadExcel')
    .then(response => response.arrayBuffer())
    .then(byteArray => {
      var blob = new Blob([byteArray], {type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'});
      var url = URL.createObjectURL(blob);
      var a = document.createElement('a');
      a.href = url;
      a.download = 'example.xlsx';
      document.body.appendChild(a);
      a.click();
      document.body.removeChild(a);
      URL.revokeObjectURL(url);
    });
}

我没有去试,我比较习惯用 jQuery,于是我问它:「这个示例是否可以使用 jquery 重写?」 它也老实照办了:

function downloadExcel() {
  $.ajax({
    url: '/downloadExcel',
    method: 'GET',
    xhrFields: {
      responseType: 'arraybuffer'
    },
    success: function(response) {
      var blob = new Blob([response], {type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'});
      var url = URL.createObjectURL(blob);
      var a = $('<a>')
        .attr('href', url)
        .attr('download', 'example.xlsx')
        .appendTo('body');
      a[0].click();
      a.remove();
      URL.revokeObjectURL(url);
    }
  });
}

这下我们看到关键点了:responseType 要设置为 'arraybuffer'

终于,下载下来的文件可以正常打开了。

总结

我有两个方面需要总结,先试试 notion AI 能不能总结出来:

这篇文章描述了作者在使用 chatGPT 解决从后端传输生成的电子表格文件到前端的功能中遇到的问题和解决过程。 作者首先解决了如何返回报错信息的问题,然后尝试使用 jQuery 实现下载功能, 但是遇到了文件大小远大于实际文件的问题。经过 chatGPT 的指导, 作者最终使用了 responseType: 'arraybuffer' 来解决问题。 最终,作者总结了这个过程并提出了两个需要总结的方面。

看来还是需要我写一下:

如何使用 chatGPT 解决问题

chatGPT 并不是全知全能的神,它的答案是有问题的,但是不代表它的答案没有参考性, 期待一问一答就解决实际问题不太现实,我们需要验证它的答案,并把验证结果反馈给它, 这样我们才能更接近答案。

如果遇到它反复纠结于同一个点(本文中的 MIME 类型),我们需要给它补充一些信息,引导它跳出牛角尖。

简单说,我觉得如果要使用 chatGPT 解决现实问题,并不是问题 → 答案这么简单, 虽然我们期望是这样,在两者之间,我们需要反复验证和引导,才能得到我们想要的结果。

js 如何接收 SpringBoot 发送的文件

首先,SpringBoot 可以返回 ResponseEntity<byte[]> 类型的结果, 同时,前端接收文件时,需要设置 xhrresponseType'arraybuffer'