先後端分離下載文件

背景
基於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)       }     }   }, 結果演示:終於能夠正常下載了

相關文章
相關標籤/搜索