• SpringBoot+Vue前后端文件传输问题总结


    在这里插入图片描述

    解决前后端文件传输的问题有以下几种解决方案:

    1.文件上传时,前端以二进制流文件发送到后端,后端通过多种方式(MultipartFile/byte[]/File)进行接受,处理后进行存储;文件下载时,后端通常返回前端二进制流(byte[])的形式,并将文件附带信息(fileName、contentType)放在response header中一并传输到前端供其解析与下载。

    2.微服务项目中,通常搭建网盘模块提供文件上传下载功能,供文件传输业务使用。

    一、文件上传功能

    前端:文件上传

    前端文件上传主要有以下五种方式

    • File
    • FormData
    • Blob
    • ArrayBuffer
    • Base64
    1.File

    文件上传 enctype 要用 multipart/form-data,而不是 application/x-www-form-urlencoded

    <form action="http://localhost:8080/files" enctype="multipart/form-data" method="POST">
      <input name="file" type="file" id="file">
      <input type="submit" value="提交">
    form>
    
    • 1
    • 2
    • 3
    • 4
    2.FormData(常用)

    采用这种方式进行文件上传,主要是掌握文件上传的请求头和请求内容。

    <template>
      <div>
          <el-form ref="form" :model="form" >
            <el-form-item v-show="!form.isURL" label="文件" prop="file">
              <el-upload
                ref="upload"
                :limit="1"
                accept="*"
                action="#"
                class="el-input"
                drag>
                <i class="el-icon-upload">i>
                <div class="el-upload__text">拖拽文件或者单击以选择要上传的文件div>
              el-upload>
            el-form-item>
            <el-form-item label="说明" prop="description">
              <el-input v-model="form.description" autosize type="textarea">el-input>
            el-form-item>
            <el-button type="primary" @click="uploadFile()">上 传el-button>
          el-form>
      div>
    template>
    <script>
    import axios from 'axios'
    import {downLoadFile} from "@/utils/downloadFile";
    export default {
      data() {
        return {
          form: {
            description: '',
            file: {},
          }
        }
      },
      methods: {
        uploadFile() {
          let formData = new FormData();
          formData.append('file', this.form.file.raw);
          formData.append('description', this.form.description);
          axios.post('http://localhost:8080/files', formData,{ headers: { 'Content-Type': 'multipart/form-data' }}).then(res => {
            console.log(res.data);
          })
        }
      }
    }
    script>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46

    image-20230812141805107

    3.Blob

    Blob 对象表示一个不可变、原始数据的类文件对象。Blob 表示的不一定是JavaScript原生格式的数据。File 接口基于Blob,继承了 blob 的功能并将其扩展使其支持用户系统上的文件。

    1.直接使用 blob 上传

    const json = { hello: "world" };
    const blob = new Blob([JSON.stringify(json, null, 2)], { type: 'application/json' });
    
    const form = new FormData();
    form.append('file', blob, 'test.json');
    axios.post('http://localhost:8080/files', form);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    2.使用 File 对象,再进行一次包装

    const json = { hello: "world" };
    const blob = new Blob([JSON.stringify(json, null, 2)], { type: 'application/json' });
    
    const file = new File([blob], 'test.json');
    form.append('file', file);
    axios.post('http://localhost:8080/files', form)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    4.ArrayBuffer

    ArrayBuffer 对象用来表示通用的、固定长度的原始二进制数据缓冲区、是最贴近文件流的方式。在浏览器中,ArrayBuffer每个字节以十进制的方式存在。

    const bufferArrary = [137,80,78,71,13,10,26,10,0,0,0,13,73,72,68,82,0,0,0,1,0,0,0,1,1,3,0,0,0,37,219,86,202,0,0,0,6,80,76,84,69,0,0,255,128,128,128,76,108,191,213,0,0,0,9,112,72,89,115,0,0,14,196,0,0,14,196,1,149,43,14,27,0,0,0,10,73,68,65,84,8,153,99,96,0,0,0,2,0,1,244,113,100,166,0,0,0,0,73,69,78,68,174,66,96,130];
    const array = Uint8Array.from(bufferArrary);
    const blob = new Blob([array], {type: 'image/png'});
    const form = new FormData();
    form.append('file', blob, 'test.png');
    axios.post('http://localhost:8080/files', form)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    这里需要注意的是 new Blob([typedArray.buffer], {type: 'xxx'}),第一个参数是由一个数组包裹。里面是 typedArray 类型的 buffer。

    5.Base64
    const base64 = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAABlBMVEUAAP+AgIBMbL/VAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAACklEQVQImWNgAAAAAgAB9HFkpgAAAABJRU5ErkJggg==';
    const byteCharacters = atob(base64);
    const byteNumbers = new Array(byteCharacters.length);
    for (let i = 0; i < byteCharacters.length; i++) {
        byteNumbers[i] = byteCharacters.charCodeAt(i);
    }
    const array = Uint8Array.from(byteNumbers);
    const blob = new Blob([array], {type: 'image/png'});
    const form = new FormData();
    form.append('file', blob, 'test.png');
    axios.post('http://localhost:8080/files', form);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    后端:文件接收

    1.MultipartFile

    MultipartFile是SpringMVC提供简化上传操作的工具类。在不使用框架之前,都是使用原生的HttpServletRequest来接收上传的数据,文件是以二进制流传递到后端的,然后需要我们自己转换为File类,MultipartFile主要是用表单的形式进行文件上传,在接收到文件时,可以获取文件的相关属性,比如文件名、文件大小、文件类型等等。

    • 需要注意,@RequestParam MultipartFile file,因此前端传来的需要有形参file,即上文formData.append('file', this.form.file.raw);
    @PostMapping("/upLoadFile")
    public void upLoadFile(@RequestBody MultipartFile file) {
        // 获取文件的完整名称,文件名+后缀名
        System.out.println(file.getOriginalFilename());
        // 文件传参的参数名称
        System.out.println(file.getName());
        // 文件大小,单位:字节
        System.out.println(file.getSize());
        // 获取文件类型,并非文件后缀名
        System.out.println(file.getContentType());
        try {
            // MultipartFile 转 File
            File resultFile = FileUtil.multipartFile2File(file);
            System.out.println(resultFile.getName());
    
        } catch (IOException e) {
            log.info("文件转换异常");
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    FileUtil工具类

    public class FileUtil {
        /**
         * file转byte
         */
        public static byte[] file2byte(File file){
            byte[] buffer = null;
            try{
                FileInputStream fis = new FileInputStream(file);
                ByteArrayOutputStream bos = new ByteArrayOutputStream();
                byte[] b = new byte[1024];
                int n;
                while ((n = fis.read(b)) != -1)
                {
                    bos.write(b, 0, n);
                }
                fis.close();
                bos.close();
                buffer = bos.toByteArray();
            }catch (FileNotFoundException e){
                e.printStackTrace();
            }
            catch (IOException e){
                e.printStackTrace();
            }
            return buffer;
        }
    
        /**
         * byte 转file
         */
        public static File byte2file(byte[] buf, String filePath, String fileName){
            BufferedOutputStream bos = null;
            FileOutputStream fos = null;
            File file = null;
            try{
                File dir = new File(filePath);
                if (!dir.exists() && dir.isDirectory()){
                    dir.mkdirs();
                }
                file = new File(filePath + File.separator + fileName);
                fos = new FileOutputStream(file);
                bos = new BufferedOutputStream(fos);
                bos.write(buf);
            }catch (Exception e){
                e.printStackTrace();
            }
            finally{
                if (bos != null){
                    try{
                        bos.close();
                    }catch (IOException e){
                        e.printStackTrace();
                    }
                }
                if (fos != null){
                    try{
                        fos.close();
                    }catch (IOException e){
                        e.printStackTrace();
                    }
                }
            }
            return file;
        }
    /**
    * multipartFile转File
    **/
    public static File multipartFile2file(MultipartFile multipartFile){
            File file = null;
            if (multipartFile != null){
                try {
                    file=File.createTempFile("tmp", null);
                    multipartFile.transferTo(file);
                    System.gc();
                    file.deleteOnExit();
                }catch (Exception e){
                    e.printStackTrace();
                    log.warn("multipartFile转File发生异常:"+e);
                }
            }
            return file;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83

    二、文件下载功能

    后端:文件传输

    @GetMapping("/download")
    public ResponseEntity<Resource> download( @RequestParam("fileId") String fileId) {
        if (StringUtils.isNotBlank(fileId)) {
            File file = new File("test.jpg");
            String fileName = file.getName();
            String contentType = file.getContentType();
            FileSystemResource fileSource = new FileSystemResource(file)
            return ResponseEntity.ok()
                .header(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + fileName)
                .header("filename", fileName)
                // 配置使前端可以获取的header中的
                .header(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, "filename")
                .contentLength(resource.contentLength())
                .contentType(parseMediaType(contentType))
                .body(fileSource);
        }
        return (ResponseEntity<Resource>) ResponseEntity.badRequest();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    image-20230812145320428

    如果后端返回的是如下图一样的,那么就是传输的文件流

    前端:文件接收

    1.设置响应类型为’blob’

    Blob:按文本或二进制的格式进行读取,在axios请求中设置response: 'blob'

    假设这是一个返回文件流的请求:

    axios.get('http://localhost:8080/download', { 
        response: 'blob'
    })
    
    • 1
    • 2
    • 3

    如果是post请求还需要在请求头里携带Content-Type: ‘multipart/form-data’

    axios.post('http://localhost:8080/download', { 
        response: 'blob',
        headers: {
            'Content-Type': 'multipart/form-data'
        }
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    2.文件解析及下载
    axios.get(`http://localhost:8080/download?fileId=${fileId}`, { responseType: 'blob', observe: 'response' })
        .then(response => {
        const headers = response.headers;
        console.log(response.headers)
        const filename = headers['x-filename'];
        const contentType = headers['content-type'];
        const linkElement = document.createElement('a');
        try {
            const blob = new Blob([response.data], { type: contentType });
            const url = URL.createObjectURL(blob);
            linkElement.setAttribute('href', url);
            linkElement.setAttribute('download', filename);
            const clickEvent = new MouseEvent('click',{
                view: window,
                bubbles: true,
                cancelable: false
            });
            linkElement.dispatchEvent(clickEvent);
            return null;
        } catch (e) {
            throw e;
        }
    })
        .catch(error => {
        console.error('下载文件时出错:', error);
    });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    三、开发中遇到的问题

    1.后端无法使用统一的结果返回类(统一结果返回类会被序列化为JSON),故需要使用可以携带二进制流文件(byte[])的返回类,即ResponseEntity,通常需要将文件配置(文件名、文件类型)保存在http response headers头中,将二进制流文件放在ResponseEntity的body中。

    2.前端发送请求时,要注意http请求的config配置(headers与responseType与observe),另外可以将文件解析下载的操作封装成一个js工具。

    3.前后端交互时,axios请求放在response header里的文件名时,会出问题,跨前后端分离发送http请求时,默认reponse header中只能取到以下5个默认值,要想取得其他的字段需要在后端设置Access-Control-Expose-Headers 配置前端想要获取的header。

    • Content-Language
    • Content-Type
    • Expires
    • Last-Modified
    • Pragma

    前端代码:

    downloadPackage(row) {
        this.$api.downloadOtaPackage(row.id.id)
            .then(res => {
            downLoadFile(res)
        })
            .catch(error => {
            console.error('下载文件时出错:', error);
        });
    },
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    downLoadFile.js

    export function downLoadFile (res) {
      // 获取响应头中的filename contentType
      const headers = res.headers;
      const filename = headers['x-filename'];
      const contentType = headers['content-type'];
      // 创建一个a链接标签
      const linkElement = document.createElement('a');
      try {
        // 将返回的文件流转换成一个blob文件对象
        const blob = new Blob([res.data], { type: contentType });
        // 生成一个文件对象的url地址
        const url = URL.createObjectURL(blob);
        // 将文件对象的url地址赋值给a标签的href属性
        linkElement.setAttribute('href', url);
        // 为a标签添加download属性并指定文件的名称
        linkElement.setAttribute('download', filename);
        // 调用a标签的点击函数
        const clickEvent = new MouseEvent('click',
          {
            view: window,
            bubbles: true,
            cancelable: false
          }
        );
        linkElement.dispatchEvent(clickEvent);
        // 释放URL对象
        URL.revokeObjectURL(url);
        // 将页面的a标签删除
        document.body.removeChild(linkElement);
      } catch (e) {
        throw e;
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    后端代码:

    @GetMapping("/download")
    public ResponseEntity<Resource> downloadOtaPackage(
        @ApiParam(value = "OTA包Id", required = true) @RequestParam("otaPackageId") String otaPackageId
    ) {
        if (StringUtils.isNotBlank(otaPackageId)) {
            ResponseEntity<Resource> responseEntity = iOtaClient.downloadOtaPackage(otaPackageId);
            ByteArrayResource resource = (ByteArrayResource) responseEntity.getBody();
            String fileName = responseEntity.getHeaders().get("x-filename").get(0);
            String contentType = responseEntity.getHeaders().getContentType().toString();
            // return FileResult.success(resource, fileName, contentType);
            return ResponseEntity.ok()
                .header(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + fileName)
                .header("x-filename", fileName)
                .header(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, "x-filename")
                .contentLength(resource.contentLength())
                .contentType(parseMediaType(contentType))
                .body(resource);
        }
        return (ResponseEntity<Resource>) ResponseEntity.badRequest();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    参考文章:

    • https://zhuanlan.zhihu.com/p/120834588
    • https://www.cnblogs.com/liuxianbin/p/13035809.html
  • 相关阅读:
    基于BERT模型的舆情分类应用研究-笔记
    C++广度优先优先搜索代码找错
    TR5521设计资料|TR5521替代方案|DP转VGA设计参考
    项目部署服务器【java】
    8K直播如何多路推流到抖音、微博、视频号、B站等平台
    使用supervisor管理你的进程
    Linux学习资源Index
    【科研新手指南4】ChatGPT的prompt技巧 心得
    Node.js | 使用 zlib 内置模块进行 gzip 压缩
    Redis从青铜到王者,从环境搭建到熟练使用,看这一篇就够了,超全整理详细解析,赶紧收藏吧
  • 原文地址:https://blog.csdn.net/qq_51808107/article/details/133785235