Gin+Vite实现单图上传

news2024/10/6 14:27:32

前言

参考文献:https://blog.csdn.net/heian_99/article/details/122447855

案例目的:实现前端上传图片并显示,后端保存图片;

技术:elementplus、axios、vue3、vite、gin

实现原理:

  1. 前端请求对应后端接口,以 POST 方法在表单添加图片数据并上传
  2. 后端接收到请求,保存图片到静态文件夹内
  3. 后端返回给前端静态文件夹内图片的完整 URL
  4. 前端获取 URL,使用动态绑定更新 img 标签的 src 属性,实现图片显示

后端

项目结构图

config 配置文件读取模块初始化+logger 初始化
files 配置 protobuf(当前案例不涉及)
runtime 静态文件夹
src/constants 常量文件夹
config.yaml 配置文件


初始化配置文件

根目录新建文件 config.yaml

port 后端端口
static-mainurl 静态文件夹路径
image-save-path 图片保存目录
image-allow-extentions 图片类型校验时可通过的类型
logs-path 日志文件输出位置

port: ":10001"
mainurl: "http://localhost:10001"
static-mainurl: "http://localhost:10001/static"

image-save-path: "./runtime/uploads/images"
image-max-size: 5
image-allow-extentions: [".jpg", ".png", ".jpeg", ".gif"]

logs-path: "./runtime/logs/logger.json"

新建代码文件执行 config 初始化: config/config_loader.go

这里需要使用 viper 包,快速上手入门教程请查看我之前介绍的文章或者对应资料,这里因篇幅原因不录入

package config

import (
	"fmt"
	"github.com/spf13/viper"
)

func ConfigurationInit() {
	viper.SetConfigName("config")	// 配置文件名字
	viper.SetConfigType("yaml")		// 配置文件后缀
	viper.AddConfigPath("./")		// 配置文件所在相对路径,路径起始点为项目根目录
	err := viper.ReadInConfig()		// 读入配置
	if err != nil {
		panic(fmt.Errorf("read config err=%s", err))
	} else {
		fmt.Println(viper.GetString("desc"))
	}
}

跨域

由于前端请求后端接口时的 referer 中端口不一致,后端会因其跨域而直接拦截,导致前端无法请求后端接口;

需要自行编写跨域中间件来阻止跨域拦截;
新建跨域中间件 middleware/cors.go

package middleware

import (
	"fmt"
	"github.com/gin-gonic/gin"
)

func CORSSetting() gin.HandlerFunc {
	return func(context *gin.Context) {
		fmt.Println("已配置跨域!")

		// 允许 Origin 字段中的域发送请求
		context.Writer.Header().Add("Access-Control-Allow-Origin", "*")
		// 设置预验请求有效期为 86400 秒
		context.Writer.Header().Set("Access-Control-Max-Age", "86400")
		// 设置允许请求的方法
		context.Writer.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, UPDATE, PATCH")
		// 设置允许请求的 Header
		context.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Referer, User-Agent")
		// 设置拿到除基本字段外的其他字段,如上面的Apitoken, 这里通过引用Access-Control-Expose-Headers,进行配置,效果是一样的。
		context.Writer.Header().Set("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Headers")
		// 配置是否可以带认证信息
		context.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
		// OPTIONS请求返回200
		if context.Request.Method == "OPTIONS" {
			context.JSON(200, context.Request.Header)
			context.Abort()
		} else {
			context.Next()
		}
	}
}

主路由

为便于管理,直接把主路由抽离出来单个配置;

新建代码:src/router/main_router.go

package router

import (
	"ginmod/src/controllers"
	"github.com/gin-gonic/gin"
)

func MainRouterInit(engine *gin.Engine) {
    // 新路由组,默认path为/uploads
	uploadsRouter := engine.Group("/uploads")
	{
        // 导入的controller,请看下一节
		uploadsRouter.POST("/image", controllers.UploadSingleImage)
	}
}

常量

针对经常使用到的常量,比如响应码与响应文本等内容,我们有必要单独抽离并指定一个文档

响应码 src/constants/code.go

package constants

const (
	SUCCESS  = 200
	REDIRECT = 300
	FAILED   = 400
	ERROR    = 500

	ERROR_UPLOAD_SAVE_IMAGE = 1001
	ERROR_UPLOAD_TYPE_IMAGE = 1002
)

响应文本 src/constants/message.go

package constants

const (
	SUCCESS  = 200
	REDIRECT = 300
	FAILED   = 400
	ERROR    = 500

	ERROR_UPLOAD_SAVE_IMAGE = 1001
	ERROR_UPLOAD_TYPE_IMAGE = 1002
)

Controller

写过 springboot 或者熟悉后端结构的话,可能会比较好理解 controller 的意义;
这里编写一个上传文件的专用 controller

新建代码 src/controller/upload_controller.go

package controllers

import (
	"ginmod/src/service"
	"github.com/gin-gonic/gin"
)

func UploadSingleImage(ctx *gin.Context) {
    // 再次细分,业务交给service层处理
	service.UploadSingleImageService(ctx)
}

Service

具体业务需要给 Service 层进行细节处理

但首先我们需要编写一个基础 service,里面包含我们最常用的 response 结构体,可帮助我们快速返回指定内容而无需重复编写

代码清单:src/service/base_service.go

package service

import "github.com/gin-gonic/gin"

// 代码很简单,就是一个JSON返回
// 包含啷个参数,一个code以及一个数据msg
func BasicResponseService(ctx *gin.Context, codeId int, msg string) {
	ctx.JSON(codeId, gin.H{
		"code": codeId,
		"msg":  msg,
	})
}

紧接着就是咱们的主业务逻辑处理,即上传相关 service

代码清单:src/service/upload_service.go

package service

import (
	"ginmod/src/constants"
	"github.com/gin-gonic/gin"
	"github.com/spf13/viper"
	"net/http"
	"os"
	"path"
	"strings"
)

func UploadSingleImageService(ctx *gin.Context) {
	img, err := ctx.FormFile("file")
	if err != nil {
		BasicResponseService(
			ctx,
			http.StatusBadRequest,
			constants.GetMessage(constants.ERROR_UPLOAD_SAVE_IMAGE))
		return
	}

	suffix := strings.ToLower(path.Ext(img.Filename))
	if allowSuffix := ".jpg.png.jpeg.gif"; !strings.Contains(allowSuffix, suffix) {
		BasicResponseService(
			ctx,
			http.StatusBadRequest,
			constants.GetMessage(constants.ERROR_UPLOAD_TYPE_IMAGE))
		return
	}

	filePath := viper.GetString("image-save-path")
	_, err2 := os.Stat(filePath + "/single")
	if err2 != nil {
		os.Mkdir(filePath+"/single", os.ModePerm)
	}

	fileName := filePath + "/single/demo.jpg"
	ctx.SaveUploadedFile(img, fileName)
	BasicResponseService(
		ctx,
		http.StatusOK,
		viper.GetString("static-mainurl")+"/uploads/images/single/demo.jpg")
}

前端

template

这一部分参考 elementplus 中的 https://element-plus.gitee.io/zh-CN/component/upload.html

由于我们不需要实现多余的功能,主体就是一个上传按钮,点击后即可向后端发送图片文件;

<template>
  <!-- 外层盒子 -->
  <div class="uploadpic-container">
    <!-- 表单组件,无实际作用,仅是为了限制上传组件的位置 -->
    <el-form class="upform">
      <!-- 上传组件 -->
      <!-- action表示请求的URL,此过程为内置的axios请求POST -->
      <!-- show-fie-list表示上传成功后是否显示文件名在上传组件的下面 -->
      <el-upload
        class="uppic"
        action="http://localhost:10001/uploads/image"
        show-file-list="false"
        :on-success="handleAvatarSuccess"
        :before-upload="beforeAvatarUpload"
      >
        <!-- 显示图片以及添加图标的地方 -->
        <!-- 条件渲染,当存在图片URL时渲染图片,否则渲染添加图标 -->
        <img v-if="imageUrl" :src="imageUrl" class="avatar" />
        <el-icon v-else class="avatar-uploader-icon">
          <!-- 使用elementplus自带的图标库中的图标 -->
          <Plus />
        </el-icon>
      </el-upload>
    </el-form>
  </div>
</template>

javascript

此处用到了 pinia 指定外部 store,该 store 的内容请看下一节代码;

handleAvatarSuccess 以及 beforeAvatarUpload 均为摘抄 elementplus 中预先给定的点击响应代码,只是对其逻辑判定做出了些许修改而已

特别注意更新 imageUrl 时代码尾部的 "?" + Math.random()
这是因为我们后端写死了上传的图片只会保存在固定的位置,且名字就叫做 demo.jpg;
这就导致了生成的图床 URL 是固定的,而前端单纯地更新 ref 是无法更新图片缓存的,所以我们需要每次都在 URL 的后面生成随机数,来表示这是不同的请求,从而清除缓存,使得每次显示的图片都能立即更新!

import { ref } from "vue";
import { ElMessage } from "element-plus";
import { Plus } from "@element-plus/icons-vue";
import fileStore from "../store/file-store.js";

// 图片URL,动态绑定到img标签,显示图片
const imageUrl = ref("");
// 外部store
const store = fileStore();

// 1. 图片POST成功后拿到的response的处理
const handleAvatarSuccess = (response, uploadFile) => {
  // 建议先log一下,看看response组成,再调出对应内容
  console.log(response);
  // 响应体中获取图床URL,修改imageUrl的值,尾部添加随机数是为了清除缓存
  imageUrl.value = response.msg + "?" + Math.random();
};

// 2. POST请求前需要执行的验证操作
const beforeAvatarUpload = (rawFile) => {
  // 从外部store中取得允许通过的图片类型
  const imageTypes = store.$state.imageTypes;
  // 多当前文件类型不等于图片类型时,拒绝POST
  // 文件大小大于2MB时,也拒绝POST
  if (!imageTypes.includes(rawFile.type.toString())) {
    ElMessage.error("别搞小动作,只能上传图片");
    return false;
  } else if (rawFile.size / 1024 / 1024 > 2) {
    ElMessage.error("文件大小不可超过2MB");
    return false;
  }
  return true;
};

less

样式表,没什么好说的

.uploadpic-container {
  width: 100%;
  height: 100%;
  display: flex;
  justify-content: center;
  align-items: center;

  .upform {
    width: 80%;
    height: 80%;
    border-radius: 8px;
    background-color: white;

    .uppic {
      width: 200px;
      margin: 20px;
      border-bottom: 6px solid lightskyblue;
      cursor: pointer;
      position: relative;
      overflow: hidden;

      transition: 0.3s ease;

      &:hover {
        background-color: lightskyblue;
        box-shadow: 0 0 20px 0.1px lightgray;
      }

      .avatar {
        width: 200px;
        height: 200px;
        background-color: lightskyblue;
      }

      .avatar-uploader-icon {
        width: 200px;
        height: 200px;
      }
    }
  }
}

END

下期文章将会说明如何处理多图上传,以及 gin 下对多图的文件结构管理处理方式

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

连通性1(Tarjan 理论版)

目录 一、无向图割点、桥、双连通分量 Tarjan 算法求割点和桥&#xff08;割边&#xff09; “割点”代码 边双和点双连通分量 边双连通分量 和 点双连通分量 的缩点 二、有向图强连通分量 1.有向图的弱连通与强连通 2.强连通分量 Kosaraju算法 Tarjan 算法&#xff08…

读书笔记:Python绘制三维图像 ← 斋藤康毅

下文给出了绘制函数 的 Python 代码。 很显然&#xff0c;这是一个三维图像。【绘制三维图像的Python代码】 import numpy as np import matplotlib.pyplot as plt from mpl_toolkits.mplot3d import Axes3Dfigplt.figure() axAxes3D(fig) x1np.arange(-3.0, 3.0, 0.1) x2np.…

python刷题-关于日期、正则表达式的题

目录标题1、计算日期范围内的所有日期2、将Unix时间戳转换为格式化日期3、计算日期数据周同比4、正则表达式判断字符串是否是日期5、从文本中提取手机号码 --正则表达式6、批量提取网页上的手机号码7、自动提取电子邮箱地址8、验证用户密码是否规范-re.findall9、提取商品价格1…

ELK简介

什么是ELKE: Elasticsearch全文搜索引擎L: logstash日志采集工具K: kibana ES的可视化工具ELK是当今业界非常流行的日志采集保存和查询的系统我们编写的程序,会有很多日志信息,但是日志信息的保存和查询是一个问题IDEA控制台是一个临时显示的位置,我们可以将它保存在文件中但是…

Jetpack架构组件库:Room

Room Room是一款轻量级orm数据库&#xff0c;本质上是一个基于SQLite之上的抽象层。它通过注解的方式提供相关功能&#xff0c;编译时自动生成实现Impl&#xff0c;相比纯 SQLite 的API使用方式更加简单。另外一个相比于SQLite API的优势是&#xff1a;它会在编译时检查 SQL 语…

SpringBoot+Vue项目在线视频教育平台

文末获取源码 开发语言&#xff1a;Java 框架&#xff1a;springboot JDK版本&#xff1a;JDK1.8 服务器&#xff1a;tomcat7 数据库&#xff1a;mysql 5.7/8.0 数据库工具&#xff1a;Navicat11 开发软件&#xff1a;eclipse/myeclipse/idea Maven包&#xff1a;Maven3.3.9 浏…

网络原理之HTTP/HTTPS、TCP、IP四层协议栈

文章目录一、应用层&#xff08;一&#xff09;xml协议&#xff08;二&#xff09;json协议&#xff08;三&#xff09;protobuffer协议&#xff08;四&#xff09;HTTP协议1. 抓包工具&#xff0c;fiddler2. HTTP报文格式3. HTTP请求(Request)&#xff08;1&#xff09;URL基本…

【VisualBasicApplication】Excel编程

VBAExcel的宏与VBA宏的录制宏的启动运行快捷键运行宏&#xff1a;使用Excel对象运行宏*VBA的数据类型字符串&#xff08;String&#xff09;整形&#xff08;Integer&#xff09;和长整形&#xff08;Long&#xff09;单精度浮点型&#xff08;Single&#xff09;和双精度浮点型…

3.mysql查询必备sql语句

文章目录1.条件查询 where2. 通配符与模糊查询3. 映射4. 排序 order_by5. 取部分 limit 和offset6. 分组 group by7.左右连表 left outer join ... on8. 联合查询 union1.条件查询 where 表内容&#xff1a; import pymysqlconn pymysql.connect(host127.0.0.1,port3306,u…

向QAbstractItemView子类如:QTreeView、QTableView等子项单元格插入窗体小部件的功能实现(第2种方法)

1.前言工作中经常会遇到这样的需求&#xff1a;向QAbstractItemView子类如QTreeView、QTableView单元格插入窗体小部件&#xff0c;如&#xff1a;进度条、按钮、单行编辑框等。下面链接的系列博文就是讲解如何实现该功能的。《向QAbstractItemView子类如:QTreeView、QTableVie…

LeetCode 2500. 删除每行中的最大值

给你一个 m x n 大小的矩阵 grid &#xff0c;由若干正整数组成。 执行下述操作&#xff0c;直到 grid 变为空矩阵&#xff1a; 从每一行删除值最大的元素。如果存在多个这样的值&#xff0c;删除其中任何一个。 将删除元素中的最大值与答案相加。 注意 每执行一次操作&#…

行为型模式-状态模式

1.概述 【例】通过按钮来控制一个电梯的状态&#xff0c;一个电梯有开门状态&#xff0c;关门状态&#xff0c;停止状态&#xff0c;运行状态。每一种状态改变&#xff0c;都有可能要根据其他状态来更新处理。例如&#xff0c;如果电梯门现在处于运行时状态&#xff0c;就不能…

时序数据处理中的拟合问题

对于深度学习或机器学习模型而言,我们不仅要求它对训练数据集有很好的拟合(训练误差),同时也希望它可以对未知数据集(测试集)有很好的拟合结果(泛化能力),所产生的测试误差被称为泛化误差。度量泛化能力的好坏,最直观的表现就是模型的过拟合(overfitting)和欠拟合(…

一起Talk Android吧(第四百七十五回:渐变类视图动画)

文章目录使用方法属性介绍示例代码共用属性各位看官们大家好&#xff0c;上一回中咱们说的例子是"如何使用视图动画",这一回中咱们说的例子是"渐变类视图动画"。闲话休提&#xff0c;言归正转&#xff0c;让我们一起Talk Android吧&#xff01; 看官们&am…

移动web动画

移动web动画动画动画属性鼠标经过暂停动画多组动画鼠标经过暂停动画多组动画动画 动画最大的特点可以不用鼠标触发&#xff0c;自动的&#xff0c;反复的执行某些动画。 动画使用分为定义和调用&#xff1a; 定义&#xff1a; /* 1. 定义的动画 */ keyframes dance {from {tr…

恶意代码分析实战 12 对抗反汇编

12.1 Lab15-01 问题 这个二进制程序中使用了何种对抗反汇编技术&#xff1f; 首先&#xff0c;使用IDA载入该文件。 我们可以看到这个程序在地址0040100E处存在一个对抗反汇编技术的痕迹。 eax总是被置为零&#xff0c;jz跳转总是被执行。所以我们认为这一行是假冒的call指…

Docker的架构设计

前面我们研究了Docker容器的本质是一个特殊的进程&#xff0c;那么这个特殊进程是如何创建、如何终止的那&#xff1f;也就是说是谁来管理这个容器进程的生命周期的那&#xff1f;在mac操作系统中我们可以通过活动监视器来观察操作系统里面有哪些进程&#xff0c;以及通过活动监…

推荐算法:序列召回

目录 序列召回&#xff08;一&#xff09; 序列召回&#xff08;二&#xff09; 序列召回&#xff08;三&#xff09; 序列召回&#xff08;四&#xff09; 序列召回&#xff08;一&#xff09; 源自论文&#xff1a;http://arxiv.org/abs/1511.06939 基于GRU的序列召回中通过…

青训营项目实战1

项目实战 实现掘金青训营报名页码的后端部分 需求描述 展示话题&#xff08;标题、文字描述&#xff09;和回帖列表 不考虑前端页面实现&#xff0c;仅实现一个本地web服务 话题和回帖数据用文件存储 附加要求&#xff1a; 支持发布帖子 本地id生成要保证不重复 append文件 更…

【ONE·C || 指针】

总言 C语言&#xff1a;指针的使用介绍。 文章目录总言1、指针初阶1.1、是什么1.2、指针和指针类型1.2.1、指针类型介绍1.2.2、作用一&#xff1a;指针解引用1.2.3、作用二&#xff1a;指针整数1.3、野指针1.3.1、野指针是什么1.3.2、为什么存在野指针1.3.3、如何避免野指针1.4…