• 04-Web Form
    • 结构优化
    • 用户登录表单
    • 接收表单数据
    • 表单后端验证
    • Links

    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. // User struct
    3. type User struct {
    4. Username string
    5. }

    model/post.go

    1. package model
    2. // Post struct
    3. type Post struct {
    4. User
    5. Body string
    6. }

    再将 view model 移到 vm文件夹下

    vm/g.go

    1. package vm
    2. // BaseViewModel struct
    3. type BaseViewModel struct {
    4. Title string
    5. }
    6. // SetTitle func
    7. func (v *BaseViewModel) SetTitle(title string) {
    8. v.Title = title
    9. }

    由于_base.html 基础模板中有 Title 字段,所以 Title是每个view都必有的字段,我们将它单独设成个 BaseViewStruct,方便用 匿名组合

    vm/index.go

    1. package vm
    2. import "github.com/bonfy/go-mega-code/model"
    3. // IndexViewModel struct
    4. type IndexViewModel struct {
    5. BaseViewModel
    6. model.User
    7. Posts []model.Post
    8. }
    9. // IndexViewModelOp struct
    10. type IndexViewModelOp struct{}
    11. // GetVM func
    12. func (IndexViewModelOp) GetVM() IndexViewModel {
    13. u1 := model.User{Username: "bonfy"}
    14. u2 := model.User{Username: "rene"}
    15. posts := []model.Post{
    16. model.Post{User: u1, Body: "Beautiful day in Portland!"},
    17. model.Post{User: u2, Body: "The Avengers movie was so cool!"},
    18. }
    19. v := IndexViewModel{BaseViewModel{Title: "Homepage"}, u1, posts}
    20. return v
    21. }

    将所有的路由相关移到controller

    utils.go 存放 辅助工具函数,一般都是本package引用,所以小写就可以了, 这里PopulateTemplates 函数其实最好是小写,不过不去管它了。

    controller/utils.go

    1. package controller
    2. import (
    3. "html/template"
    4. "io/ioutil"
    5. "os"
    6. )
    7. // PopulateTemplates func
    8. // Create map template name to template.Template
    9. func PopulateTemplates() map[string]*template.Template {
    10. const basePath = "templates"
    11. result := make(map[string]*template.Template)
    12. layout := template.Must(template.ParseFiles(basePath + "/_base.html"))
    13. dir, err := os.Open(basePath + "/content")
    14. if err != nil {
    15. panic("Failed to open template blocks directory: " + err.Error())
    16. }
    17. fis, err := dir.Readdir(-1)
    18. if err != nil {
    19. panic("Failed to read contents of content directory: " + err.Error())
    20. }
    21. for _, fi := range fis {
    22. f, err := os.Open(basePath + "/content/" + fi.Name())
    23. if err != nil {
    24. panic("Failed to open template '" + fi.Name() + "'")
    25. }
    26. content, err := ioutil.ReadAll(f)
    27. if err != nil {
    28. panic("Failed to read content from file '" + fi.Name() + "'")
    29. }
    30. f.Close()
    31. tmpl := template.Must(layout.Clone())
    32. _, err = tmpl.Parse(string(content))
    33. if err != nil {
    34. panic("Failed to parse contents of '" + fi.Name() + "' as template")
    35. }
    36. result[fi.Name()] = tmpl
    37. }
    38. return result
    39. }

    controller/g.go

    1. package controller
    2. import "html/template"
    3. var (
    4. homeController home
    5. templates map[string]*template.Template
    6. )
    7. func init() {
    8. templates = PopulateTemplates()
    9. }
    10. // Startup func
    11. func Startup() {
    12. homeController.registerRoutes()
    13. }

    controller/home.go

    1. package controller
    2. import (
    3. "net/http"
    4. "github.com/bonfy/go-mega-code/vm"
    5. )
    6. type home struct{}
    7. func (h home) registerRoutes() {
    8. http.HandleFunc("/", indexHandler)
    9. }
    10. func indexHandler(w http.ResponseWriter, r *http.Request) {
    11. vop := vm.IndexViewModelOp{}
    12. v := vop.GetVM()
    13. templates["index.html"].Execute(w, &v)
    14. }

    这里将 匿名函数 实名成 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

    本小节 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. ...

    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}}

    login.html 还是继承 _base.html 只要关注 content 的内容就行了

    vm/login.go

    1. package vm
    2. // LoginViewModel struct
    3. type LoginViewModel struct {
    4. BaseViewModel
    5. }
    6. // LoginViewModelOp strutc
    7. type LoginViewModelOp struct{}
    8. // GetVM func
    9. func (LoginViewModelOp) GetVM() LoginViewModel {
    10. v := LoginViewModel{}
    11. v.SetTitle("Login")
    12. return v
    13. }

    这里 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. func loginHandler(w http.ResponseWriter, r *http.Request) {
    7. tpName := "login.html"
    8. vop := vm.LoginViewModelOp{}
    9. v := vop.GetVM()
    10. templates[tpName].Execute(w, &v)
    11. }
    12. ...

    此时,你可以验证结果了, 运行该应用,在浏览器的地址栏中输入 http://localhost:8888/然后点击顶部导航栏中的Login链接来查看新的登录表单。

    04-Web-Form - 图1

    本小节 Diff

    接收表单数据

    目前我们点击 Login 按钮,页面发现没有变化,其实我们后台还是接收到了这个请求,只是依然返回的是这个页面,接下来我们要对POST请求和GET请求分别做处理。

    controller/home.go

    1. ...
    2. func loginHandler(w http.ResponseWriter, r *http.Request) {
    3. tpName := "login.html"
    4. vop := vm.IndexViewModelOp{}
    5. v := vop.GetVM()
    6. if r.Method == http.MethodGet {
    7. templates[tpName].Execute(w, &v)
    8. }
    9. if r.Method == http.MethodPost {
    10. r.ParseForm()
    11. username := r.Form.Get("username")
    12. password := r.Form.Get("password")
    13. fmt.Fprintf(w, "Username:%s Password:%s", username, password)
    14. }
    15. }

    Tip: html 中的form submit 是 Post 方法,简单说明下,不过相信大家都懂

    修改 loginHandler 对 MethodGetMethodPost 分别处理,MethodPost 接受 form post,运行后在login页面输入用户名、密码 点击Login,显示结果

    04-Web-Form - 图2

    本小节 Diff

    表单后端验证

    表单验证分为服务器前端验证 与 后端验证,比如验证 输入字符个数、正则匹配等,一般来说 用户名密码正确性检查只能在后端验证,其它前后端验证都可以,不过为了减少服务器压力与加强用户体验,字符长度等检查 一般放在前端做检查。

    由于本教程主要是 Go 后端 web教程,这里简单的做一个 后端检查 的示例

    LoginModelView里加入 Errs 字段,用于输出检查的错误返回

    vm/login.go

    1. ...
    2. type LoginViewModel struct {
    3. BaseViewModel
    4. Errs []string
    5. }
    6. // AddError func
    7. func (v *LoginViewModel) AddError(errs ...string) {
    8. v.Errs = append(v.Errs, errs...)
    9. }
    10. ...

    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. ...

    controller/home.go

    1. ...
    2. func check(username, password string) bool {
    3. if username == "bonfy" && password == "abc123" {
    4. return true
    5. }
    6. return false
    7. }
    8. func loginHandler(w http.ResponseWriter, r *http.Request) {
    9. tpName := "login.html"
    10. vop := vm.LoginViewModelOp{}
    11. v := vop.GetVM()
    12. if r.Method == http.MethodGet {
    13. templates[tpName].Execute(w, &v)
    14. }
    15. if r.Method == http.MethodPost {
    16. r.ParseForm()
    17. username := r.Form.Get("username")
    18. password := r.Form.Get("password")
    19. if len(username) < 3 {
    20. v.AddError("username must longer than 3")
    21. }
    22. if len(password) < 6 {
    23. v.AddError("password must longer than 6")
    24. }
    25. if !check(username, password) {
    26. v.AddError("username password not correct, please input again")
    27. }
    28. if len(v.Errs) > 0 {
    29. templates[tpName].Execute(w, &v)
    30. } else {
    31. http.Redirect(w, r, "/", http.StatusSeeOther)
    32. }
    33. }
    34. }

    再次运行,在login页面随便输入用户名密码,如果不符合规范,就会有后端验证提示你重新输入。

    04-Web-Form - 图3

    本小节 Diff

    Notice: 显示 Error , Flask-Mega 教程里面用的是 flash (flash-messages),我们这边是将错误信息直接 render 到了页面上,其实也是可以用 flash 的,不过 Go 的 Session 目前没有特别好的第三方插件,我们现在还没有用到第三方包,就用了原生的渲染。后续集成Session之后会有例子用到flash。先在此说明下,耐心往下看。

    • 目录
    • 上一节: 03-Template-Advance
    • 下一节: 05-Database