This commit is contained in:
Gerasimos (Makis) Maropoulos 2020-10-01 16:06:16 +03:00
parent 5017e3c986
commit 552539bed1
No known key found for this signature in database
GPG Key ID: 5DBE766BD26A54E7
75 changed files with 807 additions and 47 deletions

View File

@ -1,3 +1,4 @@
# Benchmarks # Benchmarks
Moved to <https://github.com/kataras/server-benchmarks#benchmarks>. - [HTTP/2 Benchmarks](https://github.com/kataras/server-benchmarks#benchmarks)
- [View Engine Benchmarks](./view)

View File

@ -0,0 +1,56 @@
# View Engine Benchmarks
Benchmark between all 8 supported template parsers.
Amber, Ace and Pug parsers minifies the template before render. So, to have a fair benchmark, we must make sure that the byte amount of the total response body is exactly the same across all. Therefore, all other template files are minified too.
![Benchmarks Chart Graph](chart.png)
> Last updated: Oct 1, 2020 at 12:46pm (UTC)
## System
| | |
|----|:---|
| Processor | Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz |
| RAM | 15.85 GB |
| OS | Microsoft Windows 10 Pro |
| [Bombardier](https://github.com/codesenberg/bombardier) | v1.2.4 |
| [Go](https://golang.org) | go1.15.2 |
## Terminology
**Name** is the name of the framework(or router) used under a particular test.
**Reqs/sec** is the avg number of total requests could be processed per second (the higher the better).
**Latency** is the amount of time it takes from when a request is made by the client to the time it takes for the response to get back to that client (the smaller the better).
**Throughput** is the rate of production or the rate at which data are transferred (the higher the better, it depends from response length (body + headers).
**Time To Complete** is the total time (in seconds) the test completed (the smaller the better).
## Results
### Test:Template Layout, Partial and Data
📖 Fires 1000000 requests with 125 concurrent clients. It receives HTML response. The server handler sets some template **data** and renders a template file which consists of a **layout** and a **partial** footer.
| Name | Language | Reqs/sec | Latency | Throughput | Time To Complete |
|------|:---------|:---------|:--------|:-----------|:-----------------|
| [Amber](./amber) | Go |125698 |0.99ms |44.67MB |7.96s |
| [Blocks](./blocks) | Go |123974 |1.01ms |43.99MB |8.07s |
| [Django](./django) | Go |118831 |1.05ms |42.17MB |8.41s |
| [Handlebars](./handlebars) | Go |101214 |1.23ms |35.91MB |9.88s |
| [Pug](./pug) | Go |89002 |1.40ms |31.81MB |11.24s |
| [Ace](./ace) | Go |64782 |1.93ms |22.98MB |15.44s |
| [HTML](./html) | Go |53918 |2.32ms |19.13MB |18.55s |
| [Jet](./jet) | Go |4829 |25.88ms |1.71MB |207.07s |
## How to Run
```sh
$ go get -u github.com/kataras/server-benchmarks
$ go get -u github.com/codesenberg/bombardier
$ server-benchmarks --wait-run=3s -o ./results
```

View File

@ -0,0 +1,24 @@
package main
import "github.com/kataras/iris/v12"
func main() {
app := iris.New()
// By default Ace engine minifies the template before render.
app.RegisterView(iris.Ace("./views", ".ace").SetIndent(""))
app.Get("/", index)
app.Listen(":8080")
}
func index(ctx iris.Context) {
data := iris.Map{
"Title": "Page Title",
"FooterText": "Footer contents",
"Message": "Main contents",
}
ctx.ViewLayout("layouts/main")
ctx.View("index", data)
}

View File

@ -0,0 +1,2 @@
h1 Index Body
h3 Message: {{.Message}}

View File

@ -0,0 +1,8 @@
= doctype html
html
head
title {{.Title}}
body
{{ yield }}
footer
= include partials/footer.ace .

View File

@ -0,0 +1,2 @@
h3 Footer Partial
h4 {{.FooterText}}

View File

@ -0,0 +1,26 @@
package main
import "github.com/kataras/iris/v12"
func main() {
app := iris.New()
// By default Amber engine minifies the template before render.
app.RegisterView(iris.Amber("./views", ".amber"))
app.Get("/", index)
app.Listen(":8080")
}
func index(ctx iris.Context) {
data := iris.Map{
"Title": "Page Title",
"FooterText": "Footer contents",
"Message": "Main contents",
}
// On Amber this is ignored: ctx.ViewLayout("layouts/main")
// Layouts are only rendered from inside the index page itself
// using the "extends" keyword.
ctx.View("index", data)
}

View File

@ -0,0 +1,5 @@
extends layouts/main.amber
block content
h1 Index Body
h3 Message: #{Message}

View File

@ -0,0 +1,8 @@
doctype html
html
head
title #{Title}
body
block content
footer
#{render("partials/footer.amber", $)}

View File

@ -0,0 +1,2 @@
h3 Footer Partial
h4 #{FooterText}

View File

@ -0,0 +1,26 @@
package main
import "github.com/kataras/iris/v12"
func main() {
app := iris.New()
app.RegisterView(iris.Blocks("./views", ".html"))
// Note, in Blocks engine, layouts
// are used by their base names, the
// blocks.LayoutDir(layoutDir) defaults to "./layouts".
app.Get("/", index)
app.Listen(":8080")
}
func index(ctx iris.Context) {
data := iris.Map{
"Title": "Page Title",
"FooterText": "Footer contents",
"Message": "Main contents",
}
ctx.ViewLayout("main")
ctx.View("index", data)
}

View File

@ -0,0 +1 @@
<h1>Index Body</h1><h3>Message: {{.Message}}</h3>

View File

@ -0,0 +1 @@
<!DOCTYPE html><html><head><title>{{.Title}}</title></head><body>{{ template "content" . }}<footer>{{ partial "partials/footer" .}}</footer></body></html>

View File

@ -0,0 +1 @@
<h3>Footer Partial</h3><h4>{{.FooterText}}</h4>

BIN
_benchmarks/view/chart.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

View File

@ -0,0 +1,25 @@
package main
import "github.com/kataras/iris/v12"
func main() {
app := iris.New()
app.RegisterView(iris.Django("./views", ".html"))
app.Get("/", index)
app.Listen(":8080")
}
func index(ctx iris.Context) {
data := iris.Map{
"Title": "Page Title",
"FooterText": "Footer contents",
"Message": "Main contents",
}
// On Django this is ignored: ctx.ViewLayout("layouts/main")
// Layouts are only rendered from inside the index page itself
// using the "extends" keyword.
ctx.View("index", data)
}

View File

@ -0,0 +1 @@
{% extends "layouts/main.html" %}{% block content %}<h1>Index Body</h1><h3>Message: {{Message}}</h3>{% endblock %}

View File

@ -0,0 +1 @@
<!DOCTYPE html><html><head><title>{{Title}}</title></head><body>{% block content %} {% endblock %}<footer>{% include "../partials/footer.html" %}</footer></body></html>

View File

@ -0,0 +1 @@
<h3>Footer Partial</h3><h4>{{FooterText}}</h4>

View File

@ -0,0 +1,23 @@
package main
import "github.com/kataras/iris/v12"
func main() {
app := iris.New()
app.RegisterView(iris.Handlebars("./views", ".html"))
app.Get("/", index)
app.Listen(":8080")
}
func index(ctx iris.Context) {
data := iris.Map{
"Title": "Page Title",
"FooterText": "Footer contents",
"Message": "Main contents",
}
ctx.ViewLayout("layouts/main")
ctx.View("index", data)
}

View File

@ -0,0 +1 @@
<h1>Index Body</h1><h3>Message: {{Message}}</h3>

View File

@ -0,0 +1 @@
<!DOCTYPE html><html><head><title>{{Title}}</title></head><body>{{ yield }}<footer>{{ render "partials/footer.html" .}}</footer></body></html>

View File

@ -0,0 +1 @@
<h3>Footer Partial</h3><h4>{{FooterText}}</h4>

View File

@ -0,0 +1,24 @@
package main
import "github.com/kataras/iris/v12"
func main() {
app := iris.New()
app.RegisterView(iris.HTML("./views", ".html"))
app.Get("/", index)
app.Listen(":8080")
}
func index(ctx iris.Context) {
data := iris.Map{
"Title": "Page Title",
"FooterText": "Footer contents",
"Message": "Main contents",
}
ctx.ViewLayout("layouts/main")
ctx.View("index", data)
}

View File

@ -0,0 +1 @@
<h1>Index Body</h1><h3>Message: {{.Message}}</h3>

View File

@ -0,0 +1 @@
<!DOCTYPE html><html><head><title>{{.Title}}</title></head><body>{{ yield }}<footer>{{ render "partials/footer.html" }}</footer></body></html>

View File

@ -0,0 +1 @@
<h3>Footer Partial</h3><h4>{{.FooterText}}</h4>

View File

@ -0,0 +1,25 @@
package main
import "github.com/kataras/iris/v12"
func main() {
app := iris.New()
app.RegisterView(iris.Jet("./views", ".jet"))
app.Get("/", index)
app.Listen(":8080")
}
func index(ctx iris.Context) {
data := iris.Map{
"Title": "Page Title",
"FooterText": "Footer contents",
"Message": "Main contents",
}
// On Jet this is ignored: ctx.ViewLayout("layouts/main")
// Layouts are only rendered from inside the index page itself
// using the "extends" keyword.
ctx.View("index", data)
}

View File

@ -0,0 +1 @@
{{ extends "../layouts/main.jet" }}{{ block documentBody() }}<h1>Index Body</h1><h3>Message: {{.Message}}</h3>{{ end }}

View File

@ -0,0 +1 @@
<!DOCTYPE html><html><head><title>{{.Title}}</title></head><body>{{ yield documentBody() }}<footer>{{ include "../partials/footer.jet" . }}</footer></body></html>

View File

@ -0,0 +1 @@
<h3>Footer Partial</h3><h4>{{.FooterText}}</h4>

View File

@ -0,0 +1,26 @@
package main
import "github.com/kataras/iris/v12"
func main() {
app := iris.New()
// Ace engine minifies the template before render.
app.RegisterView(iris.Pug("./views", ".pug"))
app.Get("/", index)
app.Listen(":8080")
}
func index(ctx iris.Context) {
data := iris.Map{
"Title": "Page Title",
"FooterText": "Footer contents",
"Message": "Main contents",
}
// On Pug this is ignored: ctx.ViewLayout("layouts/main")
// Layouts are only rendered from inside the index page itself
// using the "extends" keyword.
ctx.View("index", data)
}

View File

@ -0,0 +1,5 @@
extends layouts/main.pug
block content
h1 Index Body
h3 Message: {{.Message}}

View File

@ -0,0 +1,8 @@
doctype html
html
head
title {{.Title}}
body
block content
footer
include ../partials/footer.pug

View File

@ -0,0 +1,2 @@
h3 Footer Partial
h4 {{.FooterText}}

View File

@ -0,0 +1,27 @@
- Name: Template Layout, Partial and Data
Description: >
Fires {{.NumberOfRequests}} requests with {{.NumberOfConnections}} concurrent clients.
It receives HTML response.
The server handler sets some template **data** and renders a
template file which consists of a **layout** and a **partial** footer.
NumberOfRequests: 1000000
NumberOfConnections: 125
Method: GET
URL: http://localhost:8080
Envs:
- Name: Ace
Dir: ./ace
- Name: Amber
Dir: ./amber
- Name: Blocks
Dir: ./blocks
- Name: Django
Dir: ./django
- Name: Handlebars
Dir: ./handlebars
- Name: HTML
Dir: ./html
- Name: Jet
Dir: ./jet
- Name: Pug
Dir: ./pug

View File

@ -108,6 +108,15 @@
* [Upload Multiple Files](file-server/upload-files/main.go) * [Upload Multiple Files](file-server/upload-files/main.go)
* View * View
* [Overview](view/overview/main.go) * [Overview](view/overview/main.go)
* [Layout](view/layout)
* [Ace](view/layout/ace)
* [Amber](view/layout/amber)
* [Blocks](view/layout/blocks)
* [Django](view/layout/django)
* [Handlebars](view/layout/handlebars)
* [HTML](view/layout/html)
* [Jet](view/layout/jet)
* [Pug](view/layout/pug)
* [Basic](view/template_html_0/main.go) * [Basic](view/template_html_0/main.go)
* [A simple Layout](view/template_html_1/main.go) * [A simple Layout](view/template_html_1/main.go)
* [Layouts: `yield` and `render` tmpl funcs](view/template_html_2/main.go) * [Layouts: `yield` and `render` tmpl funcs](view/template_html_2/main.go)

View File

@ -0,0 +1,26 @@
package main
import "github.com/kataras/iris/v12"
func main() {
app := iris.New()
// By default Ace minifies the template before render,
// using the SetIndent method, we make it to match
// the rest of the template results.
app.RegisterView(iris.Ace("./views", ".ace").SetIndent(" "))
app.Get("/", index)
app.Listen(":8080")
}
func index(ctx iris.Context) {
data := iris.Map{
"Title": "Page Title",
"FooterText": "Footer contents",
"Message": "Main contents",
}
ctx.ViewLayout("layouts/main")
ctx.View("index", data)
}

View File

@ -0,0 +1,2 @@
h1 Index Body
h3 Message: {{.Message}}

View File

@ -0,0 +1,8 @@
= doctype html
html
head
title {{.Title}}
body
{{ yield }}
footer
= include partials/footer.ace .

View File

@ -0,0 +1,2 @@
h3 Footer Partial
h4 {{.FooterText}}

View File

@ -0,0 +1,25 @@
package main
import "github.com/kataras/iris/v12"
func main() {
app := iris.New()
app.RegisterView(iris.Amber("./views", ".amber"))
app.Get("/", index)
app.Listen(":8080")
}
func index(ctx iris.Context) {
data := iris.Map{
"Title": "Page Title",
"FooterText": "Footer contents",
"Message": "Main contents",
}
// On Amber this is ignored: ctx.ViewLayout("layouts/main")
// Layouts are only rendered from inside the index page itself
// using the "extends" keyword.
ctx.View("index", data)
}

View File

@ -0,0 +1,5 @@
extends layouts/main.amber
block content
h1 Index Body
h3 Message: #{Message}

View File

@ -0,0 +1,8 @@
doctype html
html
head
title #{Title}
body
block content
footer
#{render("partials/footer.amber", $)}

View File

@ -0,0 +1,2 @@
h3 Footer Partial
h4 #{FooterText}

View File

@ -0,0 +1,26 @@
package main
import "github.com/kataras/iris/v12"
func main() {
app := iris.New()
app.RegisterView(iris.Blocks("./views", ".html"))
// Note, in Blocks engine, layouts
// are used by their base names, the
// blocks.LayoutDir(layoutDir) defaults to "./layouts".
app.Get("/", index)
app.Listen(":8080")
}
func index(ctx iris.Context) {
data := iris.Map{
"Title": "Page Title",
"FooterText": "Footer contents",
"Message": "Main contents",
}
ctx.ViewLayout("main")
ctx.View("index", data)
}

View File

@ -0,0 +1,2 @@
<h1>Index Body</h1>
<h3>Message: {{.Message}}</h3>

View File

@ -0,0 +1,11 @@
<!DOCTYPE html>
<head>
<title>{{.Title}}</title>
</head>
<body>
{{ template "content" . }}
<footer>
{{ partial "partials/footer" .}}
</footer>
</body>
</html>

View File

@ -0,0 +1,2 @@
<h3>Footer Partial</h3>
<h4>{{.FooterText}}</h4>

View File

@ -0,0 +1,25 @@
package main
import "github.com/kataras/iris/v12"
func main() {
app := iris.New()
app.RegisterView(iris.Django("./views", ".html"))
app.Get("/", index)
app.Listen(":8080")
}
func index(ctx iris.Context) {
data := iris.Map{
"Title": "Page Title",
"FooterText": "Footer contents",
"Message": "Main contents",
}
// On Django this is ignored: ctx.ViewLayout("layouts/main")
// Layouts are only rendered from inside the index page itself
// using the "extends" keyword.
ctx.View("index", data)
}

View File

@ -0,0 +1,6 @@
{% extends "layouts/main.html" %}
{% block content %}
<h1>Index Body</h1>
<h3>Message: {{Message}}</h3>
{% endblock %}

View File

@ -0,0 +1,10 @@
<!DOCTYPE html>
<head>
<title>{{Title}}</title>
</head>
<body>
{% block content %} {% endblock %}
<footer>{% include "../partials/footer.html" %}</footer>
</body>
</html>

View File

@ -0,0 +1,2 @@
<h3>Footer Partial</h3>
<h4>{{FooterText}}</h4>

View File

@ -0,0 +1,24 @@
package main
import "github.com/kataras/iris/v12"
func main() {
app := iris.New()
app.RegisterView(iris.Handlebars("./views", ".html"))
app.Get("/", index)
app.Listen(":8080")
}
func index(ctx iris.Context) {
data := iris.Map{
"Title": "Page Title",
"FooterText": "Footer contents",
"Message": "Main contents",
}
ctx.ViewLayout("layouts/main")
ctx.View("index", data)
}

View File

@ -0,0 +1,2 @@
<h1>Index Body</h1>
<h3>Message: {{Message}} </h3>

View File

@ -0,0 +1,10 @@
<!DOCTYPE html>
<head>
<title>{{Title}}</title>
</head>
<body>
{{ yield }}
<footer>{{ render "partials/footer.html" .}}</footer>
</body>
</html>

View File

@ -0,0 +1,2 @@
<h3>Footer Partial</h3>
<h4>{{FooterText}}</h4>

View File

@ -0,0 +1,24 @@
package main
import "github.com/kataras/iris/v12"
func main() {
app := iris.New()
app.RegisterView(iris.HTML("./views", ".html"))
app.Get("/", index)
app.Listen(":8080")
}
func index(ctx iris.Context) {
data := iris.Map{
"Title": "Page Title",
"FooterText": "Footer contents",
"Message": "Main contents",
}
ctx.ViewLayout("layouts/main")
ctx.View("index", data)
}

View File

@ -0,0 +1,2 @@
<h1>Index Body</h1>
<h3>Message: {{.Message}}</h3>

View File

@ -0,0 +1,11 @@
<!DOCTYPE html>
<head>
<title>{{.Title}}</title>
</head>
<body>
{{ yield }}
<footer>
{{ render "partials/footer.html" }}
</footer>
</body>
</html>

View File

@ -0,0 +1,2 @@
<h3>Footer Partial</h3>
<h4>{{.FooterText}}</h4>

View File

@ -0,0 +1,26 @@
package main
import "github.com/kataras/iris/v12"
func main() {
app := iris.New()
app.RegisterView(iris.Jet("./views", ".jet"))
app.Get("/", index)
app.Listen(":8080")
}
func index(ctx iris.Context) {
data := iris.Map{
"Title": "Page Title",
"FooterText": "Footer contents",
"Message": "Main contents",
}
// On Jet this is ignored: ctx.ViewLayout("layouts/main")
// Layouts are only rendered from inside the index page itself
// using the "extends" keyword.
ctx.View("index", data)
}

View File

@ -0,0 +1,5 @@
{{ extends "../layouts/main.jet" }}
{{ block documentBody() }}
<h1>Index Body</h1>
<h3>Message: {{.Message}}</h3>
{{ end }}

View File

@ -0,0 +1,9 @@
<!DOCTYPE html>
<head>
<title>{{.Title}}</title>
</head>
<body>
{{ yield documentBody() }}
<footer>{{ include "../partials/footer.jet" . }}</footer>
</body>
</html>

View File

@ -0,0 +1,2 @@
<h3>Footer Partial</h3>
<h4>{{.FooterText}}</h4>

View File

@ -0,0 +1,25 @@
package main
import "github.com/kataras/iris/v12"
func main() {
app := iris.New()
app.RegisterView(iris.Pug("./views", ".pug"))
app.Get("/", index)
app.Listen(":8080")
}
func index(ctx iris.Context) {
data := iris.Map{
"Title": "Page Title",
"FooterText": "Footer contents",
"Message": "Main contents",
}
// On Pug this is ignored: ctx.ViewLayout("layouts/main")
// Layouts are only rendered from inside the index page itself
// using the "extends" keyword.
ctx.View("index", data)
}

View File

@ -0,0 +1,5 @@
extends layouts/main.pug
block content
h1 Index Body
h3 Message: {{.Message}}

View File

@ -0,0 +1,8 @@
doctype html
html
head
title {{.Title}}
body
block content
footer
include ../partials/footer.pug

View File

@ -0,0 +1,2 @@
h3 Footer Partial
h4 {{.FooterText}}

View File

@ -19,6 +19,8 @@ Parse using embedded assets, Layouts and Party-specific layout, Template Funcs,
[List of Examples](https://github.com/kataras/iris/tree/master/_examples/view). [List of Examples](https://github.com/kataras/iris/tree/master/_examples/view).
[Benchmarks](https://github.com/kataras/iris/tree/master/_benchmarks/view).
You can serve [quicktemplate](https://github.com/valyala/quicktemplate) files too, simply by using the `Context.ResponseWriter`, take a look at the [iris/_examples/view/quicktemplate](https://github.com/kataras/iris/tree/master/_examples/view/quicktemplate) example. You can serve [quicktemplate](https://github.com/valyala/quicktemplate) files too, simply by using the `Context.ResponseWriter`, take a look at the [iris/_examples/view/quicktemplate](https://github.com/kataras/iris/tree/master/_examples/view/quicktemplate) example.
## Overview ## Overview

View File

@ -6,10 +6,28 @@ import (
"github.com/yosssi/ace" "github.com/yosssi/ace"
) )
// Ace returns a new ace view engine. // AceEngine represents the Ace view engine.
// See the `Ace` package-level function for more.
type AceEngine struct {
*HTMLEngine
indent string
}
// SetIndent string used for indentation.
// Do NOT use tabs, only spaces characters.
// Defaults to minified response, no indentation.
func (s *AceEngine) SetIndent(indent string) *AceEngine {
s.indent = indent
return s
}
// Ace returns a new Ace view engine.
// It shares the same exactly logic with the // It shares the same exactly logic with the
// html view engine, it uses the same exactly configuration. // html view engine, it uses the same exactly configuration.
// The given "extension" MUST begin with a dot. // The given "extension" MUST begin with a dot.
// Ace minifies the response automatically unless
// SetIndent() method is set.
// //
// Read more about the Ace Go Parser: https://github.com/yosssi/ace // Read more about the Ace Go Parser: https://github.com/yosssi/ace
// //
@ -17,13 +35,14 @@ import (
// Ace("./views", ".ace") or // Ace("./views", ".ace") or
// Ace(iris.Dir("./views"), ".ace") or // Ace(iris.Dir("./views"), ".ace") or
// Ace(AssetFile(), ".ace") for embedded data. // Ace(AssetFile(), ".ace") for embedded data.
func Ace(fs interface{}, extension string) *HTMLEngine { func Ace(fs interface{}, extension string) *AceEngine {
s := HTML(fs, extension) s := &AceEngine{HTMLEngine: HTML(fs, extension), indent: ""}
s.name = "Ace" s.name = "Ace"
funcs := make(map[string]interface{}, 0) funcs := make(map[string]interface{}, 0)
once := new(sync.Once) once := new(sync.Once)
s.middleware = func(name string, text []byte) (contents string, err error) { s.middleware = func(name string, text []byte) (contents string, err error) {
once.Do(func() { // on first template parse, all funcs are given. once.Do(func() { // on first template parse, all funcs are given.
for k, v := range emptyFuncs { for k, v := range emptyFuncs {
@ -43,17 +62,20 @@ func Ace(fs interface{}, extension string) *HTMLEngine {
[]*ace.File{}, []*ace.File{},
) )
rslt, err := ace.ParseSource(src, nil) opts := &ace.Options{
if err != nil {
return "", err
}
t, err := ace.CompileResult(name, rslt, &ace.Options{
Extension: extension[1:], Extension: extension[1:],
FuncMap: funcs, FuncMap: funcs,
DelimLeft: s.left, DelimLeft: s.left,
DelimRight: s.right, DelimRight: s.right,
}) Indent: s.indent,
}
rslt, err := ace.ParseSource(src, opts)
if err != nil {
return "", err
}
t, err := ace.CompileResult(name, rslt, opts)
if err != nil { if err != nil {
return "", err return "", err
} }

View File

@ -1,6 +1,7 @@
package view package view
import ( import (
"bytes"
"fmt" "fmt"
"html/template" "html/template"
"io" "io"
@ -9,6 +10,7 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"sync" "sync"
"sync/atomic"
"github.com/eknkc/amber" "github.com/eknkc/amber"
) )
@ -23,6 +25,7 @@ type AmberEngine struct {
// //
rmu sync.RWMutex // locks for `ExecuteWiter` when `reload` is true. rmu sync.RWMutex // locks for `ExecuteWiter` when `reload` is true.
templateCache map[string]*template.Template templateCache map[string]*template.Template
bufPool *sync.Pool
Options amber.Options Options amber.Options
} }
@ -32,6 +35,8 @@ var (
_ EngineFuncer = (*AmberEngine)(nil) _ EngineFuncer = (*AmberEngine)(nil)
) )
var amberOnce = new(uint32)
// Amber creates and returns a new amber view engine. // Amber creates and returns a new amber view engine.
// The given "extension" MUST begin with a dot. // The given "extension" MUST begin with a dot.
// //
@ -40,6 +45,12 @@ var (
// Amber(iris.Dir("./views"), ".amber") or // Amber(iris.Dir("./views"), ".amber") or
// Amber(AssetFile(), ".amber") for embedded data. // Amber(AssetFile(), ".amber") for embedded data.
func Amber(fs interface{}, extension string) *AmberEngine { func Amber(fs interface{}, extension string) *AmberEngine {
if atomic.LoadUint32(amberOnce) > 0 {
panic("Amber: cannot be registered twice as its internal implementation share the same template functions across instances.")
} else {
atomic.StoreUint32(amberOnce, 1)
}
fileSystem := getFS(fs) fileSystem := getFS(fs)
s := &AmberEngine{ s := &AmberEngine{
fs: fileSystem, fs: fileSystem,
@ -51,6 +62,20 @@ func Amber(fs interface{}, extension string) *AmberEngine {
LineNumbers: false, LineNumbers: false,
VirtualFilesystem: fileSystem, VirtualFilesystem: fileSystem,
}, },
bufPool: &sync.Pool{New: func() interface{} {
return new(bytes.Buffer)
}},
}
builtinFuncs := template.FuncMap{
"render": func(name string, binding interface{}) (template.HTML, error) {
result, err := s.executeTemplateBuf(name, binding)
return template.HTML(result), err
},
}
for k, v := range builtinFuncs {
amber.FuncMap[k] = v
} }
return s return s
@ -86,6 +111,14 @@ func (s *AmberEngine) Reload(developmentMode bool) *AmberEngine {
return s return s
} }
// SetPrettyPrint if pretty printing is enabled.
// Pretty printing ensures that the output html is properly indented and in human readable form.
// Defaults to false, response is minified.
func (s *AmberEngine) SetPrettyPrint(pretty bool) *AmberEngine {
s.Options.PrettyPrint = true
return s
}
// AddFunc adds the function to the template's function map. // AddFunc adds the function to the template's function map.
// It is legal to overwrite elements of the default actions: // It is legal to overwrite elements of the default actions:
// - url func(routeName string, args ...string) string // - url func(routeName string, args ...string) string
@ -157,18 +190,11 @@ func (s *AmberEngine) ParseTemplate(name string, contents []byte) error {
name = strings.TrimPrefix(name, "/") name = strings.TrimPrefix(name, "/")
/* Sadly, this does not work, only builtin amber.FuncMap /*
can be executed as function, the rest are compiled as data (prepends a "call"), New(...).Funcs(s.builtinFuncs):
relative code: This won't work on amber, it loads only amber.FuncMap which is global.
https://github.com/eknkc/amber/blob/cdade1c073850f4ffc70a829e31235ea6892853b/compiler.go#L771-L794 Relative code:
https://github.com/eknkc/amber/blob/cdade1c073850f4ffc70a829e31235ea6892853b/compiler.go#L771-L794
tmpl := template.New(name).Funcs(amber.FuncMap).Funcs(s.funcs)
if len(funcs) > 0 {
tmpl.Funcs(funcs)
}
We can't add them as binding data of map type
because those data can be a struct by the caller and we don't want to messup.
*/ */
tmpl := template.New(name).Funcs(amber.FuncMap) tmpl := template.New(name).Funcs(amber.FuncMap)
@ -180,6 +206,22 @@ func (s *AmberEngine) ParseTemplate(name string, contents []byte) error {
return err return err
} }
func (s *AmberEngine) executeTemplateBuf(name string, binding interface{}) (string, error) {
buf := s.bufPool.Get().(*bytes.Buffer)
buf.Reset()
tmpl := s.fromCache(name)
if tmpl == nil {
s.bufPool.Put(buf)
return "", ErrNotExist{name, false}
}
err := tmpl.ExecuteTemplate(buf, name, binding)
result := strings.TrimSuffix(buf.String(), "\n") // on amber it adds a new line.
s.bufPool.Put(buf)
return result, err
}
func (s *AmberEngine) fromCache(relativeName string) *template.Template { func (s *AmberEngine) fromCache(relativeName string) *template.Template {
if s.reload { if s.reload {
s.rmu.RLock() s.rmu.RLock()

View File

@ -286,7 +286,7 @@ func (s *DjangoEngine) fromCache(relativeName string) *pongo2.Template {
// ExecuteWriter executes a templates and write its results to the w writer // ExecuteWriter executes a templates and write its results to the w writer
// layout here is useless. // layout here is useless.
func (s *DjangoEngine) ExecuteWriter(w io.Writer, filename string, layout string, bindingData interface{}) error { func (s *DjangoEngine) ExecuteWriter(w io.Writer, filename string, _ string, bindingData interface{}) error {
// re-parse the templates if reload is enabled. // re-parse the templates if reload is enabled.
if s.reload { if s.reload {
if err := s.Load(); err != nil { if err := s.Load(); err != nil {

View File

@ -36,6 +36,7 @@ type HTMLEngine struct {
middleware func(name string, contents []byte) (string, error) middleware func(name string, contents []byte) (string, error)
Templates *template.Template Templates *template.Template
customCache []customTmp // required to load them again if reload is true. customCache []customTmp // required to load them again if reload is true.
bufPool *sync.Pool
// //
} }
@ -92,6 +93,9 @@ func HTML(fs interface{}, extension string) *HTMLEngine {
layout: "", layout: "",
layoutFuncs: make(template.FuncMap), layoutFuncs: make(template.FuncMap),
funcs: make(template.FuncMap), funcs: make(template.FuncMap),
bufPool: &sync.Pool{New: func() interface{} {
return new(bytes.Buffer)
}},
} }
return s return s
@ -322,11 +326,14 @@ func (s *HTMLEngine) initRootTmpl() { // protected by the caller.
} }
} }
func (s *HTMLEngine) executeTemplateBuf(name string, binding interface{}) (*bytes.Buffer, error) { func (s *HTMLEngine) executeTemplateBuf(name string, binding interface{}) (string, error) {
buf := new(bytes.Buffer) buf := s.bufPool.Get().(*bytes.Buffer)
err := s.Templates.ExecuteTemplate(buf, name, binding) buf.Reset()
return buf, err err := s.Templates.ExecuteTemplate(buf, name, binding)
result := buf.String()
s.bufPool.Put(buf)
return result, err
} }
func (s *HTMLEngine) layoutFuncsFor(lt *template.Template, name string, binding interface{}) { func (s *HTMLEngine) layoutFuncsFor(lt *template.Template, name string, binding interface{}) {
@ -334,9 +341,9 @@ func (s *HTMLEngine) layoutFuncsFor(lt *template.Template, name string, binding
funcs := template.FuncMap{ funcs := template.FuncMap{
"yield": func() (template.HTML, error) { "yield": func() (template.HTML, error) {
buf, err := s.executeTemplateBuf(name, binding) result, err := s.executeTemplateBuf(name, binding)
// Return safe HTML here since we are rendering our own template. // Return safe HTML here since we are rendering our own template.
return template.HTML(buf.String()), err return template.HTML(result), err
}, },
} }
@ -352,11 +359,11 @@ func (s *HTMLEngine) runtimeFuncsFor(t *template.Template, name string, binding
"part": func(partName string) (template.HTML, error) { "part": func(partName string) (template.HTML, error) {
nameTemp := strings.Replace(name, s.extension, "", -1) nameTemp := strings.Replace(name, s.extension, "", -1)
fullPartName := fmt.Sprintf("%s-%s", nameTemp, partName) fullPartName := fmt.Sprintf("%s-%s", nameTemp, partName)
buf, err := s.executeTemplateBuf(fullPartName, binding) result, err := s.executeTemplateBuf(fullPartName, binding)
if err != nil { if err != nil {
return "", nil return "", nil
} }
return template.HTML(buf.String()), err return template.HTML(result), err
}, },
"current": func() (string, error) { "current": func() (string, error) {
return name, nil return name, nil
@ -364,8 +371,8 @@ func (s *HTMLEngine) runtimeFuncsFor(t *template.Template, name string, binding
"partial": func(partialName string) (template.HTML, error) { "partial": func(partialName string) (template.HTML, error) {
fullPartialName := fmt.Sprintf("%s-%s", partialName, name) fullPartialName := fmt.Sprintf("%s-%s", partialName, name)
if s.Templates.Lookup(fullPartialName) != nil { if s.Templates.Lookup(fullPartialName) != nil {
buf, err := s.executeTemplateBuf(fullPartialName, binding) result, err := s.executeTemplateBuf(fullPartialName, binding)
return template.HTML(buf.String()), err return template.HTML(result), err
} }
return "", nil return "", nil
}, },
@ -378,14 +385,14 @@ func (s *HTMLEngine) runtimeFuncsFor(t *template.Template, name string, binding
root := name[:len(name)-len(ext)] root := name[:len(name)-len(ext)]
fullPartialName := fmt.Sprintf("%s%s%s", root, partialName, ext) fullPartialName := fmt.Sprintf("%s%s%s", root, partialName, ext)
if s.Templates.Lookup(fullPartialName) != nil { if s.Templates.Lookup(fullPartialName) != nil {
buf, err := s.executeTemplateBuf(fullPartialName, binding) result, err := s.executeTemplateBuf(fullPartialName, binding)
return template.HTML(buf.String()), err return template.HTML(result), err
} }
return "", nil return "", nil
}, },
"render": func(fullPartialName string) (template.HTML, error) { "render": func(fullPartialName string) (template.HTML, error) {
buf, err := s.executeTemplateBuf(fullPartialName, binding) result, err := s.executeTemplateBuf(fullPartialName, binding)
return template.HTML(buf.String()), err return template.HTML(result), err
}, },
} }

View File

@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
"os"
"path/filepath" "path/filepath"
"reflect" "reflect"
"strings" "strings"
@ -18,9 +19,10 @@ const jetEngineName = "jet"
// JetEngine is the jet template parser's view engine. // JetEngine is the jet template parser's view engine.
type JetEngine struct { type JetEngine struct {
fs http.FileSystem fs http.FileSystem
rootDir string rootDir string
extension string extension string
left, right string
loader jet.Loader loader jet.Loader
@ -72,6 +74,7 @@ func Jet(fs interface{}, extension string) *JetEngine {
} }
s := &JetEngine{ s := &JetEngine{
fs: getFS(fs),
rootDir: "/", rootDir: "/",
extension: extension, extension: extension,
loader: &jetLoader{fs: getFS(fs)}, loader: &jetLoader{fs: getFS(fs)},
@ -109,7 +112,8 @@ func (s *JetEngine) Ext() string {
// corresponding default: {{ or }}. // corresponding default: {{ or }}.
// Should act before `Load` or `iris.Application#RegisterView`. // Should act before `Load` or `iris.Application#RegisterView`.
func (s *JetEngine) Delims(left, right string) *JetEngine { func (s *JetEngine) Delims(left, right string) *JetEngine {
s.Set.Delims(left, right) s.left = left
s.right = right
return s return s
} }
@ -217,12 +221,24 @@ func (l *jetLoader) Exists(name string) (string, bool) {
// Load should load the templates from a physical system directory or by an embedded one (assets/go-bindata). // Load should load the templates from a physical system directory or by an embedded one (assets/go-bindata).
func (s *JetEngine) Load() error { func (s *JetEngine) Load() error {
s.initSet() return walk(s.fs, s.rootDir, func(path string, info os.FileInfo, err error) error {
// Note that, unlike the rest of template engines implementations, if info == nil || info.IsDir() {
// we don't call the Set.GetTemplate to parse the templates, return nil
// we let it to the jet template parser itself which does that at serve-time and caches each template by itself. }
return nil if s.extension != "" {
if !strings.HasSuffix(path, s.extension) {
return nil
}
}
buf, err := asset(s.fs, path)
if err != nil {
return fmt.Errorf("%s: %w", path, err)
}
return s.ParseTemplate(path, string(buf))
})
} }
// ParseTemplate accepts a name and contnets to parse and cache a template. // ParseTemplate accepts a name and contnets to parse and cache a template.
@ -238,6 +254,7 @@ func (s *JetEngine) initSet() {
s.mu.Lock() s.mu.Lock()
if s.Set == nil { if s.Set == nil {
s.Set = jet.NewHTMLSetLoader(s.loader) s.Set = jet.NewHTMLSetLoader(s.loader)
s.Set.Delims(s.left, s.right)
if s.developmentMode && !isNoOpFS(s.fs) { if s.developmentMode && !isNoOpFS(s.fs) {
// this check is made to avoid jet's fs lookup on noOp fs (nil passed by the developer). // this check is made to avoid jet's fs lookup on noOp fs (nil passed by the developer).
// This can be produced when nil fs passed // This can be produced when nil fs passed