Java+vue.js前后端文件下载怎么实现?

Java前后端文件下载怎么实现?

后端,所有的文件,无论MP3,PDF,Excel,png等都是二进制文件,二进制文件在Java中是字节流对象,而字节流对象可以通过输入输出流读写操作,所以文件直接生成二进制对象,通过输出流写出这个对象即可。

同时在响应设置参数,告诉前端是什么文件,怎么下载,是什么编码才匹配不会乱码。

前端,拿到"响应"解析,根据响应设置,打开一个链接让浏览器执行"下载功能"。

整个流程打个比方:打个比方就是按照响应里面的说明书,把二进制文件(bolo)重新组装。

打个比方:你在网上下单买个大衣柜(发送下载请求),商家打包发货(将文件对象用OutputStream打包发货给你),快递运输(response.getWriter().write响应写出去),拿到货打开快递,阅读说明书(前端读取响应对象配置说明),根据说明书组装成衣柜(根据设置说明,将二进制文件(bolo)解析还原成文件)。这就是文件下载。

开发业务场景:功能--->批量导入用户消息。我们在学校做个图书管理系统,那么现在学校要分配账号给学生,不可能一个个设置,那么让学生自己填学号,姓名,班级等信息,然后取部分字段作为账号,初始设置密码,直接导入系统生成账号密码,后边学生就不用手动注册账号密码,而是由学校分配,学校不可能自己一个个设置。所以这样的业务场景就是用一个excel表格导入系统,系统遍历excel然后转化成对象,插入数据库。

后端代码:(EasyExcel是一个工具,专门处理Excel表格的),依赖如下。

xml 复制代码
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.3.2</version>
</dependency>
java 复制代码
@GetMapping("/downloadTemplate")
    public void downloadTemplate(HttpServletResponse response) throws IOException {
        // 必须设置跨域放行响应头
        response.setHeader("Access-Control-Expose-Headers", "Content-Disposition");
        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
        response.setCharacterEncoding("utf-8");
        String fileName = URLEncoder.encode("userImportTemplate.xlsx", StandardCharsets.UTF_8);
        response.setHeader("Content-Disposition", "attachment; filename=" + fileName);

        List<UserImportDto> dataList = new ArrayList<>();

        // 示例数据
        UserImportDto example1 = new UserImportDto();
        example1.setUserName("zhang san");
        example1.setRealName("张三");
        example1.setRole("学生");
        example1.setClassName("计算机科学与技术1班");
        dataList.add(example1);

        UserImportDto example2 = new UserImportDto();
        example2.setUserName("lisi");
        example2.setRealName("李四");
        example2.setRole("教师");
        example2.setClassName("软件工程系");
        dataList.add(example2);
        
        EasyExcel.write(response.getOutputStream(), UserImportDto.class)
                .sheet("用户导入模板")
                .doWrite(dataList);
    }

浏览器默认暴露的响应头(白名单)

浏览器跨域请求时,默认只暴露这 6 个响应头:

txt 复制代码
Cache-Control
Content-Language
Content-Type
Expires
Last-Modified
Pragma

其他的都不给前端拿!

  • Content-Disposition 不在白名单里
  • 浏览器拿到了,但不让你前端读取
  • 所以 response.headers['content-disposition']undefined,其他我们设置的,不在白名单的浏览器就无法按我们设置的解析,就拿不到对应的数据,比如文件名称拿不到。类似于,没有完整说明书,组装出来的衣柜不完整。

用大白话给你解释:

1. response.setContentType()

java 复制代码
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");

翻译:告诉浏览器 "我发给你的是一个 Excel 文件"

参数 含义
application/ 这是一个"应用程序文件",不是网页(text/html)
vnd.openxmlformats-officedocument.spreadsheetml.sheet 这是 .xlsx 格式的 Excel 文件

常见对照:response.setContentType可以设置的参数对照

java 复制代码
// Excel 2007+ (.xlsx)
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"

// Excel 2003 (.xls)
"application/vnd.ms-excel"

// PDF
"application/pdf"

// 普通文本
"text/plain"

// Word (.docx)
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"

// 图片
"image/jpeg"
"image/png"

// ZIP 压缩包
"application/zip"

浏览器收到后会怎么做?

  • 看到 application/pdf → 知道是 PDF → 要么下载,要么在浏览器打开
  • 看到 text/html → 知道是网页 → 直接渲染

2. response.setCharacterEncoding()

java 复制代码
response.setCharacterEncoding("utf-8");

翻译:用 UTF-8 编码来传数据

什么意思?

复制代码
中文不乱码 → 依赖这个
编码 效果
UTF-8 支持中文,全球通用 ✅
GBK 支持中文,但国外软件可能乱码
ISO-8859-1 不支持中文,全是乱码 ❌

3. response.setHeader("Content-Disposition", ...)

java 复制代码
response.setHeader("Content-Disposition", "attachment; filename=" + fileName);

翻译:告诉浏览器 "这个文件要下载,文件名是 xxx"

部分 含义
attachment 下载,不要在浏览器打开
filename=xxx.xlsx 保存时的默认文件名

对比

java 复制代码
// 下载(弹窗保存)
"attachment; filename=文件.xlsx"
// 浏览器:弹出下载框,默认文件名是"文件.xlsx"

// 直接打开(浏览器内预览)
"inline; filename=文件.pdf"
// 浏览器:直接在浏览器打开(PDF 会预览)

完整流程大白话

java 复制代码
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
// ① "喂浏览器,我发的是 Excel 文件"

response.setCharacterEncoding("utf-8");
// ② "我用 UTF-8 编码,别乱码"

response.setHeader("Content-Disposition", "attachment; filename=用户导入模板.xlsx");
// ③ "你要下载保存,名字叫'用户导入模板.xlsx'"

response.getOutputStream().write(excelData);
// ④ "给你文件内容(二进制数据)"

浏览器收到后

  1. 看到 Content-Type → 知道是 Excel
  2. 看到 Content-Disposition → 知道要下载
  3. 看到文件名 → 用这个名字保存
  4. 收到数据 → 生成文件

前端收到的是什么?

前端用 responseType: 'blob' 接收:

javascript 复制代码
const blob = response.data
// blob 就是文件的二进制数据(0101010101...)
// 里面包含了 Excel 的所有内容

const url = URL.createObjectURL(blob)
// 把二进制数据变成一个浏览器能访问的临时地址

a.href = url
a.download = filename
// 告诉浏览器:下载这个地址的内容,文件名是 xxx
a.click()
// 触发下载

刚刚后端响应设置的了Disposition大写 ,前端设置小写影响吗?const contentDisposition = response.headers?.'content-disposition'

不影响! HTTP 响应头是大小写不敏感的。

前端代码:

javascript 复制代码
export const downloadBatchTemplate = (url, params = {}, method = 'get') => {
    // 这个request是请求对象,请求后会回调响应,她两是一对的,所以.then(reponse)其中的response就是返回的响应对象--->打包的快递
  return request({
    url,
    method,
    params,
    responseType: 'blob'
  }).then((response) => { // 这里接收到的是完整的 response 对象

    const blob = response.data // 从 response 中取出 blob 文件
    console.log('所有响应头:', response.headers)
    const link = document.createElement('a')	// 创建a标签
    const blobUrl = URL.createObjectURL(blob)	// 创建打开的浏览器路径
    link.href = blobUrl

    // 从 response.headers 中获取文件名
    const contentDisposition = response.headers?.['content-disposition']
    let filename = 'importBatchTemplate.xlsx' // 默认文件名,如果读取响应头失败拿不到文件名给个默认的
    if (contentDisposition) {
      const match = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/)
      if (match && match[1]) {
        filename = decodeURIComponent(match[1].replace(/['"]/g, '')) // 去掉引号,解码 URL 编码(防止中文乱码)
      }
    }
    link.download = filename // 设置下载文件名
    document.body.appendChild(link) // 兼容 Firefox
    link.click()	// 模拟人为点击链接
    document.body.removeChild(link) // 清理 DOM
    URL.revokeObjectURL(blobUrl) // 释放内存,防止内存泄漏
  })
}

总结

代码 英文 中文意思
setContentType Content Type 告诉浏览器"这是啥类型文件"
setCharacterEncoding Character Encoding 用啥编码,防止乱码
setHeader("Content-Disposition") Content Disposition 告诉浏览器"下载还是预览"

简单说就是:告诉浏览器这是什么文件,叫什么名字,要下载还是打开。

当然你可以弄一个通用的下载方法然后导出去,调用的时候只需要传入请求方法和地址即可。