前端同学们,你们知道如何应对后端通过响应体返回文件流吗?

606 阅读4分钟

「这是我参与2022首次更文挑战的第7天,活动详情查看:2022首次更文挑战」。

今天给大家带来一个小知识,那就是分别从前端和后端同学的不同角度看他们是如何处理文件下载的,这个知识不光能有助于我们处理文件导出的需求,也能加深我们对HTTP协议的理解。

前言

今天说一个简单实用的小知识,在做一些管理系统时,我们往往会遇到导出Excel报表的需求,往往呢比较传统的方式就是后端借助POI工具包,或者是easyPOI,EasyExcel等更为简单,封装性更强的工具去做,使用这些工具往往就是将需要导出的文件以二进制流的方式通过HTTP请求的响应体传输。当然现在前端的功能也越来越强大,更为优雅的方式就是后端提供查询数据的接口,让前端同学去生成Excel文件,这样更能节约服务器资源,充分利用客户端的能力。

本篇文章主要是介绍传统的后端生成文件的形式,以及前端如何将后端返回的文件流进行下载。

后端出招

后端我们需要做的就是查询需要的数据,并且封装一个导出Excel文件的通用工具类,将一些导出信息传入工具类即可完成下载,本文我们以EasyExcel为例,这个工具很好用,导出的表头我们只需要通过注解配置即可。

下面便是我们需要封装的工具方法,我们只需要传入response、导出文件的文件名,数据,数据的DTO类,sheet的名称即可

这里我们需要关注几个点

  • response.setContentType("application/vnd.ms-excel");

    这个是指定网络文件的类型和网页的编码,决定浏览器将以什么形式、什么编码读取这个文件,这就是经常看到一些链接点击后是图片或者下载文件(每一种对应不同的文件扩展名))。

    • Content-Type: text/html; charset=utf-8: 网页格式(通过HTTP获取网页就是这种类型) .htm
    • Content-Type: application/json: JSON数据格式(我们经常写的前后端分离项目RestController的接口返回类型就是这个)
    • Content-Type: application/vnd.ms-excel: excel格式 .xls
    • Content-Type: application/pdf:pdf格式 .pdf
    • Content-Type: image/jpeg :jpg图片格式 .jpeg
    • ...
  • response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileNameEnCode + ".xlsx");

    Content-disposition 是 MIME 协议的扩展,MIME 协议指示 MIME 用户代理如何显示附加的文件。Content-disposition其实可以控制用户请求所得的内容存为一个文件的时候提供一个默认的文件名,文件直接在浏览器上显示或者在访问时弹出文件下载对话框。

    这里需要注意的是中文的文件名往往需要指定字符编码,防止乱码。

  • response.getOutputStream()

    以字节流输出,我们只需要将数据写入response.getOutputStream()的输出流中

    举例:response.getOutputStream().write("计算机".getBytes());

    /**
     * 导出 Excel
     * @param response  HttpServletResponse
     * @param fileName  文件名
     * @param list      数据
     * @param clazz   导出结构体
     * @param sheetName 导入文件的 sheet 名
     */
    public static <T> void writeExcel(HttpServletResponse response, String fileName, List<T> list, Class<T> clazz, String sheetName) throws IOException {
        response.setContentType("application/vnd.ms-excel");
        response.setCharacterEncoding("utf-8");
        // 这里URLEncoder.encode可以防止中文乱码 当然和easyexcel没有关系
        String fileNameEnCode = URLEncoder.encode(fileName, "UTF-8").replaceAll("\+", "+");
        response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileNameEnCode + ".xlsx");
        writeExcel(response.getOutputStream(), list, clazz, sheetName);
    }

后端我们只需要做这些操作即可,这样我们就可以通过HTTP向前端返回一个标准的携带文件的响应.

前端接招

我们前端同学在接收到这样请求,该如何去处理呢?

如果没有做过文件下载的前端同事可能一脸懵,怀疑后端给返回的这是什么鬼,满屏乱码,离谱

如果一个深入研究过HTTP的前端同学可能会对上述请求头的参数有所了解

通过HTTP的Content-Type和Content-disposition看出端倪,知道后端返回的这是一个Excel文件

那前端该如何处理呢?

上代码

export const download = (data) => {
  return request({
    url:'请求地址',
    method: 'post',
    responseType:'blob',
    data: data
  })
}
​
// 下载处理逻辑
const filename = res.headers["content-disposition"];
const blob = new Blob([res.data]);
var downloadElement = document.createElement("a");
// URL.createObjectURL()方法会根据传入的参数创建一个指向该参数对象的URL. 这个URL的生命仅存在于它被创建的这个文档里. 新的对象URL指向执行的File对象或者是Blob对象.
var href = window.URL.createObjectURL(blob);
downloadElement.href = href;
downloadElement.download = decodeURIComponent(filename.split("filename=")[1]);
document.body.appendChild(downloadElement);
downloadElement.click();
document.body.removeChild(downloadElement);
// URL.revokeObjectURL()方法会释放一个通过URL.createObjectURL()创建的对象URL. 当你要已经用过了这个对象URL,然后要让浏览器知道这个URL已经不再需要指向对应的文件的时候,就需要调用这个方法.
window.URL.revokeObjectURL(href);  

前端需要做如下几步:

  • 指定响应体类型 responseType:'blob'
  • 获取后端定义的文件名const filename = res.headers["content-disposition"];
  • 构建一个a标签
  • 创建指向blob的链接
  • 将文件名,链接设置到a标签上
  • 将a标签添加到document上
  • 调用a标签点击事件
  • document删除a节点
  • 释放href指向blob