网上介绍了很多前端文件下载的方式,这里就只给出一种完整的方式:前端将后端返回的文件流转化为blob对象,搭配FileReader实现文件下载。

1、前端vue代码

前端的主要逻辑是,点击下载按钮,通过post请求把 下载路径传递给后端,下面是点击click以后的执行函数downloadAppendix。

downloadAppendix(fileUrl){
      let params = {};
      params['pathName'] = fileUrl;
      //第一个参数是请求方式,第二个参数是后端接口的路径,第三个参数是请求携带的数据(Object类型)
      downloadFile('post','/api/download/file',params,null)
},
其中downloadFile函数的定义,我单独放在了downloadUtil.js中,下面的代码可以直接照搬。
function downloadEvt(url, fileName ) {
    const el = document.createElement('a');
    el.style.display = 'none';
    el.setAttribute('target', '_blank');
    /**
     * download的属性是HTML5新增的属性
     * href属性的地址必须是非跨域的地址,如果引用的是第三方的网站或者说是前后端分离的项目(调用后台的接口),这时download就会不起作用。
     * 此时,如果是下载浏览器无法解析的文件,例如.exe,.xlsx..那么浏览器会自动下载,但是如果使用浏览器可以解析的文件,比如.txt,.png,.pdf....浏览器就会采取预览模式
     * 所以,对于.txt,.png,.pdf等的预览功能我们就可以直接不设置download属性(前提是后端响应头的Content-Type: application/octet-stream,如果为application/pdf浏览器则会判断文件为 pdf ,自动执行预览的策略)
     */
    fileName && el.setAttribute('download', fileName);
    el.href = url;
    console.log(el);
    document.body.appendChild(el);
    el.click();
    document.body.removeChild(el);
};

// 根据header里的contenteType转换请求参数
function transformRequestData(contentType, requestData) {
    requestData = requestData || {};
    if (contentType.includes('application/x-www-form-urlencoded')) {
        // formData格式:key1=value1&key2=value2,方式二:qs.stringify(requestData, {arrayFormat: 'brackets'}) -- {arrayFormat: 'brackets'}是对于数组参数的处理
        let str = '';
        for (const key in requestData) {
            if (Object.prototype.hasOwnProperty.call(requestData, key)) {
                str += `${key}=${requestData[key]}&`;
            }
        }
        return encodeURI(str.slice(0, str.length - 1));
    } else if (contentType.includes('multipart/form-data')) {
        const formData = new FormData();
        for (const key in requestData) {
            const files = requestData[key];
            // 判断是否是文件流
            const isFile = files ? files.constructor === FileList || (files.constructor === Array && files[0].constructor === File) : false;
            if (isFile) {
                for (let i = 0; i < files.length; i++) {
                    formData.append(key, files[i]);
                }
            } else {
                formData.append(key, files);
            }
        }
        return formData;
    }
    // json字符串{key: value}
    return Object.keys(requestData).length ? JSON.stringify(requestData) : '';
}
/**
 * ajax实现文件下载、获取文件下载进度
 * @param {String} method - 请求方法get/post
 * @param {String} url
 * @param {Object} [params] - 请求参数,{name: '文件下载'}
 * @param {Object} [config] - 方法配置
 */
export function downloadFile(method = 'get', url, params, config){
    const _method = method.toUpperCase();
    const _config = Object.assign({
        contentType: _method === 'GET' ? 'application/x-www-form-urlencoded' : 'application/json',  // 请求头类型
        fileName: '未知文件',                                        // 下载文件名(必填,若为空,下载下来都是txt格式)
        async: true,                                                // 请求是否异步-true异步、false同步
        token: 'token'                                              // 用户token
    }, config);

    const queryParams = transformRequestData(_config.contentType, params);
    const _url = `${url}${_method === 'GET' && queryParams ? '?' + queryParams : ''}`;

    const ajax = new XMLHttpRequest();
    ajax.open(_method, _url, _config.async);
    ajax.setRequestHeader('Authorization', _config.token);
    ajax.setRequestHeader('Content-Type', _config.contentType);
    // responseType若不设置,会导致下载的文件可能打不开
    ajax.responseType = 'blob';
    // 获取文件下载进度
    ajax.addEventListener('progress', (progress) => {
        const percentage = ((progress.loaded / progress.total) * 100).toFixed(2);
        const msg = `下载进度 ${percentage}%...`;
        // console.log(msg);
    });
    ajax.onload = function () {
        if (this.status === 200 || this.status === 304) {
            // 通过FileReader去判断接口返回是json还是文件流
            const fileReader = new FileReader();
            fileReader.onloadend = (e) => {
                if (this.getResponseHeader('content-type').includes('application/json')) {
                    const result = JSON.parse(fileReader.result || '{message: 服务器出现问题,请联系管理员}');
                    alert(result.message);
                } else {
                    // 两种解码方式,区别自行百度: decodeURIComponent/decodeURI(主要获取后缀名,否则低版本浏览器会一律识别为txt,导致下载下来的都是txt)
                    let _fileName = this.getResponseHeader('content-disposition').split('filename=')[1];
                    _fileName = decodeURI(encodeURI(_fileName));
                    /**
                     * Blob.type一个字符串,表明该 Blob 对象所包含数据的 MIME 类型。如果类型未知,则该值为空字符串。
                     * 对于pdf:type为application/pdf  同时 a标签 不设置download属性, 可以直接预览
                     */
                    const blob = new Blob([this.response]);

                    const href = URL.createObjectURL(blob);
                    downloadEvt(href,_fileName);
                    // 释放一个之前已经存在的、通过调用 URL.createObjectURL() 创建的 URL 对象
                    URL.revokeObjectURL(href);
                }
            };
            // 调用readAsText读取文件,少了readAsText将不会触发onloadend事件
            fileReader.readAsText(this.response);
        } else {
            alert('服务器出现问题,请联系管理员');
        }
    };
    // send(string): string:仅用于 POST 请求
    ajax.send(queryParams);
}

2、后端springboot代码

这里有个奇怪的现象是,我使用其他名字作为下载的文件的名字,文件下载以后的格式全都是txt格式,即便原本是word格式,下载以后也会变成txt格式,但是使用pathName变量就不会。

package com.example.demo.controller;

import com.alibaba.fastjson.JSONObject;
import com.example.demo.utils.GlobalValueUtil;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.InputStream;

@RestController
@RequestMapping("/download")
public class DownloadFileController {
    @PostMapping("/file")
    public void downloadFile(@RequestBody String params, HttpServletResponse response) {
        //逐一取出前端发来的数据
        String pathName = JSONObject.parseObject(params).getString("pathName");

        //拼接成完整的路径
        String fileUrl = GlobalValueUtil.rootPath + pathName;

        ServletOutputStream out =null;
        ByteArrayOutputStream baos = null;
        try {
            //通过输入流获取文件数据
            InputStream inStream = new FileInputStream(fileUrl);
            byte[] buffer = new byte[1024];
            int len;
            baos = new ByteArrayOutputStream();
            while ((len=inStream.read(buffer))!=-1){
                baos.write(buffer,0,len);
            }
            //修改下载文件的名字
            response.addHeader("Content-Disposition", "attachment;filename=" + pathName);
            response.addHeader("Content-Length", "" + baos.size());
//            response.setHeader("filename", pathName);
            response.setContentType("application/octet-stream");
            out = response.getOutputStream();
            out.write(baos.toByteArray());

            baos.flush();
            baos.close();
            out.flush();
            out.close();
        }catch (Exception e){
            e.printStackTrace();
        }
//        return ReadFileUtil.readFromIOStream(fileUrl);
    }
}

测试结果如下,能够下载word格式和txt格式的文件,但是文件名无法显示中文

前端参考博文:

vue下载文件常用的几种方式_愤怒的小前端的博客-CSDN博客_vue 下载文件在Vue中利用Blob对象实现下载文件的超详细教程_SunFlower914的博客-CSDN博客_vue文件下载blob

后端参考博文:java后端向前端发送blob类型、arraybuffer类型数据流_小七蒙恩的博客-CSDN博客_java 返回blob 

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐