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

参考文章:

相关推荐
志尊宝14 分钟前
Vue3 零基础每日笔记(005):ref 响应式基础——为什么 script 里要 .value
前端·javascript·vue.js·笔记
BS30813_vx39 分钟前
基于 Express + uni-app 的养老院管理系统设计与实现
后端·uni-app·express
yyt36304584142 分钟前
一次鼠标交互触发两次 Vue flushJobs:把高频交互从 VDOM 里拆出来
前端·vue.js·计算机外设
明月_清风1 小时前
DeepSeek Harness 安全审计实录:四个 PoC 揭露的 Agent 运行时"信任危机"
后端·安全·agent
苏三说技术1 小时前
再见了EasyExcel,我决定用Apache Fesod
后端
知兀1 小时前
框架脚手架搭建,推送github一键使用
javascript·vue.js·ecmascript
XR1234567881 小时前
医院移动医护零漫游无线网络选型指南
java·后端·struts
慧一居士1 小时前
Mybatis Plus中 LambdaQueryWrapper、QueryWrapper 区别对比
spring boot
干到60岁退休的码农1 小时前
13.构建登录接口响应数据
java·spring boot·mybatis
水巷石子2 小时前
学习langChain4j的第二天,体验springBoot中的starter
java·spring boot·学习·spring·langchain4j