背景
基於SpringBoot+Vue先後端分離項目中進行文件下載
SpringBoot版本:2.0.3.RELEASE
vue版本:2.5.2前端
本博客中前端實現文件下載的方式有3種方式以下:vue
經過a連接下載(須要繞過安全校驗框架的token驗證);
axios+Blob發送post請求實現下載(通過安全校驗框架的登陸或者token驗證,可是下載複雜類型文件異常,盡能夠支持.txt或者csv文件);
XMLHttpRequest+Blob發送post請求實現下載(通過安全校驗框架的登陸或者token驗證,能夠下載全部的文件)。
1.後臺
1.1後臺下載文件的工具類
該工具類型的主要處理邏輯是設置響應頭屬性,將待下載文件轉換爲字節流寫入HttpServletResponse 實例中。java
Content-type:說明了實體主體內對象的媒體類型;不一樣文件對應的value ,而下面代碼中使用的application/octet-stream則表示以流方式下載文件,能夠匹配全部類型的文件。
Content-Disposition:下載文件的一個標識字段,filename屬性值是下載獲得的文件的文件名;
Content-Length:該資源的大小,單位爲字節。
package com.mark.common.utils;ios
import com.mark.common.exception.Campuso2oException;
import org.apache.commons.lang3.StringUtils;
import sun.misc.BASE64Encoder;web
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;spring
/**
* @Description: 下載文件的工具類
* @Author: Mark
* @CreateDate: 2019/2/23 16:29
* @Version: 2.0
* @Copyright : 豆漿油條我的非正式工做室
*/
public class DownloadFileUtil {apache
/**
* 下載文件
* @param originalFileName :下載文件的原始文件名
* @param file :下載的文件
* @param response :相應對象
*/
public static void downloadFile(String originalFileName, File file, HttpServletResponse response, HttpServletRequest request) {
// 數據校驗
checkParam(originalFileName,file);json
//相應頭的處理
//清空response中的輸出流
response.reset();
//設置文件大小
response.setContentLength((int) file.length());
//設置Content-Type頭
response.setContentType("application/octet-stream;charset=UTF-8");
//設置Content-Disposition頭 以附件形式解析
String encodedFilename = getEncodedFilename(request, originalFileName);
response.addHeader("Content-Disposition", "attchment;filename=" + encodedFilename);axios
//未來文件流寫入response中
FileInputStream fileInputStream = null;
ServletOutputStream outputStream = null;
try {
//獲取文件輸入流
fileInputStream = new FileInputStream(file);
//建立數據緩衝區
byte[] buffers = new byte[1024];
//經過response中獲取ServletOutputStream輸出流
outputStream = response.getOutputStream();
int length;
while ((length = fileInputStream.read(buffers)) > 0) {
//寫入到輸出流中
outputStream.write(buffers, 0, length);
}後端
} catch (IOException e) {
e.printStackTrace();
} finally {
//流的關閉
if(fileInputStream != null){
try {
fileInputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (outputStream != null) {
try {
outputStream.flush();
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/**
* 下載文件的參數的校驗,若是參數不合法則拋出自定義異常
* @param originalFileName :文件原始文件名
* @param file :待下載的文件
*/
private static void checkParam(String originalFileName, File file) {
if(StringUtils.isBlank(originalFileName)){
throw new Campuso2oException("輸入的文件原始文件名爲空");
}
if(file == null || !file.exists() ){
throw new Campuso2oException("待在下載的文件不存在!");
}
}
/**
* 獲取URL編碼後的原始文件名
* @param request :客戶端請求
* @param originalFileName :原始文件名
* @return :
*/
private static String getEncodedFilename(HttpServletRequest request, String originalFileName) {
String encodedFilename = null;
String agent = request.getHeader("User-Agent");
if(agent.contains("MSIE")){
//IE瀏覽器
try {
encodedFilename = URLEncoder.encode(originalFileName, "utf-8");
encodedFilename = encodedFilename.replace("+", " ");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}else if(agent.contains("Firefox")){
//火狐瀏覽器
BASE64Encoder base64Encoder = new BASE64Encoder();
encodedFilename = "=?utf-8?B?" + base64Encoder.encode(originalFileName.getBytes(StandardCharsets.UTF_8))+"?=";
}else{
//其餘瀏覽器
try {
encodedFilename = URLEncoder.encode(originalFileName, "utf-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
return encodedFilename;
}
}
1.2.Controller層的調用測試
package com.mark.web.common.controller;
import com.mark.common.utils.DownloadFileUtil;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
/**
* @Description: 通用文件下載類
* @Author: Kingsley
* @CreateDate: 2019/2/23 20:22
* @Version: 2.0
* @Copyright : 豆漿油條我的非正式工做室
*/
@RestController()
@RequestMapping("/common/file/")
public class FileDownloadController {
/**
* 下載文件的demo
*/
@RequestMapping(value = "download")
public void downloadFile(HttpServletRequest request, HttpServletResponse response) {
File file = new File("C:\\Users\\Administrator\\Desktop\\bak\\test.jpg");
String fileName = "測試文件下載.png";
DownloadFileUtil.downloadFile(fileName,file,response,request);
}
}
2.前端下載
2.1前端使用a鏈接標籤下載
a標籤的download屬性
<a href="http://localhost:8080/common/file/download" download="true">下載文件</a>
1
下載結果演示
左邊爲原始文件,右邊爲下載的前端演示以及下載成功後的獲得的文件。下載完成後比較兩個文件的大小,發現是一致的!
問題
以上能夠實現經過a連接下載文件。但這種下載文件的有一個安全問題,經過a連接下載是沒法進行token驗證的,若是後臺有實現了權限驗證框架,例如Shiro。那麼這種下載方式是須要在Shiro中設置url放行。
2.2優化1-axios發送post請求下載(有bug)
前端vue框架中居於axios組件經過post請求實現文件的下載,這樣前端會被後臺的權限驗證攔截,驗證token信息。
注意響應類型須要設置爲blob ,Blob對象使用說明
/**
* 文件的下載
* @param url : 請求的Url
* @param data : 請求的數據
* @param errorHandler : 請求發生異常的回調函數
*/
loadLoadFile:(url, data, errorHandler) =>{
axios.post(url,data,{
headers :{
responseType: 'blob'
}
}).then(res=>{
const blob = new Blob([res.data])
let url = window.URL.createObjectURL(blob)
//建立一個a標籤元素,設置下載屬性,點擊下載,最後移除該元素
let link = document.createElement('a')
link.href = url
link.style.display = 'none'
//res.headers.fileName 取出後臺返回下載的文件名
const downlaodFileName = decodeURIComponent(res.headers.filename)
console.log(res.headers)
link.setAttribute('download',downlaodFileName)
link.click()
window.URL.revokeObjectURL(url)
}).catch(errorHandler)
},
工具須要在響應頭信息中添加一個自定義的下載文件名
注意:雖而後臺設置文件名的key爲fileName,請求返回客戶端的時候會轉爲小寫即filename
//添加下載文件名
response.addHeader("fileName",encodedFilename);
1
2
結果演示
從下載結果中能夠看出雖然能夠下載文件,可是下載後的文件的大小比原始的文件的大小不一致。更重要的通過測試發現若是下載文件爲文檔、圖片、表格等文件會出現亂碼或者文件打不開的狀況。搜索一堆網上的博客文檔,提到多是axios下載的鍋,響應獲得文件流已是亂碼不正確的了。基於axios請求下載文件還沒找到更好的解決方法。換個方式呢?下面經過原始XHR請求能夠解決這個問題。
2.3優化2:使用XMLHttpRequest發送post請求
關於XMLHttpRequest的使用
window.URL.createObjectURL()和 window.URL.revokeObjectURL() 的使用說明
/** * 經過XMLHttpRequest發送post請求下載文件 *@param url : 請求的Url * @param data : 請求的數據 */ XHRLoadLoadFile:(url, data)=>{ let xhr = new XMLHttpRequest() xhr.open('post',url) //若是須要請求頭中這是token信息能夠在這設置 xhr.setRequestHeader('Content-Type','application/json;charset=UTF-8') xhr.responseType = 'blob' xhr.send(JSON.stringify(data)) xhr.onreadystatechange = function(){ if(xhr.readyState ===4 && xhr.status === 200){ const blob = new Blob([xhr.response]) let url = window.URL.createObjectURL(blob) //建立一個a標籤元素,設置下載屬性,點擊下載,最後移除該元素 let link = document.createElement('a') link.href = url link.style.display = 'none' //取出下載文件名 const fileName = xhr.getResponseHeader('filename') const downlaodFileName = decodeURIComponent(fileName) link.setAttribute('download',downlaodFileName) link.click() window.URL.revokeObjectURL(url) } } }, 結果演示:終於能夠正常下載了