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

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

前言

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

原理篇

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

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

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

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

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

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

image-20240322100322978

这个时候我们需要把他转换为一个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

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

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

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

<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文件里面

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

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

之后我们做一个文件映射

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

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

之后是后端的代码

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

二进制存储到数据库

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

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

image-20240322090612941

第三方存储 cos

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

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

之后直接调用就可以

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的。其他的都大同小异:

<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)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/477977.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

阿里云OCR文字识别-Python3接口

1.注册/登录阿里云账号 官网链接注册登录 2.选择阿里云OCR产品 选择产品 3.开通阿里云OCR产品 开通服务&#xff08;每个月赠送200次&#xff0c;不用超就不额外收费&#xff09; 4.进入调试页面&#xff0c;下载SDK示例 下载SDK模板 5.创建 AccessKey密钥 RAM传送门 创建…

外腔激光器(ECL)市场发展空间大 外腔半导体激光器(ECDL)是主要产品类型

外腔激光器&#xff08;ECL&#xff09;市场发展空间大 外腔半导体激光器&#xff08;ECDL&#xff09;是主要产品类型 外腔激光器&#xff08;ECL&#xff09;&#xff0c;是一种利用外腔进行光反馈的激光器。根据新思界产业研究中心发布的《》2024-2029年中国外腔激光器&…

立体式学习灯最推荐哪款?书客、孩视宝、雷士等热销大路灯强势PK!

立体式学习灯是一款能够帮助长时间伏案工作以及学习人群的照明家电,正因为其优越的表现也受到了不少消费者的喜爱。作为一名电器博主,我也购入过不少立体式学习灯但时有买到一些光线不足、品质差的大路灯&#xff0c;呈现出来的光线不能提升照明条件&#xff0c;反而还会引起越…

LeetCode每日一题【19. 删除链表的倒数第 N 个结点】

思路&#xff1a;快慢指针 /*** Definition for singly-linked list.* struct ListNode {* int val;* ListNode *next;* ListNode() : val(0), next(nullptr) {}* ListNode(int x) : val(x), next(nullptr) {}* ListNode(int x, ListNode *next) : val(x)…

瑞_Redis_商户查询缓存_什么是缓存

文章目录 项目介绍1 短信登录2 商户查询缓存2.1 什么是缓存2.1.1 缓存的应用场景2.1.2 为什么要使用缓存2.1.3 Web应用中缓存的作用2.1.4 Web应用中缓存的成本 附&#xff1a;缓存封装工具类 &#x1f64a; 前言&#xff1a;本文章为瑞_系列专栏之《Redis》的实战篇的商户查询缓…

【数据库系统】数据库完整性和安全性

第六章 数据库完整性和安全性 基本内容 安全性&#xff1b;完整性&#xff1b;数据库恢复技术&#xff1b;SQL Server的数据恢复机制&#xff1b; 完整性 实体完整性、参照完整性、用户自定义完整性 安全性 身份验证权限控制事务日志&#xff0c;审计数据加密 数据库恢复 冗余…

Vue3 v-for绑定的dom获取ref为undefined

这是代码结构 <div class"playerInfo" v-for"(item, index) in data.playersInfo" :key"index" :ref"el > {if(el)playersRef[index] el}":style"left:${item.position[0]};top:${item.position[1]}"click"pla…

【ZooKeeper】2、安装

本文基于 Apache ZooKeeper Release 3.7.0 版本书写 作于 2022年3月6日 14:22:11 转载请声明 下载zookeeper安装包 wget https://mirrors.tuna.tsinghua.edu.cn/apache/zookeeper/zookeeper-3.7.0/apache-zookeeper-3.7.0-bin.tar.gz解压 tar -zxvf apache-zookeeper-3.7.0-b…

Transformer的前世今生 day06(Self-Attention和RNN、LSTM的区别

Self-Attention和RNN、LSTM的区别 RNN的缺点&#xff1a;无法做长序列&#xff0c;当输入很长时&#xff0c;最后面的输出很难参考前面的输入&#xff0c;即长序列会缺失上文信息&#xff0c;如下&#xff1a; 可能一段话超过50个字&#xff0c;输出效果就会很差了 LSTM通过忘…

什么是行业垂直类媒体?有哪些?怎么邀约

传媒如春雨&#xff0c;润物细无声&#xff0c;大家好&#xff0c;我是51媒体胡老师。 行业垂直类媒体是聚焦于特定行业或领域的媒体平台。 行业垂直类媒体不同于主流媒体&#xff0c;它们专注于提供与某个特定领域相关的深入内容和服务&#xff0c;例如商业新闻、旅游、数字…

什么快递可以寄摩托车?看你要啥样的了

一辆49cc的二冲程摩托车仅需561元。 购买125的组装车不会超过1元&#xff0c;购买250品牌发动机的组装车不会超过4000元。 购买一辆名牌摩托车大约需要4000到10000元。 花一万到两百万多就能买到一辆像样、动力强劲、能玩的炫酷摩托车。 哈哈&#xff0c;就看你想要什么了&…

力扣---随机链表的复制

给你一个长度为 n 的链表&#xff0c;每个节点包含一个额外增加的随机指针 random &#xff0c;该指针可以指向链表中的任何节点或空节点。 构造这个链表的 深拷贝。 深拷贝应该正好由 n 个 全新 节点组成&#xff0c;其中每个新节点的值都设为其对应的原节点的值。新节点的 n…

关于5.x版本的Neo4j与py2neo的访问技巧

先说结果。 Neo4j是可以使用py2neo来操作的。而且网上搜到的教程和方法里&#xff0c;首推的http连接方法可能并不是最好的&#xff0c;应该用 bolt 方法可能更好。 对于大多数使用 py2neo 与 Neo4j 数据库进行交互的应用程序来说&#xff0c;建议使用 Bolt 协议&#xff08;即…

操作系统实践之路——五、初始化(2.Linux初始化)

文章目录 一、全局流程二、从BIOS到GRUB三、GRUB是如何启动的四、详解vmlinuz文件结构五、流程梳理-1六、内核初始化从_start开始七、流程梳理-2参考资料 前言 ​ 本章节将讨论一下Linux如何去做初始化。 一、全局流程 ​ 在机器加电后&#xff0c;BIOS 会进行自检&#xff…

Wi-Fi 7:下一代无线网络的革命性技术与特点解析

更多精彩内容在 随着无线通信的不断发展&#xff0c;Wi-Fi技术作为无线网络连接的重要组成部分&#xff0c;也在不断演进。Wi-Fi 7作为下一代无线网络标准&#xff0c;被认为将带来革命性的变化&#xff0c;提供更快速、更可靠的网络连接。本文将深入解析Wi-Fi 7的技术和特点&a…

广交会参展,一起来看看展会二维如何制作吧

展会&#xff0c;一直都是企业开发客户、寻找合作伙伴、拓展渠道、展示产品和技术、提升品牌知名度、行业交流的重要宣传活动。 据相关资料显示&#xff0c;2024年新能源行业和电子电力行业依旧是展会青睐的重点行业&#xff0c;分别占到统计数据的35%和38%。从举办展会的国家…

GPT-4 VS Claude3、Gemini、Sora:五大模型的技术特点与用户体验

【最新增加Claude3、Gemini、Sora、GPTs讲解及AI领域中的集中大模型的最新技术】 2023年随着OpenAI开发者大会的召开&#xff0c;最重磅更新当属GPTs&#xff0c;多模态API&#xff0c;未来自定义专属的GPT。微软创始人比尔盖茨称ChatGPT的出现有着重大历史意义&#xff0c;不亚…

如何在个人Windows电脑搭建Cloudreve云盘并实现无公网IP远程访问

文章目录 1、前言2、本地网站搭建2.1 环境使用2.2 支持组件选择2.3 网页安装2.4 测试和使用2.5 问题解决 3、本地网页发布3.1 cpolar云端设置3.2 cpolar本地设置 4、公网访问测试5、结语 1、前言 自云存储概念兴起已经有段时间了&#xff0c;各互联网大厂也纷纷加入战局&#…

九、C#桶排序算法

简介 桶排序是一种线性时间复杂度的排序算法&#xff0c;它将待排序的数据分到有限数量的桶中&#xff0c;每个桶再进行单独排序&#xff0c;最后将所有桶中的数据按顺序依次取出&#xff0c;即可得到排序结果。 实现原理 首先根据待排序数据&#xff0c;确定需要的桶的数量。…

27 OpenCV 凸包

文章目录 概念Graham扫描算法convexHull 凸包函数示例 概念 什么是凸包(Convex Hull)&#xff0c;在一个多变形边缘或者内部任意两个点的连线都包含在多边形边界或者内部。 正式定义&#xff1a; 包含点集合S中所有点的最小凸多边形称为凸包 Graham扫描算法 首先选择Y方向最低…