工作中常会遇到导入导出功能。最近遇到vue+springboot文件流方式下载的Excel文档打不开,提示文件格式不正确的问题。解决这个问题花了我很长时间和很多精力。在这记录一下问题和解决方法,在以后工作中再有这类问题,可作为参考。
1.首先先写后台代码
新建工程 File->New->Project -> spring initializer 勾选web依赖 -> Next -> Finish。
再添加poi依赖。
pom.xml
<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