工作中常會遇到導入導出功能。最近遇到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
