启动

解析启动参数

"github.com/spf13/pflag"

读取配置文件

"github.com/spf13/viper"

viper 是一个配置文件格式通吃的库,支持各种格式的配置文件。看了一下,github 上有 10000+ stars,值得使用。

根据项目的运行模式,执行不同的操作

// 打印命名路由
func printRoute() {
 // 只有非 release 时才可用该函数
 if config.AppConfig.RunMode == config.RunmodeRelease {
  return
 }

 named.PrintRoutes()
}

注入模版函数

g.SetFuncMap(template.FuncMap{
 // 根据 laravel-mix 的 public/mix-manifest.json 生成静态文件 path
 "Mix": helpers.Mix,
 // 生成项目静态文件地址
 "Static": helpers.Static,
 // 获取命名路由的 path
 "Route":         named.G,
 "RelativeRoute": named.GR,
})

pflag 从终端传递进去的命令参数,然后通过 makefile 进行一层封装封装,通过 fresh.conf 配置 fresh 命令的一些参数

APP_NAME = "gin_weibo"

default:
 go build -o ${APP_NAME}
 # env GOOS=linux GOARCH=amd64 go build -o ${APP_NAME}

install:
 go mod download

dev:
 fresh -c ./fresh.conf

mock:
 go run ./main.go -m

clean:
 if [ -f ${APP_NAME} ]; then rm ${APP_NAME}; fi

help:
 @echo "make - compile the source code"
 @echo "make install - install dep"
 @echo "make dev - run go fresh"
 @echo "make mock - mock data"
 @echo "make clean - remove binary file"

路由

响应没有路由的页面

g.NoRoute(func(c *gin.Context) {
 controllers.Render404(c)
})

抽象出针对Web和Api的路由注册方法

// web
registerWeb(g)
// api
registerApi(g)

动态路由实现

一些动态语言的框架,通过语言自身的特性等来动态获取路由地址(根据一个名字,返回地址),在 Gin 中没有内置这个特性,静态语言也不太容易实现。这里是搞了两个数组,然后注入到网页模版中,在需要时进行调用来返回地址:

var (
 // RouterMap : 存放路由 name 和它的 path map
 RouterMap = make(map[string]string, 0)
 methodMap = make(map[string]string, 0)
)

// Name : 注册路由
// 动态参数目前只支持 :xx 形式
func Name(g gin.IRouter, name string, method string, path string) {
 s := path
 if group, ok := g.(*gin.RouterGroup); ok {
  s = group.BasePath() + path
 }

 if RouterMap[name] != "" {
  panic("该路由已经命名过了: [" + name + "] " + s)
 }
 RouterMap[name] = s
 methodMap[name] = method
}

路由使用:

// ------------------------------ static page ------------------------------
{
 g.GET("/", staticpage.Home)
 // 绑定路由 path 和 路由 name,之后可通过 named.G("root") 或 named.GR("root") 获取到路由 path
 // 模板文件中可通过  或  获取 path
 named.Name(g, "root", "GET", "/")

 g.GET("/help", staticpage.Help)
 named.Name(g, "help", "GET", "/help")

 g.GET("/about", staticpage.About)
 named.Name(g, "about", "GET", "/about")
}

用户

注册及验证

用户表中有一个 activation_token 字段,当用户注册的时候,进行赋值:

// 生成用户激活 token
if u.ActivationToken == "" {
	u.ActivationToken = string(utils.RandomCreateBytes(30))
}
		
然后注册成功,给用户发邮件,邮件中包括用户一个路由链接:

g.GET("/signup/confirm/:token", wrapper.Guest(user.ConfirmEmail))

用户在点击这个链接时候,对 token 进行确认,判断是否是登录用户。

	token := c.Param("token")

	user, err := userModel.GetByActivationToken(token)
	if user == nil || err != nil {
		controllers.Render404(c)
		return
	}

	// 更新用户
	user.Activated = models.TrueTinyint
	user.ActivationToken = ""
	user.EmailVerifiedAt = time.Now()
	if err = user.Update(false); err != nil {
		flash.NewSuccessFlash(c, "用户激活失败: "+err.Error())
		controllers.RedirectRouter(c, "root")
		return
	}


密码加密

以为是加盐保存的,看了下代码是通过 bcrypt 库进行加密以及验证来实现的。

package auth

import "golang.org/x/crypto/bcrypt"

// 密码加密
func Encrypt(source string) (string, error) {
 hashedBytes, err := bcrypt.GenerateFromPassword([]byte(source), bcrypt.DefaultCost)
 return string(hashedBytes), err
}

// 密码比对 (传入未加密的密码即可)
func Compare(hashedPassword, password string) error {
 return bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
}

前端

ts 预处理

scss 预处理

flash_message 实现

保存在 Cookie 里,实现参考了 beego 的方式:

// 将 flash 数据保存到 gin context keys 中和 cookie 中
func (fd *FlashData) save(c *gin.Context, keyName string) {
 c.Keys[keyName] = fd.Data

 var flashValue string
 for key, value := range fd.Data {
  flashValue += "\x00" + key + "\x23" + FlashSeparator + "\x23" + value + "\x00"
 }
 c.SetCookie(keyName, flashValue, 0, "/", "", false, true)
}

// 从 request 中的 cookie 里解析出 flash 数据
func read(c *gin.Context, keyName string) *FlashData {
 flash := NewFlashByName(keyName)
 if cookie, err := c.Request.Cookie(keyName); err == nil {
  v, _ := url.QueryUnescape(cookie.Value)
  vals := strings.Split(v, "\x00")
  for _, v := range vals {
   if len(v) > 0 {
    kv := strings.Split(v, "\x23"+FlashSeparator+"\x23")
    if len(kv) == 2 {
     flash.Data[kv[0]] = kv[1]
    }
   }
  }
  // 读取一次即删除 (beego 里 flash 的实现方式)
  // github.com/tommy351/gin-sessions 的实现方式是,每次 save 都会保存替换所有 session,
  //    所以读取 flash 时,将 flash 从 session 对象中 delete 掉再 save 即可
  c.SetCookie(keyName, "", -1, "/", "", false, true)
 }
 c.Keys[keyName] = flash.Data
 return flash
}

在 Controllers 包中,封装一个公共方法,所有需要返回模版的请求,都从这个方法中走:

// Render : 渲染 html
func Render(c *gin.Context, tplPath string, data renderObj) {
 obj := make(renderObj)
 flashStore := flash.Read(c)
 oldValueStore := flash.ReadOldFormValue(c)
 validateMsgArr := flash.ReadValidateMessage(c)

 // flash 数据
 obj[flash.FlashInContextAndCookieKeyName] = flashStore.Data
 // 上次 post form 的数据,用于回填
 obj[flash.OldValueInContextAndCookieKeyName] = oldValueStore.Data
 // 上次表单的验证信息
 obj[flash.ValidateContextAndCookieKeyName] = validateMsgArr
 // csrf
 if config.AppConfig.EnableCsrf {
  if csrfHTML, csrfToken, ok := csrfField(c); ok {
   obj[csrfInputHTML] = csrfHTML
   obj[csrfTokenName] = csrfToken
  }
 }

 // 获取当前登录的用户 (如果用户登录了的话,中间件中会通过 session 存储用户数据)
 if user, err := auth.GetCurrentUserFromContext(c); err == nil {
  obj[config.AppConfig.ContextCurrentUserDataKey] = viewmodels.NewUserViewModelSerializer(user)
 }

 // 填充传递进来的数据
 for k, v := range data {
  obj[k] = v
 }

 c.HTML(http.StatusOK, tplPath, obj)
}

这个方法中,也会从 cookie 中读取flash message,然后返回给模版渲染引擎。这时如果对应的模板文件引入了拆分出去的 flash 子模板文件,那就可以正确显示消息:

flash message子模板:


    
      <div class="flash-message">
        <p class="alert alert-danger">
          
        </p>
      </div>
    
    
      <div class="flash-message">
        <p class="alert alert-warning">
          
        </p>
      </div>
    
    
      <div class="flash-message">
        <p class="alert alert-success">
          
        </p>
      </div>
    
    
      <div class="flash-message">
        <p class="alert alert-info">
          
        </p>
      </div>
    

子模板

依赖的引用拆分的可以细一些,比如:

  • 公共样式

    
    
    <link rel="stylesheet" href=>
    
    
    

这样使用起来更灵活。

下一篇讨论一下具体功能的实现以及数据库的设计。



blog comments powered by Disqus

Published

2019-11-09

Categories


Tags