丁丁 软硬件、前后端全栈开发者。热爱,并将终身学习有关计算机的一切 mdi-home 首页 mdi-language-go Golang mdi-cpu-32-bit STM32 mdi-format-list-bulleted-square 文章列表 mdi-share-variant 分享 mdi-book-open-page-variant 推荐书单 mdi-chat-processing 碎语 mdi-help-box ISSUE About Me mdi-message 知乎 mdi-sina-weibo 微博 mdi-television-play bilibili mdi-rss-box RSS

gin使用实战之--模板文件存入数据库且热加载——丁丁的个人网站

mdi-heart mdi-login mdi-logout mdi-settings
mdi-chevron-left Last:跨平台终端神器Tabby Next:ubuntu20.04安装MySQL纪实 mdi-chevron-right
# gin简介 Golang中的gin是一个非常强大而常用的web引擎,我们很多项目都是使用gin来构建的。 前台系统一般需要考虑SEO,我不太习惯基于nodejs的vue-ssr,通常我会使用gin的template功能来实现,类似于JAVA中的freemarker,当然,它的模板引擎是Golang自带的,而不需要自己手撸,这一点Golang真的太爽了,内置SDK简直就是军火库。 后台系统一般使用vue,这时候可以使用gin的RouteGroup,Get,Post,Parse等功能来舒服地设计restful API。 # gin template常规用法 一个非常常见的场景,使用`c.HTML(200, "a.tmpl", obj)`来返回一个被obj对象渲染了的`a.tmpl`模板的html给客户。obj可以有很多属性,甚至是一个map,来将内部的数据渲染到html中,比如“文章”的加载。 在这样使用之前,我们需要为gin配置template,按照官方文档的demo,我们可以这样配置: ```go func main() { router := gin.Default() router.LoadHTMLGlob("templates/*") //router.LoadHTMLFiles("templates/template1.html", "templates/template2.html") router.GET("/index", func(c *gin.Context) { c.HTML(http.StatusOK, "index.tmpl", gin.H{ "title": "Main website", }) }) router.Run(":8080") } ``` 其中第一种用法`router.LoadHTMLGlob("templates/*")`是加载相对路径`./templates`下所有的一级文件,比如`/templates/index.tmpl`等。第二种用法`router.LoadHTMLFiles("templates/template1.html", "templates/template2.html")`,是分别加载指定的文件。 # 场景和痛点 考虑一个简单的CMS系统场景,系统分为前后台,前台使用gin的template渲染数据,后台使用vue编写。 要求:前台系统的渲染,可以通过后台系统动态调整模板代码,并且热加载。 在实现这个简单的功能时,我遇到了一些关于gin使用template的痛点,记录如下: ## 痛点1:模板文件路径 ### 描述 如上面所说,`router.LoadHTMLGlob("templates/*")`可以加载相对路径`./templates`下所有的一级文件,但是它无法加载诸如`./templates/components/banner.tmpl`这种子目录下的文件。如果我们的项目模板文件夹下就这么干巴巴的一层路径,那显然不太具有层次感,显得文件组织相当杂乱。 或许你会发现无法加载子路径下的模板文件,是因为`router.LoadHTMLGlob("templates/*")`参数里的正则引起的。我们可以换成`router.LoadHTMLGlob("templates/**/*")`试试,结果会发现,这种写法,可以加载`templates/`下所有二级文件,比如`./templates/components/banner.tmpl`,或者`./templates/products/p1.tmpl`,但是却无法加载根目录下的`/templates/index.tmpl`文件了。 ### 解决 这个问题我们可以使用golang自带的filepath包来解决,使用它来遍历所有的`.templates/`文件夹下的模板文件,并使用`router.LoadHTMLFiles()`函数分别加载。 ```go // load templates in any deep director     templateFiles := make([]string, 0, 32)     filepath.Walk("./templates", func(path string, info os.FileInfo, err error) error {         if strings.HasSuffix(path, ".html") {             templateFiles = append(templateFiles, path)         }         return nil     })     e.LoadHTMLFiles(templateFiles...) ``` ## 痛点2:模板一定要是文件吗? ### 描述 有时候,我们希望在前台系统上线后,后台系统有能力让前台系统动态更新模板,以快速调整前台页面渲染。 于是我们考虑一个问题:模板一定要是文件吗?可不可以存在于数据库中,甚至上线后还可以动态改变,即时生效。 答案是可以,参考gin的官方文件,有这样一段代码: ![](https://files.hexcode.cn/20220510175033.png) 借鉴这个demo,编写自己的模板渲染器: ```go var frontendTemplate *template.Template func InitFrontend(e *gin.Engine) { // init template frontendTemplate = nil InitFrontendTemplate() e.SetHTMLTemplate(frontendTemplate) } func InitFrontendTemplate() { // 从数据库加载模板map fmap, err := model.GetSetting("tmpl") if err != nil { panic(err) } for k, v := range fmap { err = SetFrontendTemplate(k, v) if err != nil { panic(err) } } } func SetFrontendTemplate(key string, value string) (err error) { if frontendTemplate == nil { frontendTemplate, err = template.New(key).Parse(value) } else { _, err = frontendTemplate.New(key).Parse(value) } return } ``` 以上代码使用`e.SetHTMLTemplate(frontendTemplate)`函数给gin传入一套模板`frontendTemplate`,这套模板来自于数据库,使用`SetFrontednTemplate`进行每一项数据的添加。 好了,现在gin启动时,模板的配置来自于数据库了,不再将模板数据暴露在文件系统中了。下面,问题来了。 ## 痛点3:模板文件可以热加载吗? ### 描述 在开发过程中,我们可能认为模板理所应当地可以热加载,因为在我们使用模板文件的时候,每一次修改模板文件,前端均可以做出即时的响应。那么,这是天经地义的吗? 通过Github上的提问可以看出,模板并不是每种场景下都能热加载的,gin在Debug模式下,允许模板热加载,实时生效,这是牺牲性能的模式。而在gin的Release模式下,是不允许模板热加载的,相关说明如下: ![](https://files.hexcode.cn/20220511093316.png) 模板文件的热加载尚且受制于Gin的运行模式,我们项目上线一般都会选在在Release模式下部署,如果我们的模板数据来自于数据库,想必同样存在无法热更新的问题,那么,将模板代码存入数据库的意义就丧失了,因为我们的最终目的是在后台系统中可以动态修改模板代码,实时影响到前台。 ### 解决 既然gin在Release模式下,运行时是不可以热加载模板的,那我们只有思考另一种解决办法:让前台服务能够受控于后台系统,优雅地重启! 网络上关于gin优雅地关闭,有很多文章可以借鉴,但是关于gin的热重启,很少有人提到,尤其是我这种场景:golang程序中,同时运行两个gin服务,一个backendServer,一个frontendServer,两个服务运行于不同的端口上,希望能通过backendServer的相关API,控制frontendServer的相关行为,比如重启。 #### 多个gin服务 首先介绍一下go程序中运行多个gin服务的方法,gin的官方文档有相关介绍:https://github.com/gin-gonic/gin#run-multiple-service-using-gin ![](https://files.hexcode.cn/20220511100337.png) 官方示例使用了`golang.org/x/sync/errgroup`包,来并发运行多个gin服务,我这里借鉴其思想,稍作调整: ```go func main() { frontendEngine := gin.Default() service.InitFrontend(frontendEngine) backendEngine := gin.Default() service.InitBackend(backendEngine) // 无阻塞的方式启动前台服务 frontServer := &http.Server{ Addr: utils.Config.Server.FrontPort, Handler: frontendEngine, } go func() { if err := frontServer.ListenAndServe(); err != nil { seelog.Warn(err) } }() // 无阻塞的方式启动后台服务 backServer := &http.Server{ Addr: utils.Config.Server.BackPort, Handler: backendEngine, } go func() { if err := backServer.ListenAndServe(); err != nil && errors.Is(err, http.ErrServerClosed) { seelog.Warn(err) } }() // 等待空信号,保持golang程序的运行 blank := make(chan int64) <-blank } ``` 上述代码,初始化了一个前台一个后台两个gin服务,然后通过`go`关键字并发运行这两个服务,最后通过阻塞一个空的chan让程序一直等待,不至于一运行就结束。 #### 优雅地重启前端服务 基于上述代码,我们可以很容易想象,给后端服务设置一个restful API,在这个API中,产生一个重启前端的信号,将主程序尾部的空等代码,改成循环监听这个重启信号,一旦监听到这个信号,则让前端服务器重启,从而达到重新加载所有模板数据的目的。代码如下: ```go restartChan := make(chan int64) frontendEngine := gin.Default() service.InitFrontend(frontendEngine) backendEngine := gin.Default() service.InitBackend(backendEngine) backendEngine.POST("api/restart", service.MidwarePower(service.POWERMASK_RESTART), func(c *gin.Context) { restartChan <- 1 c.Status(200) }) // ... // 跟之前的代码一致 // 等待重启信号 for range restartChan { seelog.Info("restart...") ctx, cancel := context.WithTimeout(context.Background(), time.Second*3) err := frontServer.Shutdown(ctx) if err != nil { seelog.Error(err) } // 无阻塞重新启动前端服务 go func() { time.Sleep(time.Second * 3) cancel() frontendEngine = gin.Default() service.InitFrontend(frontendEngine) frontServer = &http.Server{ Addr: utils.Config.Server.FrontPort, Handler: frontendEngine, } if err := frontServer.ListenAndServe(); err != nil { seelog.Warn(err) } else { seelog.Info("restart success.") } }() } ``` # 总结 至此,完美地解决了场景给出的要求,后台可以实时从数据库调取模板数据供管理员编辑,管理员在后台中可以编辑保存模板文件,然后通过重启前台的API,让前台服务优雅地重启,实现模板数据的实时更新。
mdi-chevron-left Last:跨平台终端神器Tabby Next:ubuntu20.04安装MySQL纪实 mdi-chevron-right
Tags JAVA Golang STM32 Links 丁丁喜欢这些网站或者博客 MCU起航 JBlog
Tags JAVA Golang STM32 Links 丁丁喜欢这些网站或者博客 MCU起航 JBlog
{{ $store.state.notice.msg }}