vue+springboot文件流方式下载的Excel文档

工作中常会遇到导入导出功能。最近遇到vue+springboot文件流方式下载的Excel文档打不开,提示文件格式不正确的问题。解决这个问题花了我很长时间和很多精力。在这记录一下问题和解决方法,在以后工作中再有这类问题,可作为参考。

1.首先先写后台代码
新建工程 File->New->Project -> spring initializer 勾选web依赖 -> Next -> Finish。

再添加poi依赖。

pom.xml


4.0.0

org.springframework.boot
spring-boot-starter-parent
2.3.0.RELEASE


com.szile.excel
export-excel
0.0.1-SNAPSHOT
export-excel
Demo project for Spring Boot

<properties>
    <java.version>1.8</java.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- https://mvnrepository.com/artifact/org.apache.poi/poi -->
    <dependency>
        <groupId>org.apache.poi</groupId>
        <artifactId>poi</artifactId>
        <version>3.17</version>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
        <exclusions>
            <exclusion>
                <groupId>org.junit.vintage</groupId>
                <artifactId>junit-vintage-engine</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
</dependencies>

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>
在resources下的templates目录下放了一个文件名为 “测试.xlsx”文件和“测试.zip”文件。

写接口 ExportFileController

@CrossOrigin // 解决跨域问题
@RestController
public class ExportFileController {

private static final Logger logger = LoggerFactory.getLogger(ExportFileController.class);

@GetMapping("download")
public ResponseEntity<byte[]> exportExcel() throws IOException {
    logger.info("download start");
    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
    ClassPathResource pathResource = new ClassPathResource("templates/测试.xlsx");
    HttpHeaders httpHeaders = new HttpHeaders();
    InputStream inputStream = pathResource.getInputStream();
    String filename = Optional.ofNullable(pathResource.getFilename()).orElse("Template.xlsx");
    IOUtils.copy(inputStream, outputStream);
    filename = URLEncoder.encode(filename, "UTF-8"); // 解决文件名乱码问题,对文件名编码,前端拿到文件名后解码
    // 使前端可以通过response.headers['Content-Disposition']获取到文件名称
    httpHeaders.set("Access-Control-Expose-Headers", "Content-Disposition");
    httpHeaders.setContentDispositionFormData("filename", filename);
    // 经过实验验证设置ContentType不起作用
    // final MediaType mediaType = MediaType
    //       .parseMediaType("application/force-download; charset=UTF-8");
    // httpHeaders.setContentType(mediaType);
    logger.info("done");
    return new ResponseEntity<>(outputStream.toByteArray(), httpHeaders, HttpStatus.OK);
}

@GetMapping("download_2")
public void exportExcel2(HttpServletResponse response) throws IOException {
    logger.info("download_2 start");
    ClassPathResource pathResource = new ClassPathResource("templates/测试.zip");
    InputStream inputStream = pathResource.getInputStream();
    String filename = Optional.ofNullable(pathResource.getFilename()).orElse("Template.xlsx");
    filename = URLEncoder.encode(filename, "UTF-8"); // 解决文件名乱码问题,对文件名编码,前端拿到文件名后解码
    response.setHeader("Content-Disposition", "attachment; filename=".concat(filename));
    // 使前端可以通过response.headers['Content-Disposition']获取到文件名称
    response.setHeader("Access-Control-Expose-Headers", "Content-Disposition");
    // 经过实验验证设置ContentType不起作用
    // response.setContentType("application/octet-stream; charset=UTF-8");
    ServletOutputStream outputStream = response.getOutputStream();
    IOUtils.copy(inputStream, outputStream);
    logger.info("done");
}

}
这里用了两种不同的实现方式。

启动项目,通过浏览器直接发送请求和Postman(选择Send and Download方式)都顺利下载文件。浏览器下载下来文件名称正确,Postman下载下来的文件名乱码。因为浏览器会对文件名进行decodeURI解码,而Postman不会。

2.前端Vue项目
在一给个空文件夹中,打开terminal  输入 vue init webpack,回车

? Generate project in current directory? Yes
? Project name test-demo
? Project description A Vue.js project
? Author szile
? Vue build standalone
? Install vue-router? No
? Use ESLint to lint your code? No
? Set up unit tests No
? Setup e2e tests with Nightwatch? No
? Should we run npm install for you after the project has been created? (recommended) npm

这样就新建一个Vue项目。

因为只是一个demo,为了简单对HelloWorld.vue进行修改,加入两个button,点击触发两个方法。

HelloWorld.vue

npm install axios  安装axios依赖 ,封装请求。

request.js

import axios from 'axios'
import {resolveFileName, convertBlobToFile} from './utils/util'
export const httpRequsetBlob = axios.create({
  baseURL: '',
  timeout: 1000
})
/**
 * 获取文件流的请求
 */
httpRequsetBlob.interceptors.request.use(function (config) {
  // Do something before request is sent
  config.responseType = 'blob' // 设置相应类型为 blob
  return config
}, function (error) {
  // Do something with request error
  return Promise.reject(error)
})
// Add a response interceptor
httpRequsetBlob.interceptors.response.use(function (response) {
  // Do something with response data
  if (response.status === 200) {
    // 需要后端设置 response.setHeader("Access-Control-Expose-Headers", "Content-Disposition")
    const contentDisposition = response.headers['content-disposition']
    console.log(contentDisposition)
    const filename = resolveFileName(contentDisposition)
    // 将文件流转为文件下载
    convertBlobToFile(response.data, filename)
    return response
  }
}, function (error) {
  // Do something with response error
  return Promise.reject(error)
})
util.js 封装工具方法

/**
 * String对象添加replaceAll方法
 /
/
 eslint-disable /
String.prototype.replaceAll = function (searchStr, replaceStr) {
  return this.replace(new RegExp(searchStr, 'g'), replaceStr)
}
/
*
 * 从Content-Disposition中获取下载文件名
 * @param {} contentDisposition response.headers['content-disposition']的值
 *
 * contentDisposition = form-data; name="filename"; filename="%E6%B5%8B%E8%AF%95.xlsx"
 * contentDisposition = attachment; filename=%E6%B5%8B%E8%AF%95.zip
 /
export const resolveFileName = function(contentDisposition) {
  /
*
   * 获取headers content-disposition中的文件名称
   * 需要后端设置 response.setHeader("Access-Control-Expose-Headers", "Content-Disposition"),否则无法获取
   /
  if (contentDisposition) {
    const contents = contentDisposition.replaceAll('"', '').split(';').map(item => { return item.trim() })
    if (contents.indexOf('attachment') !== -1 || contents.indexOf('form-data') !== -1) {
      let filename
      contents.forEach(item => {
        const values = item.split('=')
        if (values.length > 1 && values[0] === 'filename') {
          filename = decodeURI(values[1]) // 对文件名decodeURI解码,解决中文名乱码问题
        }
      })
      return filename
    }
  }
  return
}
/
*
 * 将文件流转为文件下载
 * @param {
} data 文件流
 * @param {*} filename 文件名
 */
export const convertBlobToFile = function (data, filename) {
  const blob = new Blob([data]) // 无需指定 type
  const downloadElement = document.createElement('a')
  const href = window.URL.createObjectURL(blob) // 创建下载的链接
  downloadElement.href = href
  downloadElement.download = filename // 下载后文件名
  document.body.appendChild(downloadElement)
  downloadElement.click() // 点击下载
  document.body.removeChild(downloadElement) // 下载完成移除元素
  window.URL.revokeObjectURL(href) // 释放掉blob对象
}

源码连接:https://gitee.com/szile/VueSpringBootExportFileDemo.git

posted @ 2021-12-27 12:37  自北徂南  阅读(1314)  评论(0编辑  收藏  举报