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

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

html 复制代码
<form action="http://localhost:8080/files" enctype="multipart/form-data" method="POST">
  <input name="file" type="file" id="file">
  <input type="submit" value="提交">
</form>
2.FormData(常用)

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

html 复制代码
<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>
3.Blob

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

1.直接使用 blob 上传

js 复制代码
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);

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

js 复制代码
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)
4.ArrayBuffer

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

js 复制代码
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)

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

5.Base64
js 复制代码
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.MultipartFile

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

  • 需要注意,@RequestParam MultipartFile file,因此前端传来的需要有形参file,即上文formData.append('file', this.form.file.raw);
java 复制代码
@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("文件转换异常");
    }

}

FileUtil工具类

java 复制代码
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;
    }
}

二、文件下载功能

后端:文件传输

java 复制代码
@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.设置响应类型为'blob'

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

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

js 复制代码
axios.get('http://localhost:8080/download', { 
    response: 'blob'
})

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

js 复制代码
axios.post('http://localhost:8080/download', { 
    response: 'blob',
    headers: {
        'Content-Type': 'multipart/form-data'
    }
})
2.文件解析及下载
js 复制代码
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.后端无法使用统一的结果返回类(统一结果返回类会被序列化为JSON),故需要使用可以携带二进制流文件(byte[])的返回类,即ResponseEntity<Resource>,通常需要将文件配置(文件名、文件类型)保存在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

前端代码:

js 复制代码
downloadPackage(row) {
    this.$api.downloadOtaPackage(row.id.id)
        .then(res => {
        downLoadFile(res)
    })
        .catch(error => {
        console.error('下载文件时出错:', error);
    });
},

downLoadFile.js

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;
  }
}

后端代码:

java 复制代码
@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();
}

参考文章:

相关推荐
黄尚圈圈3 小时前
Vue 中引入 ECharts 的详细步骤与示例
前端·vue.js·echarts
2401_857622666 小时前
SpringBoot框架下校园资料库的构建与优化
spring boot·后端·php
2402_857589366 小时前
“衣依”服装销售平台:Spring Boot框架的设计与实现
java·spring boot·后端
哎呦没7 小时前
大学生就业招聘:Spring Boot系统的架构分析
java·spring boot·后端
_.Switch7 小时前
Python Web 应用中的 API 网关集成与优化
开发语言·前端·后端·python·架构·log4j
一路向前的月光7 小时前
Vue2中的监听和计算属性的区别
前端·javascript·vue.js
长路 ㅤ   7 小时前
vue-live2d看板娘集成方案设计使用教程
前端·javascript·vue.js·live2d
编程、小哥哥7 小时前
netty之Netty与SpringBoot整合
java·spring boot·spring
IT学长编程8 小时前
计算机毕业设计 玩具租赁系统的设计与实现 Java实战项目 附源码+文档+视频讲解
java·spring boot·毕业设计·课程设计·毕业论文·计算机毕业设计选题·玩具租赁系统
Jiaberrr9 小时前
Element UI教程:如何将Radio单选框的圆框改为方框
前端·javascript·vue.js·ui·elementui