第一次用Web框架开发网站 · Golang
我所管理的一个群实行的是问卷考核制,要进群的人必须先填写一份问卷,由管理员批改后发送邮件通知结果。我们有一个专门用于发送这个邮件的后台,管理员仅需要填写对方的姓名、邮箱、分数、是否通过等信息,点击发送即可。
老的系统是基于WordPress + Contact form 7搭建的,现由于要与我们群内的机器人对接,所以重新写了一个新的系统。既然我来写,自然用最熟悉的Go语言,顺便学习一些Go语言用于Web应用的框架,比如之前了解过却没有真正使用过的Gin和Gorm,在这次开发中也是好好体验了一番。
接下来还是先简单了解一下Gin:
Gin is a HTTP web framework written in Go (Golang). It features a Martini-like API with much better performance – up to 40 times faster. If you need smashing performance, get yourself some Gin.
总之是个Web框架,合理使用减轻开发的负担,用更少的代码写出更好的程序。
嗯,确实很简单。
再来看看Gorm:
The fantastic ORM library for Golang, aims to be developer friendly (v2 is under development, PR based on master branch won’t be accepted)
首先要说说ORM是什么,面向对象编程把一切实体都看作对象(Object),而关系型数据库则按照关系(Relation)联系数据。直到有人提出关系可以用对象来表达,于是就可以用面向对象的语法,来操作关系型数据库了。这就是对象/关系映射(Object/Relational Mapping,简称ORM)。下面就是这个映射关系:
| 面向对象 | 关系型数据库 |
|---|---|
| 类(Class) | 表(Table) |
| 对象(Object) | 记录(Row) |
| 属性(Attribute) | 记录值(Column) |
假设有一个数据库如下
| ID | Name | Score |
|---|---|---|
| 0 | Chen | 60 |
| 1 | Limz | 59 |
原本查询语句要这么写:
var name string
var score int
db.QueryRow("SELECT (Name,Score) WHERE ID = ?", 0).Scan(&name, &score)
fmt.Printf("%s's score is %d", name, score)
而如果用ORM,则不需要自己编写SQL语句:
var s Student
db.Where("ID = ?", 0).First(&s)
fmt.Printf("%s's score is %d", s.Name, s.Score)
看到这里ORM的好处不言而喻(如果还不明白,之后就明白了)。而Gorm就是Go语言里超棒的一款ORM库,我们的系统将基于它开发。
现在就明白了,Web框架用Gin,数据库一直在用MySQL,操作数据库用Gorm。而要做的功能还没给大家讲清楚。
我们的最终目的是允许管理员发送邮件给想进群的人,方式是让管理员填写一份邀请表单并提交给服务器,服务器根据邀请表单和预先写好的邀请邮件模版自动生成邮件,并发送给想进群的人。
也就是说,我们的Web服务器的核心功能是接收管理员发送的POST请求,并把数据传给模版引擎html/template,生成最终的邮件并发送。问题是,如何发送邮件?
经过一番研究,发现用一个第三方库gopkg.in/gomail.v2可以快速简单地解决发送邮件的问题,虽然标准库也有net/stmp用于收发邮件,但是文档中赫然写着:
The smtp package is frozen and is not accepting new features. Some external packages provide more functionality. See:
而且某群友折腾半天,仍然无法使标准库正常工作,遂放弃。
func sendMail(mail string){
m := gomail.NewMessage()
m.SetHeader("From", "alex@example.com")
m.SetHeader("To", "bob@example.com")
m.SetHeader("Subject", "主题")
m.SetBody("text/html", "Hello <b>Bob</b>!")
d := gomail.NewDialer("smtp.example.com", 587, "user", "123456")
// Send the email to Bob
if err := d.DialAndSend(m); err != nil {
panic(err)
}
}
这个库用法可以说是简单明了,先构造一条Message,然后用Dialer发送出去。非常简单易用,用标准库时出现的奇奇怪怪的问题也都解决掉了。
发送邮件的问题解决掉了,但是邮件从哪来呢?
graph LR
temp{{模版}} -->|解析| render[模版引擎]
form[/表单/] -->|输入| render[模版引擎]
render -->|渲染| mail(邮件)
这里当然还是选用标准库的模版引擎html/template,它是text/template的升级版,为html提供了一些特别的处理。模版引擎的使用方法是,启动时预先解析模版邮件,来一份表单执行一次模版,线性复杂度。
var mailTmpl = template.Must(template.ParseFiles("tmpl/mail.tmpl"))
func render(data MailData) (string, error) {
var buf strings.Builder
if err := mailTmpl.Execute(&buf, data); err != nil {
return "", err
}
return buf.String(), nil
}
至此,关于提交表单之后的全部操作已经介绍完了,接下来到了搭建基础Web服务的时候了。Gin的使用非常简单:
func main() {
r := gin.Default()
// 配置路由
r.GET("/invite", func(c *gin.Context) {
// 操作界面
})
r.POST("/mail", func(c *gin.Context) {
// 发送接口
})
// 运行
r.Run()
}
目前让其接收两种请求,在/invite下的GET请求,提供让管理员访问的操作页面,展示一张表单让管理员填写。填写完毕后表单会以POST请求被发送到/mail路径,/mail会接收该表单,如前所述渲染并发送邮件。
/invite
管理员能访问到的页面,自然是要用html编写。再辅以轻度的css样式,即可达到简洁而又实用,清新而不失优雅的效果。ʕ •ᴥ•ʔ
<!DOCTYPE html>
<html lang="zh">
<head>
<title>喵喵公馆邀请邮件发送页面</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>/* 此处省略很多很多css */</style>
</head>
<body>
<form action="mail" method="post">
<table><!-- 此处省略很多很多<input> --></table>
</form>
</body>
</html>
其实从原理上讲,不需要任何Javascript即可实现所要的功能。不过最后实际做出来的版本,使用了XMLHttpRequest技术,不仅可以防止意外重复提交,还能让提交的体验更丝滑ε-(´∀`; )。
将这个文件保存为invite.tmpl,并保存到所有模版文件所在的目录tmpl/下,然后在main函数内加载所有模版:
r.LoadHTMLGlob("tmpl/*.tmpl")
最后,将/invite的访问全部用这个模版来处理:
r.GET("/invite", func(c *gin.Context) {
c.HTML(http.StatusOK, "invite.tmpl", nil)
})
如此一来,管理员就已经可以正常访问/invite页面了,但是他们会吃惊地发现,即使再怎么点提交按钮,也没法成功发送邮件。为了不让管理员们生气,赶紧来把发邮件的接口写好吧!
其实说难也不难,只要把前面说的渲染邮件那套代码放进来就可以了
type Mail struct {
Name string `form:"name"`
Mail string `form:"mail"`
Score string `form:"score"`
Version string `form:"version"` // 问卷版本
Stat string `form:"status"` // 是否通过 Pass/Remain/Fail
Postscript string `form:"postscript"` // 附言
Sender string `form:"sender"` // 管理员名
Date time.Time
}
r.POST("/mail", func(c *gin.Context) {
// 解析表单
var data Mail
if err := c.Bind(&data); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"status": "error",
"error": "bind form error",
})
return
}
data.Date = time.Now()
// 渲染邮件
if err := mailTmpl.Execute(&buf, data); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"status": "error",
"error": "execute template error",
})
return
}
// 发送邮件
if err := sendMail(buf.String()); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"status": "error",
"error": "send e-mail error",
})
return
}
c.JSON(http.StatusOK, gin.H{
"status": "ok",
})
})
小结
至此,这个系统已经初步完成,但也仅仅处于能用的地步,而安全则半点都谈不上,因为任何人都可以访问/invite来发邮件。后面到文章会讲到如何给这个系统加入授权系统。得益于Gin的优秀设计,加入一个中间件在/invite和/mail之前用于鉴权轻而易举。
敬请期待。