关于Gin+Vue的项目协作流程

-目录-

流程图

项目流程

后端

设计理念

主引擎注册

使用gin.New()创建引擎,或者使用gin.Default()创建引擎并自动使用中间件gin.Logger()gin.Recovery()两个中间件。

使用中间件

中间件需要在路由前或者创建路由时使用,这里可以在所有路由前使用中间件即全局中间件

gin.GET('/path',HandlerFunc,Middleware)

中间件

ctx.Next()为分界线,之前的为在请求前执行,之后的为在请求后执行。

众多中间件形成方法链,使用ctx.Abort()终止方法链并直接退出当前请求。

使用日志库

快速了解

logrus是一个常见的日志记录库。使用方法,先注册,再使用。

注册:

Log = logrus.New()
Log.Out = file							   //设置输出文件(需可写权限)
Log.SetLevel(logrus.DebugLevel)			   //设置指定等级以上才输出
Log.SetFormatter(&logrus.JSONFormatter{})  //设置输出格式

在程序任何部位使用:

Log.Infof(...)

使用Session

Session是一种保存会话的机制,用来记录用户状态信息。根据保存位置的不同可以分为基于Cookie的和基于Redis的。先注册,再使用。在gin中以中间件的形式注册Session

//基于cookie的session
cstore := cookie.NewStore([]byte("secret"))
router.Use(sessions.Sessions("gin_session", cstore))
//基于redis的session
//最大连接数,连接模式,连接Host,密码,加密的盐
rstore, _ := redis.NewStore(10, "tcp", "host:6379", "password", []byte("secret"))
router.Use(sessions.Sessions("gin_session", rstore))

在handler或middleware里使用:

session := sessions.Default(ctx)
session.Set("age", "100")
session.Get("name")

使用数据验证器

在结构体里使用tag里的binding进行验证时,可以设置自己需要的验证逻辑。

if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
    v.RegisterValidation("len_valid6", func(fl validator.FieldLevel) bool {
        return len(fl.Field().String()) > 6
    })
}

亦或者使用beego的验证器,tag为valid

ctx.ShouldBind(&user)
valid := validation.Validation{}
if b, _ := valid.Valid(ret); !b {
    for _, err := range valid.Errors {
        fmt.Println(err.Key, err.Message)
    }
}

连接数据库

快速了解gorm

使用gorm库连接数据库,一个数据库可以复用,因此可以设计函数每次使用时返回一个连接,通过以下函数打开和设置最大空闲连接数和空闲数

db, _ := gorm.Open(DSN)
db.DB().SetMaxIdleConns(10)
db.DB().SetMaxOpenConns(100)
return db
DataSourceName
[username[:password]@][protocol[(address)]]/dbname[?param1=value1&...&paramN=valueN]
例:root:12345@tcp(hostname:3306)/dbname?charset=utf8&parseTime=True&loc=Local

注册页面模板目录

当前后端不分离或者说需要后端渲染页面HTML代码时,需要用到页面模板。使用以下注册模板所在路径:

router.LoadHTMLGlob("template/**/*")

注册静态目录

当程序内或者网络上需要访问文件时需要注册目录,这样可以直接在网络上访问:

// 网络上访问到的,真实的路径
router.Static("static", "static")

前端

注册主组件

main.js内,注册主组件

const app = createApp(App)
app.mount('#app')

注册路由

main.js内,注册路由。此路由是router.js里引入的

app.use(router)

注册组件库

此ant组件库是ant-design-vue引入

app.use(ant)

创建全局变量

方便后续直接使用

app.config.globalProperties.$router = router
axios库 
app.config.globalProperties.$axios = axios.create({
    baseURL: 'http://localhost:8090',
})

创建主组件

App.vue是主组件,可以在<template>里放置菜单与路由进行跳转。

创建路由

此路由供前端跳转各个组件使用。

const webHistory = createWebHistory()

const router = createRouter({
    history: webHistory,
    routes: [
        {
            path: "/",
            name: "index",
            component: () => import("./components/Home.vue")
        },
    ]
})

export default router

创建各个分组件

放置在commponent里,实现各个功能。

前端与后端交互

总体流程图

总体流程

后端-数据库

使用gorm库。

先通过连接池建立连接。

然后创建数据模型(结构体)。数据库表一般通过数据模型进行创建。可以通过tag指定模型信息,数据库列名,类型,主外键,级联操作等:

type User struct {
	//User has one UserDesp
	//User has many Order
	//User han many Language

	gorm.Model
	Name string `gorm:"column:name;type:varchar(100);not null"` //通过标签更改数据库属性
	Age  int
	//以下仅作关联,不会被写进数据库里
	UserDesp  UserDesp   `gorm:"foreignKey:UserID;references:ID;constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"` //默认
	Orders    []Order    `gorm:"foreignKey:UserID;references:ID;constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"`
	Languages []Language `gorm:"many2many:user_languages;references:ID"` //多对多自动生成中间表
}

type UserDesp struct {
	//UserDesp belongs to User
	gorm.Model
	UserID      uint //通常使用 拥有者名称+它的主键名称 作为字段名
	Description string
}

type Order struct {
	gorm.Model
	UserID    uint //通常使用 拥有者名称+它的主键名称 作为字段名
	OrderDesp string
}

type Language struct {
	//Language has many User
	gorm.Model
	Lan   string
	Users []User `gorm:"many2many:user_languages;references:ID"`
}

建表与删表

//创建表,默认使用user的复数形式变为蛇形命名,不会改变存在的列
db.AutoMigrate(&User{})
//指定名称创建表
//db.Table("user").AutoMigrate(&User{})
//使用接口更改表名前后缀
gorm.DefaultTableNameHandler=func(db *gorm.DB, defaultTableName string) string {
    return "sys_" + defaultTableName
}

//删除表,使用automigrate时默认使用user的复数形式
db.DropTable(&User{})
//指定名称删除表
db.DropTable("user")
//存在则删表
db.DropTableIfExists(&User{})

//判断表是否存在
b1 := db.HasTable(&User{})
b2 := db.HasTable("user")
}

基本增删改查

//增,直接传入结构体
db.Create(&User{Name: "Tom", Age: 18})

//查询,若DeleteAt字段不为空,则视作被删除的
user := User{}
res := db.First(&user, "name=?", "Tom")
db.Model(&user).Association("UserDesp").Find(&userDesp)//关联查询

//更新,根据前一个查询的结果里的主键,若主键为空则全部更新
//更新多个字段直接传入目标结构体即可
db.Model(&user).Update("age", 22)
db.Model(&user.UserDesp).Update("Description", "hahaha")//关联更新

//删除,根据传入里的主键,若主键为空则全部删除,默认只会设置删除时间
db.Delete(&user)

事务处理

tx := db.Begin()
var user User
tx.Take(&user) //此处使用tx而不是db
res = tx.Commit()
if res.Error != nil {
    tx.Rollback()
}

所有接口

/*
		查询名字为Roman的第一个user的所有信息
		若为“所有user”,则改为Find,用法完全一样
		若为“没有且创建”,则为FirstOrCreate,用法完全一样
		若为“最后一个“,则为Last,用法完全一样
		若”部分信息“使用Select
		若”查询列“使用Pluck

		Where子句也可以进一步分割为Not Or Order Limit Offset Count Group Having Joins
*/
var user User
db.First(&user, "name=?", "Roman")
db.First(&user, User{Name: "Roman"})
db.Where("name=?", "Roman").First(&user)
db.Where(User{Name: "Roman"}).First(&user)
db.Model(User{Name: "Roman"}).First(&user)

//创建
db.Create(&User{
    Name:      "Jack",
    Age:       81,
    UserDesp:  UserDesp{Description: "I am a j"},
    Orders:    []Order{{OrderDesp: "oj1"}, {OrderDesp: "oj2"}},
    Languages: []Language{{Lan: "gopher"}, {Lan: "python3"}, {Lan: "java2"}},
})

/*
		更新
		直接传入新的结构体或KV
		若UpdateOrCreate,用Save 无主键则插入,有主键则更改
*/
db.Model(&user).Update("name", "Bob")

/*
		删除
		单个删除直接传需要删除的地址
		若批量删除,加Where更改User数据
		若要物理删除,默认是更改DeleteAt,用Unscoped
*/
db.Where("name=", "Jack").Delete(&User{})

后端传前端

后端流程

获取与返回数据

使用gin框架的核心功能GET,POST进行路由并传入handler。常用的有这些形式:

gin.GET("/path",HandleFunc,MiddleWare)
gin.POST("/path",HandleFunc,MiddleWare)

其中HandleFuncMiddleware参数里必须包括*gin.Context,Context是整个请求的上下文,包含了请求的各种信息,HandleFuncMiddleware也通过这个数据结构进行信息交换。

在POST或GET里,HandleFunc可以通过ctx.JSON()返回数据

router.GET("/", func(c *gin.Context) {
    //通过模板渲染,渲染网页并返回
    c.HTML(http.StatusOK, "index.html", gin.H{
        "title": "首页",
    })
    //返回JSON
    c.JSON(http.StatusOK, gin.H{
        "code": 0,
        "msg":  "success",
    })
    //返回字符串
    c.String(http.StatusOK, "test")
})

在POST或GET里,HandleFunc可以通过ctx.Param()获取数据(动态路由)

router.GET("/:id/:name", func(c *gin.Context) {
    id:=c.Param("id")
    name := c.Param("name")
    c.String(http.StatusOK, "id:%s,name:%s", id, name)
})

router.GET("/id=?&name=?", func(c *gin.Context) {
    id := c.Query("id")
    name := c.Query("name")
    c.String(http.StatusOK, "id:%s,name:%s", id, name)
})

在POST里,HandleFunc可以通过ctx.PostForm()获取表单数据(前端通过form表单提交):

router.POST("/login", func(c *gin.Context) {
    id := c.PostForm("id")
    password := c.PostForm("password")
    list := c.PostFormArray("list")
    user := c.PostFormMap("user")
    file, _ := c.FormFile("file")
    c.String(http.StatusOK, "id: %s, password: %s, list: %v, user: %v, file: %v", id, password, list, user, file)
})

数据绑定与验证

数据绑定,减少过多的PostForm(),同时tag还可以有uri绑定动态路由,binding数据验证。

router.POST("/post/:id", func(c *gin.Context) {
    type User struct {
        ID   string `form:"id" binding:"required,len=6"`
        Name string `form:"name" binding:"required,len_valid6"`
        Age  int    `form:"age" binding:"required,gte=0,lte=150"`
    }
    var user User
    c.ShouldBind(&user)
    c.ShouldBindUri(&user)
})

路由组

为了体现层次路由可以使用路由组gin.Group()

v1 := engine.Group("/v1")
v1.GET("/hello",Handler) // 路由 /v1/hello
v2 := v1.Group("/v2")    
v2.GET("/hello",Handler) // 路由 /v1/v2/hello

跨域请求CROS

使用中间件,设置header:

func CrosMiddleWare(c *gin.Context)  {
    method := c.Request.Method
    c.Header("Access-Control-Allow-Origin", "*")
    c.Header("Access-Control-Allow-Headers", "Content-Type,AccessToken,X-CSRF-Token, Authorization, Token")
    c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS")
    c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type")
    c.Header("Access-Control-Allow-Credentials", "true")

    //放行所有OPTIONS方法
    if method == "OPTIONS" {
        c.AbortWithStatus(http.StatusNoContent)
    }
    // 处理请求
    c.Next()
}

前端传后端

前端流程

GET获取数据

前端本质还是单个组件渲染页面,每个vue组件有templatescriptstyle三个模块,在script进行前后端数据交互,然后直接渲染在template里。

script里,需要在export的数据data()返回值里定义数据默认值,这个数据将在template里使用。注意定义数据默认值是不限类型的,因为JavaScript可以动态改变变量的类型。

<script>
export default {
  name: 'ComponentBook',
  data() {
    return {
      //这里必须定义
      books: [],
    }
  },
  methods: {
	//这里使用axios的get请求后端api数据,成功后返回response,经过then分支处理。注意这里需要使用箭头函数传递给整个文件,里面的this.books指的是在data()里return的books。respose里有很多字段,data字段是返回的数据类似gin.H{}。
    getBooks() { 
      this.$axios.get('/api/books').then(response => {
        this.books = response.data.books
      });
    }
  },
  //为了保证在渲染页面时能够直接运行这个函数,使用created自动运行,详细生命周期函数见下表
  created() {
    this.getBooks();
  },
}
</script>

生命周期函数和浏览器渲染过程挂钩

生命周期函数 是否获取dom节点 是否获取data 是否获取methods
beforeCreate
created
beforeMount
mounted

路由

使用router-link :to语法进行路由,使用<router-view/>确认路由后的内容显示位置。

<router-link to="/">
    首页
</router-link>

动态路由:

//router.js
{
path: '/book/:id',
name: 'book_detail',
component: () => import('./components/ComponentBookDetail.vue')
},
<!-- component.vue -->
<!-- name指定路由名称 是已经注册的router.js里的路由 变量与该路由相同 -->
<router-link :to="{name:'book_detail',params:{id:book.id}}">
  {{ book.name }}
</router-link>

POST发送数据

<script>
export default {
  data() {
    return {
      //必须定义,此处为结构体类型
      form: {
        name: '',
        password: ''
      }
    };
  },
  methods: {
    // 定义onsubmit方法,在表单@click时调用
    onSubmit() {
      //使用formData解决跨域请求
      const formData = new FormData();
      formData.append('name', this.form.name);
      formData.append('password', this.form.password);
      formData.append('file', document.querySelector('input[type=file]').files[0]);
      this.$axios.post('/api/testpost', formData).then((res) => {
        console.log(res);
      });
    }
  },
};
</script>

注意,需要使用v-model绑定数据:

<a-input v-model:value="form.name"/>
<a-input v-model:value="form.password"/>

前端与页面

直接使用{{ }}访问data()里注册的数据:

<h1>{{user.name}}</h1>

或者在需要显示的标签里使用v-for访问数组:

<!-- 注意必须使用:key 指定值 -->
<span v-for="(a,i) in datas" :key="a">
    {{ a.name }}
    {{ a.age }}
    {{ i }}
</span>
© 2019 - 2023 · YuYoung's Blog