## 編寫一個 Web 服務
> Web 網頁其實也是一個 Console 命令行,只是在 Command 中啟動了一個 gin 服務器,并將配置好的路由傳入服務器中執行而已
首先我們使用 `mix` 命令創建一個 Web 項目骨架:
~~~
mix web --name=hello
~~~
通過前面我們對 Console 命令行程序結構的了解,我們先看一下骨架的 `manifest/commands` 目錄配置的命令:
~~~
package commands
import (
"github.com/mix-go/console"
"github.com/mix-go/web-skeleton/commands"
)
var (
Commands []console.CommandDefinition
)
func init() {
Commands = append(Commands,
console.CommandDefinition{
Name: "web",
Usage: "\tStart the api server",
Options: []console.OptionDefinition{
{
Names: []string{"a", "addr"},
Usage: "\tListen to the specified address",
},
{
Names: []string{"d", "daemon"},
Usage: "\tRun in the background",
},
},
Command: &commands.WebCommand{},
},
)
}
~~~
從上面我們可以看到定義了一個名稱為 `api` 的命令,關聯的是 `commands.WebCommand` 結構體,然后我們打開骨架 `commands/web.go` 的源碼查看他:
- WebCommand 結構體中啟動了一個 gin 服務器
- 并且設置 logrus 為服務器的日志組件
- 還捕獲信號,做了服務器的 Shutdown 處理
- 代碼中 `routes.RouteDefinitionCallbacks` 定義了全部的路由配置,只需修改這個全局變量即可擴展其他接口
- `router.LoadHTMLGlob` 提前讀取了全部視圖模板文件
- `router.Static` 設置了靜態文件的處理
~~~
package commands
import (
"context"
"fmt"
gin2 "github.com/gin-gonic/gin"
"github.com/mix-go/console"
"github.com/mix-go/console/flag"
"github.com/mix-go/dotenv"
"github.com/mix-go/gin"
"github.com/mix-go/web-skeleton/globals"
"github.com/mix-go/web-skeleton/routes"
"net/http"
"os"
"os/signal"
"strings"
"syscall"
"time"
)
const Addr = ":8080"
type WebCommand struct {
}
func (t *WebCommand) Main() {
logger := globals.Logger()
// server
gin.SetMode(dotenv.Getenv("GIN_MODE").String(gin.ReleaseMode))
router := gin.New(routes.RouteDefinitionCallbacks...)
srv := &http.Server{
Addr: flag.Match("a", "addr").String(Addr),
Handler: router,
}
// signal
ch := make(chan os.Signal)
signal.Notify(ch, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-ch
logger.Info("Server shutdown")
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
globals.Logger().Errorf("Server shutdown error: %s", err)
}
}()
// error handle
router.Use(gin2.Recovery())
// logger
router.Use(gin.LoggerWithFormatter(logger, func(params gin.LogFormatterParams) string {
return fmt.Sprintf("%s|%s|%d|%s",
params.Method,
params.Path,
params.StatusCode,
params.ClientIP,
)
}))
// templates
router.LoadHTMLGlob(fmt.Sprintf("%s/../templates/*", console.App.BasePath))
// static file
router.Static("/static", fmt.Sprintf("%s/../public/static", console.App.BasePath))
router.StaticFile("/favicon.ico", fmt.Sprintf("%s/../public/favicon.ico", console.App.BasePath))
// run
welcome()
logger.Info("Server start")
if err := srv.ListenAndServe(); err != nil && !strings.Contains(err.Error(), "http: Server closed") {
panic(err)
}
}
~~~
因為骨架中已經處理了基本的常用邏輯,所以我們無需修改這個文件,只需修改 `api.RouteDefinitionCallbacks` 定義的路由,該全局變量在 `routes/all.go` 文件中:
- 我們在路由配置中增加一個 `users/add` 的路由,由于新增用戶需要登錄才可操作,因此這里增加了 `middleware.SessionMiddleware()` 中間件在前面,并且使用 `router.Any` 接收全部類型的請求。
~~~
router.Any("users/add",
middleware.SessionMiddleware(),
func(ctx *gin.Context) {
user := controllers.UserController{}
user.Add(ctx)
},
)
~~~
然后創建一個 `controllers.UserController` 結構體,文件路徑為 `controllers/user.go`:
- 代碼中當請求為 GET 時,渲染 `user_add.tmpl` 模板并傳入參數,當為 POST 時使用 gorm 在 users 表中插入了一個新記錄。
~~~
package controllers
import (
"github.com/gin-gonic/gin"
"github.com/mix-go/web-skeleton/globals"
"github.com/mix-go/web-skeleton/models"
"net/http"
"time"
)
type UserController struct {
}
func (t *UserController) Add(c *gin.Context) {
// 網頁
if c.Request.Method == http.MethodGet {
c.HTML(http.StatusOK, "user_add.tmpl", gin.H{
"title": "User add",
})
c.Abort()
return
}
db := globals.DB()
if err := db.Create(&models.User{
Name: c.Request.PostFormValue("name"),
CreateAt: time.Now(),
}).Error; err != nil {
c.String(http.StatusInternalServerError, "<html><h1>%s</h1></html>", "Add error!")
c.Abort()
return
}
c.String(http.StatusInternalServerError, "<html><h1>%s</h1></html>", "Add ok!")
}
~~~
上面的代碼中使用了 `models.User` 模型,該文件定義在 `models/users.go`:
- 結構體中的備注指定了字段關聯的數據庫字段名稱,表名可自行增加前綴等
~~~
package models
import "time"
type User struct {
ID int `gorm:"primary_key"`
Name string `gorm:"column:name"`
CreateAt time.Time `gorm:"column:create_at"`
}
func (User) TableName() string {
return "users"
}
~~~
上面使用的 `globals.DB()` 都是骨架中定義好的全局方法,方法內部是采用 `mix-go/bean` 庫的依賴注入容器獲取的全局 GORM 實例,改實例的依賴配置在 `manifest/beans/db.go` 文件中:
- 文件中的依賴配置定義了使用 `gorm.Open` 實例化,`bean.SINGLETON` 定義了這個實例化后的對象是單例模式,`ConstructorArgs` 字段定義了實例化時傳入的構造參數,這里傳入的 `DATABASE_DSN` 是從環境變量中獲取的,也就是說如果我們要修改連接信息,我們還需要到 `.env` 環境配置文件中修改。
~~~
package beans
import (
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/mysql"
"github.com/mix-go/bean"
"github.com/mix-go/dotenv"
)
func DB() {
Beans = append(Beans,
bean.BeanDefinition{
Name: "db",
Reflect: bean.NewReflect(gorm.Open),
Scope: bean.SINGLETON,
ConstructorArgs: bean.ConstructorArgs{"mysql", dotenv.Getenv("DATABASE_DSN").String()},
},
)
}
~~~
## 編譯與測試
> 也可以在 Goland Run 里配置 Program arguments 直接編譯執行,[Goland 使用] 章節有詳細介紹
接下來我們編譯上面的程序:
~~~
// linux & macOS
go build -o bin/go_build_main_go main.go
// win
go build -o bin/go_build_main_go.exe main.go
~~~
首先在命令行啟動 `web` 服務器:
~~~
$ bin/go_build_main_go web
___
______ ___ _ /__ ___ _____ ______
/ __ `__ \/ /\ \/ /__ __ `/ __ \
/ / / / / / / /\ \/ _ /_/ // /_/ /
/_/ /_/ /_/_/ /_/\_\ \__, / \____/
/____/
Server Name: mix-web
Listen Addr: :8080
System Name: darwin
Go Version: 1.13.4
Framework Version: 1.0.9
time=2020-09-16 20:24:41.515 level=info msg=Server start file=web.go:58
~~~
瀏覽器測試
- 首先瀏覽器進入 http://127.0.0.1:8080/login 獲取 session

- 提交表單后跳轉到 http://127.0.0.1:8080/users/add 頁面
