一文带你看懂 前后端之间图片的上传与回显

一文带你看懂 前后端之间图片的上传与回显

前言

看了很多类似的文章,发现很多文章,要不就是不对,要不就是代码写的不通俗易懂,所以有了这篇文章,我将会从原理到实战,带你了解 实战包含前端 原生 vue3 react 后端springboot 主流框架来进行实现。

原理篇

上传文件需要发送请求。在这些请求中,浏览器将数据拆分为小的"块",然后通过连接逐个发送这些块。这是必要的,因为文件可能过大而无法一次性发送作为一个庞大的有效负载。

随时间发送的数据块组成了所谓的"流"。流在第一次理解时有点难 它们值得有一篇完整的文章(或多篇文章)来介绍,

基本上,流有点像是数据的传送带,每个块都可以在进入时被处理。就 HTTP 请求而言,后端会逐位接收请求的各个部分。

当我们使用请求上传文件时,浏览器将使用流一次发送一个块的数据。这是因为我们不能一次将整个文件放在请求对象中。multipart/form-data

我们直接去打印这个文件的请求。

我们应该看到一个包含所有表单字段及其值的对象,但对于每个文件输入,我们将看到一个表示上传文件的对象,而不是文件本身。此对象包含各种有用的信息,包括其在磁盘上的路径、名称等

这个时候我们需要把他转换为一个FormData 对象

这样便于我们给后端传输我们需要传输的东西。

文件上传为什么要用 multipart/form-data?

The encoding type application/x-www-form-urlencoded is inefficient for sending large quantities of binary data or text containing non-ASCII characters. Thus, a new media type,multipart/form-data, is proposed as a way of efficiently sending the values associated with a filled-out form from client to server.

1867文档中也写了为什么要新增一个类型,而不使用旧有的application/x-www-form-urlencoded:因为此类型不适合用于传输大型二进制数据或者包含非ASCII字符的数据。平常我们使用这个类型都是把表单数据使用url编码后传送给后端,二进制文件当然没办法一起编码进去了。所以multipart/form-data就诞生了,专门用于有效的传输文件。

文件上传为什么要用 multipart/form-data? 可以用application/json吗

文件上传通常使用multipart/form-data格式,而不是application/json,因为multipart/form-data格式允许在HTTP请求中传输二进制文件数据,例如图像、视频或文档等。而application/json格式通常用于传输结构化的文本数据,例如JSON对象或数组。

multipart/form-data格式允许在一个请求中同时发送文本数据和二进制文件数据,这对于上传文件非常有用。它使用一种多部分的格式,将请求体划分为多个部分,每个部分可以包含不同类型的数据,例如文本字段和文件数据。

相比之下,application/json格式虽然可以用于传输文本数据,但不支持直接在请求体中传输二进制文件数据。如果尝试将文件数据编码为JSON字符串并在application/json格式的请求中发送,通常会导致数据丢失或不可用。

实战篇

本地存储

第一个我要介绍最常用的,vue3+springboot

vue3+springboot

第一个实现方式是本地存储

也就是存储到自己的服务器上。

首先我们来看前端的源码:

xml 复制代码
<template>
  <div>
    <input type="file" @change="handleFileChange">
    <button @click="uploadImage">上传图片</button>
    <img :src="getImageUrl()" v-if="imageUrl">
  </div>
</template>
​
<script setup>
import { ref } from 'vue';
import axios from 'axios';
​
const file = ref(null);
const imageUrl = ref(null);
​
const handleFileChange = (event) => {
  file.value = event.target.files[0];
};
​
const uploadImage = async () => {
  const formData = new FormData();
  formData.append('image', file.value);
​
  try {
    const response = await axios.post('http://localhost:8081/api/upload', formData, {
      headers: {
        'Content-Type': 'multipart/form-data'
      }
    });
​
    imageUrl.value = response.data;
  } catch (error) {
    console.error('Error uploading image: ', error);
  }
};
​
const getImageUrl = () => {
  if (imageUrl.value) {
    // 拼接后端服务器地址和图片地址
    return `http://localhost:8081${imageUrl.value}`;
  }
};
</script>

这里我用到了axios 当然你也可以选择别的去用。相信我这个代码很简洁,我就不多说了。

我们来看后端的代码。

首先我们要在upload里面去配置一下我们存储文件的一个地址 我把这个放到了yml文件里面

yaml 复制代码
upload:
  path: D:\onenodes\project\xiaou-easy-code\1\xiaou-spring boot-demo-backend\src\main\java\com\xiaou\upload\

这里需要注意的是,如果是本地的话,就是完整路径,如果你是想要部署上线的话,要填写你服务器的文件路径。

之后我们做一个文件映射

typescript 复制代码
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
    registry.addResourceHandler("/uploads/**")
            .addResourceLocations("file:uploads/");
}

确保后端可以打开这个图片

之后是后端的代码

java 复制代码
package com.xiaou.controller;
​
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
​
import java.io.File;
import java.io.IOException;
import java.util.UUID;
​
@RestController
@RequestMapping("/api")
@Slf4j
public class ImageController {
​
    @Value("${upload.path}")
    private String uploadPath;
​
    @PostMapping("/upload")
    public String uploadImage(@RequestParam("image") MultipartFile image) throws IOException {
        String imageName = UUID.randomUUID().toString() + "_" + image.getOriginalFilename();
        File dest = new File(uploadPath + imageName);
        image.transferTo(dest);
​
        log.info("图片后端地址 " + "/api/images" + imageName);
        return "/api/images/" + imageName;
    }
​
    @GetMapping("/images/{imageName}")
    public ResponseEntity<Resource> getImage(@PathVariable String imageName) throws IOException {
        File file = new File(uploadPath + imageName);
        Resource resource = new UrlResource(file.toURI());
​
        return ResponseEntity.ok()
                .contentType(MediaType.IMAGE_JPEG)
                .body(resource);
    }
}

这里设置俩个接口,一个是上传,一个就是图片的一个回显。

这里的ResponseEntity 是 Spring Framework 提供的一个类,用于表示 HTTP 响应实体。它允许你将 HTTP 响应的状态码、头部信息以及响应体等内容封装到一个对象中,然后返回给客户端。

之后我们来介绍原生的html css js

后端代码保持不变。前端代码如下:

原生html css JavaScript

xml 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Image Uploader</title>
    <style>
        /* CSS 样式 */
        .container {
            margin-top: 20px;
        }
    </style>
</head>
<body>
<div class="container">
    <input type="file" id="fileInput">
    <button id="uploadButton">上传图片</button>
    <img id="uploadedImage" style="display: none;">
</div>
​
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
    // JavaScript 代码
    document.getElementById('uploadButton').addEventListener('click', function() {
        var fileInput = document.getElementById('fileInput');
        var file = fileInput.files[0];
        var formData = new FormData();
        formData.append('image', file);
​
        axios.post('http://localhost:8081/api/upload', formData, {
            headers: {
                'Content-Type': 'multipart/form-data'
            }
        }).then(function(response) {
            var imageUrl = "http://localhost:8081"+response.data;
            console.log(imageUrl)
​
            document.getElementById('uploadedImage').src = imageUrl;
            document.getElementById('uploadedImage').style.display = 'block';
        }).catch(function(error) {
            console.error('Error uploading image: ', error);
        });
    });
</script>
</body>
</html>

react

javascript 复制代码
import React, { useState } from 'react';
import axios from 'axios';
​
function ImageUploader() {
  const [file, setFile] = useState(null);
  const [imageUrl, setImageUrl] = useState(null);
​
  const handleFileChange = (event) => {
    setFile(event.target.files[0]);
  };
​
  const uploadImage = async () => {
    const formData = new FormData();
    formData.append('image', file);
​
    try {
      const response = await axios.post('http://localhost:8081/api/upload', formData, {
        headers: {
          'Content-Type': 'multipart/form-data'
        }
      });
      var imgUrl="http://localhost:8081/"+response.data
      setImageUrl(imgUrl);
    } catch (error) {
      console.error('Error uploading image: ', error);
    }
  };
​
  return (
      <div>
        <input type="file" onChange={handleFileChange} />
        <button onClick={uploadImage}>上传图片</button>
        {imageUrl && <img src={imageUrl} alt="Uploaded" />}
      </div>
  );
}
​
export default ImageUploader;

二进制存储到数据库

这个经过我的测试不是很好实现。

数据库字段会超出。考虑过压缩图片,但是这样完全没必要。所以这个直接跳过。

第三方存储 cos

这里用到工具类,你也可以自己封装,我这里用到了阿里云的oss

java 复制代码
package com.xiaou.util;
​
import com.aliyun.oss.ClientException;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.OSSException;
​
import java.io.InputStream;
​
public class AliOssUtil {
    private static final String ENDPOINT = "xxx";
    private static final String ACCESS_KEY_ID = "xx";
    private static final String SECRET_ACCESS_KEY = "xxx";
    private static final String BUCKET_NAME = "xxx";
​
    //上传文件,返回文件的公网访问地址
    public static String uploadFile(String objectName, InputStream inputStream){
        // 创建OSSClient实例。
        OSS ossClient = new OSSClientBuilder().build(ENDPOINT,ACCESS_KEY_ID,SECRET_ACCESS_KEY);
        //公文访问地址
        String url = "";
        try {
            // 创建存储空间。
            ossClient.createBucket(BUCKET_NAME);
            ossClient.putObject(BUCKET_NAME, objectName, inputStream);
            url = "https://"+BUCKET_NAME+"."+ENDPOINT.substring(ENDPOINT.lastIndexOf("/")+1)+"/"+objectName;
        } catch (OSSException oe) {
            System.out.println("Caught an OSSException, which means your request made it to OSS, "
                    + "but was rejected with an error response for some reason.");
            System.out.println("Error Message:" + oe.getErrorMessage());
            System.out.println("Error Code:" + oe.getErrorCode());
            System.out.println("Request ID:" + oe.getRequestId());
            System.out.println("Host ID:" + oe.getHostId());
        } catch (ClientException ce) {
            System.out.println("Caught an ClientException, which means the client encountered "
                    + "a serious internal problem while trying to communicate with OSS, "
                    + "such as not being able to access the network.");
            System.out.println("Error Message:" + ce.getMessage());
        } finally {
            if (ossClient != null) {
                ossClient.shutdown();
            }
        }
        return url;
    }
}

之后直接调用就可以

kotlin 复制代码
package com.xiaou.controller;
​
import com.xiaou.util.AliOssUtil;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
​
@RestController
@RequestMapping("/api")
public class FileUploadController {
​
    @PostMapping("/upload")
    public String uploadFile(@RequestParam("file") MultipartFile file) {
        try {
            // 调用阿里云 OSS 工具类上传文件
            String objectName = file.getOriginalFilename();
            String url = AliOssUtil.uploadFile(objectName, file.getInputStream());
            return "File uploaded successfully! URL: " + url;
        } catch (Exception e) {
            e.printStackTrace();
            return "Error uploading file: " + e.getMessage();
        }
    }
}

之后来看前端实现,我只写一下vue3的。其他的都大同小异:

xml 复制代码
<template>
  <div>
    <input type="file" @change="handleFileChange">
    <button @click="uploadFile">上传文件</button>
    <div v-if="uploadStatus">{{ uploadStatus }}</div>
  </div>
</template>
​
<script>
import axios from 'axios';
​
export default {
  data() {
    return {
      file: null,
      uploadStatus: ''
    };
  },
  methods: {
    handleFileChange(event) {
      this.file = event.target.files[0];
    },
    async uploadFile() {
      if (!this.file) {
        this.uploadStatus = '请选择要上传的文件';
        return;
      }
​
      const formData = new FormData();
      formData.append('file', this.file);
​
      try {
        const response = await axios.post('http://localhost:8080/api/upload', formData, {
          headers: {
            'Content-Type': 'multipart/form-data'
          }
        });
        this.uploadStatus = response.data;
      } catch (error) {
        this.uploadStatus = '上传文件出错: ' + error.message;
      }
    }
  }
};
</script>
​

至此,文件上传完成。

后记

这里说一个题外话,关于我自己开了一个新的项目,在业务开发中,我们有很多很固定的代码,这些东西我们大部分情况下会去选择复制一些,但是由于网络上的资源良莠不齐,而且很多代码没有详细的讲解,所以我打算开一个这样的通用模板项目。

目前项目在初期阶段,这个也是这个项目的第一个通用解决方法。各位如果有兴趣可以来看一看我这个项目,提个pr issue 一起共创这个项目。

我也会经常去更新这个项目,去抽离出一些优秀的解决方案。

xiaou61/xiaou-easy-code: 全栈通用解决方案合集 包含在开发工作中解决常用问题的较优方案 包括springboot vue3 react java javescript (github.com)

以及文档地址:

Xiaou-EasyCode-Docs (xiaou61.top)

相关推荐
sunshine64119 分钟前
【CSS】实现tag选中对钩样式
前端·css·css3
Q_192849990622 分钟前
基于Spring Boot的九州美食城商户一体化系统
java·spring boot·后端
真滴book理喻42 分钟前
Vue(四)
前端·javascript·vue.js
蜜獾云44 分钟前
npm淘宝镜像
前端·npm·node.js
dz88i81 小时前
修改npm镜像源
前端·npm·node.js
Jiaberrr1 小时前
解锁 GitBook 的奥秘:从入门到精通之旅
前端·gitbook
ZSYP-S1 小时前
Day 15:Spring 框架基础
java·开发语言·数据结构·后端·spring
Yuan_o_2 小时前
Linux 基本使用和程序部署
java·linux·运维·服务器·数据库·后端
程序员一诺2 小时前
【Python使用】嘿马python高级进阶全体系教程第10篇:静态Web服务器-返回固定页面数据,1. 开发自己的静态Web服务器【附代码文档】
后端·python
顾平安2 小时前
Promise/A+ 规范 - 中文版本
前端