04-Web-Form
上两章我们讨论了Template,本章节我们继续讨论 Web 表单
Web表单是所有Web应用程序中最基本的组成部分之一。 本章我们将使用表单来为用户发表动态和登录认证提供途径。
本章的GitHub链接为 Source, Diff, Zip

结构优化

上一章我们通过 模板继承 以及 PopulateTemplates 对 templates 文件夹进行了梳理,在继续开始Web表单之前,我们再来梳理下其中 Go 代码的结构。
目前我们的所有的逻辑都集中在了 main.go 文件里,包括 model struct, viewmodel struct 定义,还有 handler 的实现,对于小点的web应用程序,可能这有助于快速开发,不过随着项目的扩大,代码量会变得越来越多,越来越臃肿,最后甚至会影响到阅读代码。这个时候结构的优化就显得尤为重要了。
我们的思路是 建立这样的数据结构:
    package model - 负责数据建模(以及后一章 数据库 ORM)
    package vm - 负责View Model
    package controller - 负责 http 路由
每个文件夹下的 g.go 负责存放该package的全局变量 以及 init 函数。( 只能说 类似 Python 的 __init__.py, 因为 Go 其实是通过大小写来表明是否可以外部引用, 不像Python,一定要 import 到 __init__.py 文件里才能通过 package 名来引用。)
我们先建立model 文件夹,然后将 Post、User struct 分别移到 model 文件夹下
model/user.go
1
package model
2
3
// User struct
4
type User struct {
5
Username string
6
}
Copied!
model/post.go
1
package model
2
3
// Post struct
4
type Post struct {
5
User
6
Body string
7
}
Copied!
再将 view model 移到 vm文件夹下
vm/g.go
1
package vm
2
3
// BaseViewModel struct
4
type BaseViewModel struct {
5
Title string
6
}
7
8
// SetTitle func
9
func (v *BaseViewModel) SetTitle(title string) {
10
v.Title = title
11
}
Copied!
由于_base.html 基础模板中有 Title 字段,所以 Title是每个view都必有的字段,我们将它单独设成个 BaseViewStruct,方便用 匿名组合
vm/index.go
1
package vm
2
3
import "github.com/bonfy/go-mega-code/model"
4
5
// IndexViewModel struct
6
type IndexViewModel struct {
7
BaseViewModel
8
model.User
9
Posts []model.Post
10
}
11
12
// IndexViewModelOp struct
13
type IndexViewModelOp struct{}
14
15
// GetVM func
16
func (IndexViewModelOp) GetVM() IndexViewModel {
17
u1 := model.User{Username: "bonfy"}
18
u2 := model.User{Username: "rene"}
19
20
posts := []model.Post{
21
model.Post{User: u1, Body: "Beautiful day in Portland!"},
22
model.Post{User: u2, Body: "The Avengers movie was so cool!"},
23
}
24
25
v := IndexViewModel{BaseViewModel{Title: "Homepage"}, u1, posts}
26
return v
27
}
Copied!
将所有的路由相关移到controller
utils.go 存放 辅助工具函数,一般都是本package引用,所以小写就可以了, 这里PopulateTemplates 函数其实最好是小写,不过不去管它了。
controller/utils.go
1
package controller
2
3
import (
4
"html/template"
5
"io/ioutil"
6
"os"
7
)
8
9
// PopulateTemplates func
10
// Create map template name to template.Template
11
func PopulateTemplates() map[string]*template.Template {
12
const basePath = "templates"
13
result := make(map[string]*template.Template)
14
15
layout := template.Must(template.ParseFiles(basePath + "/_base.html"))
16
dir, err := os.Open(basePath + "/content")
17
if err != nil {
18
panic("Failed to open template blocks directory: " + err.Error())
19
}
20
fis, err := dir.Readdir(-1)
21
if err != nil {
22
panic("Failed to read contents of content directory: " + err.Error())
23
}
24
for _, fi := range fis {
25
f, err := os.Open(basePath + "/content/" + fi.Name())
26
if err != nil {
27
panic("Failed to open template '" + fi.Name() + "'")
28
}
29
content, err := ioutil.ReadAll(f)
30
if err != nil {
31
panic("Failed to read content from file '" + fi.Name() + "'")
32
}
33
f.Close()
34
tmpl := template.Must(layout.Clone())
35
_, err = tmpl.Parse(string(content))
36
if err != nil {
37
panic("Failed to parse contents of '" + fi.Name() + "' as template")
38
}
39
result[fi.Name()] = tmpl
40
}
41
return result
42
}
Copied!
controller/g.go
1
package controller
2
3
import "html/template"
4
5
var (
6
homeController home
7
templates map[string]*template.Template
8
)
9
10
func init() {
11
templates = PopulateTemplates()
12
}
13
14
// Startup func
15
func Startup() {
16
homeController.registerRoutes()
17
}
Copied!
controller/home.go
1
package controller
2
3
import (
4
"net/http"
5
6
"github.com/bonfy/go-mega-code/vm"
7
)
8
9
type home struct{}
10
11
func (h home) registerRoutes() {
12
http.HandleFunc("/", indexHandler)
13
}
14
15
func indexHandler(w http.ResponseWriter, r *http.Request) {
16
vop := vm.IndexViewModelOp{}
17
v := vop.GetVM()
18
templates["index.html"].Execute(w, &v)
19
}
Copied!
这里将 匿名函数 实名成 indexHandler,并将所有的构造 indexviewmodel 的逻辑全部移到了 vm/index.go 中的 GetVM 方法里
最终我们的结构优化成了下图的树状结构,这样有利于我们以后的扩展。
1
go-mega-code
2
├── controller
3
│ ├── g.go
4
│ ├── home.go
5
│ └── utils.go
6
├── main.go
7
├── model
8
│ ├── post.go
9
│ └── user.go
10
├── templates
11
│ ├── _base.html
12
│ └── content
13
│ └── index.html
14
└── vm
15
├── g.go
16
└── index.go
Copied!
本小节 Diff

用户登录表单

在将整个项目优化结构之后,我们建立登陆表单就非常简单了。
按照 index 的做法,login表单 我们其实需要,一个 template, 一个 vm, 以及一个 handler(其实后面基本上所有的加页面的做法也是类似)
templates/_base.html
1
...
2
<div>
3
Blog:
4
<a href="/">Home</a>
5
<a href="/login">Login</a>
6
</div>
7
...
Copied!
templates/content/login.html
1
{{define "content"}}
2
<h1>Login</h1>
3
<form action="/login" method="post" name="login">
4
<p><input type="text" name="username" value="" placeholder="Username or Email"></p>
5
<p><input type="password" name="password" value="" placeholder="Password"></p>
6
<p><input type="submit" name="submit" value="Login"></p>
7
</form>
8
{{end}}
Copied!
login.html 还是继承 _base.html 只要关注 content 的内容就行了
vm/login.go
1
package vm
2
3
// LoginViewModel struct
4
type LoginViewModel struct {
5
BaseViewModel
6
}
7
8
// LoginViewModelOp strutc
9
type LoginViewModelOp struct{}
10
11
// GetVM func
12
func (LoginViewModelOp) GetVM() LoginViewModel {
13
v := LoginViewModel{}
14
v.SetTitle("Login")
15
return v
16
}
Copied!
这里 v.SetTitle 就是用了 匿名组合 的特性,继承了 BaseViewModel 的 SetTitle 方法
controller/home.go 中加入 loginHandler
controller/home.go
1
func (h home) registerRoutes() {
2
...
3
http.HandleFunc("/login", loginHandler)
4
}
5
6
...
7
8
func loginHandler(w http.ResponseWriter, r *http.Request) {
9
tpName := "login.html"
10
vop := vm.LoginViewModelOp{}
11
v := vop.GetVM()
12
templates[tpName].Execute(w, &v)
13
}
14
15
...
Copied!
此时,你可以验证结果了, 运行该应用,在浏览器的地址栏中输入 http://localhost:8888/然后点击顶部导航栏中的Login链接来查看新的登录表单。
本小节 Diff

接收表单数据

目前我们点击 Login 按钮,页面发现没有变化,其实我们后台还是接收到了这个请求,只是依然返回的是这个页面,接下来我们要对POST请求和GET请求分别做处理。
controller/home.go
1
...
2
3
func loginHandler(w http.ResponseWriter, r *http.Request) {
4
tpName := "login.html"
5
vop := vm.IndexViewModelOp{}
6
v := vop.GetVM()
7
if r.Method == http.MethodGet {
8
templates[tpName].Execute(w, &v)
9
}
10
if r.Method == http.MethodPost {
11
r.ParseForm()
12
username := r.Form.Get("username")
13
password := r.Form.Get("password")
14
fmt.Fprintf(w, "Username:%s Password:%s", username, password)
15
}
16
}
Copied!
Tip: html 中的form submit 是 Post 方法,简单说明下,不过相信大家都懂
修改 loginHandler 对 MethodGetMethodPost 分别处理,MethodPost 接受 form post,运行后在login页面输入用户名、密码 点击Login,显示结果
本小节 Diff

表单后端验证

表单验证分为服务器前端验证 与 后端验证,比如验证 输入字符个数、正则匹配等,一般来说 用户名密码正确性检查只能在后端验证,其它前后端验证都可以,不过为了减少服务器压力与加强用户体验,字符长度等检查 一般放在前端做检查。
由于本教程主要是 Go 后端 web教程,这里简单的做一个 后端检查 的示例
LoginModelView里加入 Errs 字段,用于输出检查的错误返回
vm/login.go
1
...
2
3
type LoginViewModel struct {
4
BaseViewModel
5
Errs []string
6
}
7
8
// AddError func
9
func (v *LoginViewModel) AddError(errs ...string) {
10
v.Errs = append(v.Errs, errs...)
11
}
12
...
Copied!
login.html 中加入判断是否有错误,以及错误输出
templates/content/login.html
1
...
2
{{if .Errs}}
3
<ul>
4
{{range .Errs}}
5
<li>{{.}}</li>
6
{{end}}
7
</ul>
8
{{end}}
9
...
Copied!
controller/home.go
1
...
2
3
func check(username, password string) bool {
4
if username == "bonfy" && password == "abc123" {
5
return true
6
}
7
return false
8
}
9
10
func loginHandler(w http.ResponseWriter, r *http.Request) {
11
tpName := "login.html"
12
vop := vm.LoginViewModelOp{}
13
v := vop.GetVM()
14
15
if r.Method == http.MethodGet {
16
templates[tpName].Execute(w, &v)
17
}
18
if r.Method == http.MethodPost {
19
r.ParseForm()
20
username := r.Form.Get("username")
21
password := r.Form.Get("password")
22
23
if len(username) < 3 {
24
v.AddError("username must longer than 3")
25
}
26
27
if len(password) < 6 {
28
v.AddError("password must longer than 6")
29
}
30
31
if !check(username, password) {
32
v.AddError("username password not correct, please input again")
33
}
34
35
if len(v.Errs) > 0 {
36
templates[tpName].Execute(w, &v)
37
} else {
38
http.Redirect(w, r, "/", http.StatusSeeOther)
39
}
40
}
41
}
Copied!
再次运行,在login页面随便输入用户名密码,如果不符合规范,就会有后端验证提示你重新输入。
本小节 Diff
Notice: 显示 Error , Flask-Mega 教程里面用的是 flash (flash-messages),我们这边是将错误信息直接 render 到了页面上,其实也是可以用 flash 的,不过 Go 的 Session 目前没有特别好的第三方插件,我们现在还没有用到第三方包,就用了原生的渲染。后续集成Session之后会有例子用到flash。先在此说明下,耐心往下看。

Links