关于Gin+Vue的项目协作流程
流程图
后端
设计理念
- 配置与程序分离,单独写在配置文件里方便修改
- log记录在文件里持久化
- redis记录session更快速
主引擎注册
使用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
库连接数据库,一个数据库可以复用,因此可以设计函数每次使用时返回一个连接,通过以下函数打开和设置最大空闲连接数和空闲数
db, _ := gorm.Open(DSN)
db.DB().SetMaxIdleConns(10)
db.DB().SetMaxOpenConns(100)
return db
DataSourceName
[username[:password]@][protocol[(address)]]/dbname[?param1=value1&...¶mN=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)
其中HandleFunc
和Middleware
参数里必须包括*gin.Context
,Context是整个请求的上下文,包含了请求的各种信息,HandleFunc
和Middleware
也通过这个数据结构进行信息交换。
在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组件有template
、script
和style
三个模块,在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>