Go Web App Example - Views, Request Workflow, and View Plugins
This is a continuation to my previous post, Go Web App Example - Entry Point, File Structure, Models, and Routes. In this post, I want to cover views (templates), request workflow, and view plugins in my web app example.
All the code and screenshots are available on GitHub: https://github.com/josephspurrier/gowebapp
For those of you that are new to the Model-View-Controller pattern (MVC), I want to touch on how models, views, and controllers work in my web app example. Everyone structures them a little differently. MVC web applications are different from MVC desktop applications. The differences have a lot to do with application states, but also how each application architect interprets the definition of MVC. That being said, I try to stick to these rules when I’m writing MVC web applications in Go:
- Models - all database queries and structs must only be in models
- Views - majority of the HTML must only be in views (templates)
- Controllers - each controller should control which view displays and which model is used
Views
We went over models in the last post so now it’s time to learn about views. Go is a very modern language and has a nice HTML templating system included in the standard library. You may want to read over the text/template package first to see all features in the package.
When I worked with PHP, I used a few of the templating frameworks in my projects. PHP is actually a templating system, but there are no rules around escaping text when and that lead to cross-site scripting (XSS) attacks. The authors of Go knew this so by default, all variables are escaped which strips elements like HTML tags.
One of the differentiators in the templating world is how much logic is allowed to be written into the templates. Some templating systems allow little to no logic while others, like PHP, allow any logic to be written into the templates. The less logic you write in templates, the easier they are to test. On the flip side, the more logic you write in the templates, the less work the controller has to do which can save you a few lines of codes.
My preference is allowing only minimal logic in templates. If I’m passing an array to a template, I want to check if the array is empty first before I iterate over it. I like writing that check in the template so I can display a “No results” message to the user. I’d rather not have two templates for the same page - one to let the user know the array is empty and the other one to display all the elements in the array. Again, that is my preference, but in Go, either is possible.
To make the views a little easier to work with, I created a package to allow me to configure settings and functions when the app first loads. I set up my templates so index.tmpl is the base template for every page that displays HTML (settable in config.json). The content is then loaded from a definition in a template that is specified by the controller.
Request Workflow
The quickest way to understand how a view is rendered to so see how a simple request works. The developer sets the template and view settings in config.json:
...
"Template": {
"Root": "index",
"Children": [
"menu",
"footer"
]
},
"View": {
"BaseURI": "/",
"Extension": "tmpl",
"Folder": "template",
"Name": "blank",
"Caching": true
}
...
When the application is started, settings are passed to the View package in gowebapp.go:
...
func main() {
// Load the configuration file
jsonconfig.Load("config"+string(os.PathSeparator)+"config.json", config)
...
// Setup the views
view.Configure(config.View)
view.LoadTemplates(config.Template.Root, config.Template.Children)
view.LoadPlugins(
plugin.TagHelper(config.View),
plugin.NoEscape(),
plugin.PrettyTime(),
recaptcha.Plugin())
// Start the listener
server.Run(route.LoadHTTP(), route.LoadHTTPS(), config.Server)
}
...
When a user requests the page, /about, the route defined in route.go specifies that controller.AboutGET() should handle the request.
...
// About
r.GET("/about", hr.Handler(alice.
New().
ThenFunc(controller.AboutGET)))
...
The controller, about.go, has the AboutGET() func.
package controller
import (
"net/http"
"github.com/josephspurrier/gowebapp/shared/view"
)
// Displays the About page
func AboutGET(w http.ResponseWriter, r *http.Request) {
// Display the view
v := view.New(r)
v.Name = "about"
v.Render(w)
}
The controller doesn’t need to access any models so it simply creates a new instance of the view, sets the template name to about which loads the template, about.tmpl, and then renders the page. The about.tmpl template file has these definitions:
{{define "title"}}About{{end}}
{{define "head"}}{{end}}
{{define "content"}}
<div class="container">
<div class="page-header">
<h1>{{template "title" .}}</h1>
</div>
<p>This project demonstrates how to structure and build a website using
the Go language without a framework.</p>
{{template "footer" .}}
</div>
{{end}}
{{define "foot"}}{{end}}
Since index is specified as the root template in the config.json file,
The file, index.tmpl, is the base template (specified as Root in config.json), so the definitions from about.tmpl are displayed in index.tmpl when rendered by the controller. You can see where the definitions are called in index.tmpl:
<!DOCTYPE html>
<html lang="en">
<head>
...
<title>{{template "title" .}}</title>
{{template "head" .}}
</head>
<body>
...
{{template "content" .}}
{{JS "static/js/jquery1.11.0.min.js"}}
{{JS "static/js/underscore-min.js"}}
{{JS "static/js/bootstrap.min.js"}}
{{JS "static/js/global.js"}}
{{template "foot" .}}
</body>
</html>
The final result is the rendered page:
View Plugins
The view plugins are a functions that are passed as template funcs by view.go to the templates.
They are functions that are made available in the templates. There are a few plugins included in GoWebApp:
The functions can be called in the templates like this:
<!-- CSS files with timestamps -->
{{CSS "static/css/normalize3.0.0.min.css"}}
parses to
<link rel="stylesheet" type="text/css" href="/static/css/normalize3.0.0.min.css?1435528339" />
<!-- JS files with timestamps -->
{{JS "static/js/jquery1.11.0.min.js"}}
parses to
<script type="text/javascript" src="/static/js/jquery1.11.0.min.js?1435528404"></script>
<!-- Hyperlinks -->
{{LINK "register" "Create a new account."}}
parses to
<a href="/register">Create a new account.</a>
<!-- Output an unescaped variable (not a safe idea, but I find it useful when troubleshooting) -->
{{.SomeVariable | NOESCAPE}}
<!-- Time format -->
{{.SomeTime | PRETTYTIME}}
parses to format
3:04 PM 01/02/2006
There is also a plugin in the Recaptca package:
The code looks like this:
// Plugin returns a map of functions that are usable in templates
func Plugin() template.FuncMap {
f := make(template.FuncMap)
f["RECAPTCHA_SITEKEY"] = func() template.HTML {
if ReadConfig().Enabled {
return template.HTML(ReadConfig().SiteKey)
}
return template.HTML("")
}
return f
}
All the plugins are written the same way. The outer function returns a template.FuncMap so it can be passed to view.LoadPlugins() in the main() func during setup. view.LoadPlugins() combines all the plugins together and then passes all of them to every template at parse time. Each function itself returns template.HTML.
If you’re interested in reading more about the application, take a look at the README and code on GitHub: https://github.com/josephspurrier/gowebapp.