template/html: refactor template loading

Allow customization of the template loading process by providing a
Loader implementation.

Allow auto-reloading of templates in development mode.
This commit is contained in:
wpetit 2020-07-07 09:01:04 +02:00
parent f48503f2b6
commit 985ce3eba3
4 changed files with 124 additions and 63 deletions

56
template/html/loader.go Normal file
View File

@ -0,0 +1,56 @@
package html
import (
"io/ioutil"
"path/filepath"
)
type Loader interface {
Load(*TemplateService) error
}
type DirectoryLoader struct {
rootDir string
}
func (l *DirectoryLoader) Load(srv *TemplateService) error {
blockFiles, err := filepath.Glob(filepath.Join(l.rootDir, "blocks", "*.tmpl"))
if err != nil {
return err
}
for _, f := range blockFiles {
blockContent, err := ioutil.ReadFile(f)
if err != nil {
return err
}
blockName := filepath.Base(f)
srv.AddBlock(blockName, string(blockContent))
}
layoutFiles, err := filepath.Glob(filepath.Join(l.rootDir, "layouts", "*.tmpl"))
if err != nil {
return err
}
// Generate our templates map from our layouts/ and blocks/ directories
for _, f := range layoutFiles {
templateData, err := ioutil.ReadFile(f)
if err != nil {
return err
}
templateName := filepath.Base(f)
if err := srv.LoadLayout(templateName, string(templateData)); err != nil {
return err
}
}
return nil
}
func NewDirectoryLoader(rootDir string) *DirectoryLoader {
return &DirectoryLoader{rootDir}
}

View File

@ -6,6 +6,7 @@ import "html/template"
type Options struct { type Options struct {
Helpers template.FuncMap Helpers template.FuncMap
PoolSize int PoolSize int
DevMode bool
} }
// OptionFunc configures options for the template service // OptionFunc configures options for the template service
@ -48,6 +49,14 @@ func WithPoolSize(size int) OptionFunc {
} }
} }
// WithDevMode configures the template service
// to use the development mode (auto reload of templates).
func WithDevMode(enabled bool) OptionFunc {
return func(opts *Options) {
opts.DevMode = enabled
}
}
func defaultOptions() *Options { func defaultOptions() *Options {
options := &Options{} options := &Options{}
funcs := []OptionFunc{ funcs := []OptionFunc{

View File

@ -4,9 +4,9 @@ import "gitlab.com/wpetit/goweb/service"
// ServiceProvider returns a service.Provider for the // ServiceProvider returns a service.Provider for the
// the HTML template service implementation // the HTML template service implementation
func ServiceProvider(templateDir string, funcs ...OptionFunc) service.Provider { func ServiceProvider(loader Loader, funcs ...OptionFunc) service.Provider {
templateService := NewTemplateService(funcs...) templateService := NewTemplateService(loader, funcs...)
err := templateService.LoadTemplatesDir(templateDir) err := templateService.Load()
return func(container *service.Container) (interface{}, error) { return func(container *service.Container) (interface{}, error) {
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -1,91 +1,79 @@
package html package html
import ( import (
"fmt" "errors"
"html/template" "html/template"
"io" "io"
"io/ioutil"
"net/http" "net/http"
"path/filepath" "sync"
"github.com/oxtoacart/bpool" "github.com/oxtoacart/bpool"
) )
var (
ErrLayoutNotFound = errors.New("layout not found")
)
// TemplateService is a template/html based templating service // TemplateService is a template/html based templating service
type TemplateService struct { type TemplateService struct {
templates map[string]*template.Template mutex sync.RWMutex
pool *bpool.BufferPool blocks map[string]string
helpers template.FuncMap layouts map[string]*template.Template
loader Loader
pool *bpool.BufferPool
helpers template.FuncMap
devMode bool
} }
func (t *TemplateService) LoadLayout(name string, layout string, blocks []string) error { func (t *TemplateService) AddBlock(name string, blockContent string) {
t.mutex.Lock()
defer t.mutex.Unlock()
t.blocks[name] = blockContent
}
func (t *TemplateService) LoadLayout(name string, layoutContent string) error {
tmpl := template.New(name) tmpl := template.New(name)
tmpl.Funcs(t.helpers) tmpl.Funcs(t.helpers)
for _, b := range blocks { t.mutex.RLock()
for _, b := range t.blocks {
if _, err := tmpl.Parse(b); err != nil { if _, err := tmpl.Parse(b); err != nil {
t.mutex.RUnlock()
return err return err
} }
} }
t.mutex.RUnlock()
tmpl, err := tmpl.Parse(layout) tmpl, err := tmpl.Parse(layoutContent)
if err != nil { if err != nil {
return err return err
} }
t.templates[name] = tmpl t.mutex.Lock()
t.layouts[name] = tmpl
t.mutex.Unlock()
return nil return nil
} }
// LoadTemplatesDir loads the templates used by the service // Load parses templates via the configured template loader.
func (t *TemplateService) LoadTemplatesDir(templatesDir string) error { func (t *TemplateService) Load() error {
layoutFiles, err := filepath.Glob(filepath.Join(templatesDir, "layouts", "*.tmpl")) return t.loader.Load(t)
if err != nil {
return err
}
blockFiles, err := filepath.Glob(filepath.Join(templatesDir, "blocks", "*.tmpl"))
if err != nil {
return err
}
blockTemplates := make([]string, 0, len(blockFiles))
for _, f := range blockFiles {
templateData, err := ioutil.ReadFile(f)
if err != nil {
return err
}
blockTemplates = append(blockTemplates, string(templateData))
}
// Generate our templates map from our layouts/ and blocks/ directories
for _, f := range layoutFiles {
templateData, err := ioutil.ReadFile(f)
if err != nil {
return err
}
templateName := filepath.Base(f)
if err := t.LoadLayout(templateName, string(templateData), blockTemplates); err != nil {
return err
}
}
return nil
} }
// RenderPage renders a template to the given http.ResponseWriter // RenderPage renders a template to the given http.ResponseWriter
func (t *TemplateService) RenderPage(w http.ResponseWriter, templateName string, data interface{}) error { func (t *TemplateService) RenderPage(w http.ResponseWriter, templateName string, data interface{}) error {
if t.devMode {
if err := t.loader.Load(t); err != nil {
return err
}
}
// Ensure the template exists in the map. // Ensure the template exists in the map.
tmpl, ok := t.templates[templateName] tmpl, ok := t.layouts[templateName]
if !ok { if !ok {
return fmt.Errorf("the template '%s' does not exist", templateName) return ErrLayoutNotFound
} }
// Create a buffer to temporarily write to and check if any errors were encountered. // Create a buffer to temporarily write to and check if any errors were encountered.
@ -103,20 +91,25 @@ func (t *TemplateService) RenderPage(w http.ResponseWriter, templateName string,
} }
// Render renders a template to the given io.Writer // Render renders a layout to the given io.Writer.
func (t *TemplateService) Render(w io.Writer, templateName string, data interface{}) error { func (t *TemplateService) Render(w io.Writer, layoutName string, data interface{}) error {
if t.devMode {
if err := t.loader.Load(t); err != nil {
return err
}
}
// Ensure the template exists in the map. // Ensure the template exists in the map.
tmpl, ok := t.templates[templateName] tmpl, ok := t.layouts[layoutName]
if !ok { if !ok {
return fmt.Errorf("the template '%s' does not exist", templateName) return ErrLayoutNotFound
} }
// Create a buffer to temporarily write to and check if any errors were encountered. // Create a buffer to temporarily write to and check if any errors were encountered.
buf := t.pool.Get() buf := t.pool.Get()
defer t.pool.Put(buf) defer t.pool.Put(buf)
if err := tmpl.ExecuteTemplate(buf, templateName, data); err != nil { if err := tmpl.ExecuteTemplate(buf, layoutName, data); err != nil {
return err return err
} }
@ -126,14 +119,17 @@ func (t *TemplateService) Render(w io.Writer, templateName string, data interfac
} }
// NewTemplateService returns a new Service // NewTemplateService returns a new Service
func NewTemplateService(funcs ...OptionFunc) *TemplateService { func NewTemplateService(loader Loader, funcs ...OptionFunc) *TemplateService {
options := defaultOptions() options := defaultOptions()
for _, f := range funcs { for _, f := range funcs {
f(options) f(options)
} }
return &TemplateService{ return &TemplateService{
templates: make(map[string]*template.Template), blocks: make(map[string]string),
pool: bpool.NewBufferPool(options.PoolSize), layouts: make(map[string]*template.Template),
helpers: options.Helpers, pool: bpool.NewBufferPool(options.PoolSize),
helpers: options.Helpers,
loader: loader,
devMode: options.DevMode,
} }
} }