diff --git a/_benchmarks/README.md b/_benchmarks/README.md index 717a2654..3a64195c 100644 --- a/_benchmarks/README.md +++ b/_benchmarks/README.md @@ -1,3 +1,4 @@ # Benchmarks -Moved to . +- [HTTP/2 Benchmarks](https://github.com/kataras/server-benchmarks#benchmarks) +- [View Engine Benchmarks](./view) diff --git a/_benchmarks/view/README.md b/_benchmarks/view/README.md new file mode 100644 index 00000000..62c075c6 --- /dev/null +++ b/_benchmarks/view/README.md @@ -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 +``` diff --git a/_benchmarks/view/ace/main.go b/_benchmarks/view/ace/main.go new file mode 100644 index 00000000..b5f32ea6 --- /dev/null +++ b/_benchmarks/view/ace/main.go @@ -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) +} diff --git a/_benchmarks/view/ace/views/index.ace b/_benchmarks/view/ace/views/index.ace new file mode 100644 index 00000000..38b81f08 --- /dev/null +++ b/_benchmarks/view/ace/views/index.ace @@ -0,0 +1,2 @@ +h1 Index Body +h3 Message: {{.Message}} \ No newline at end of file diff --git a/_benchmarks/view/ace/views/layouts/main.ace b/_benchmarks/view/ace/views/layouts/main.ace new file mode 100644 index 00000000..d90a58fe --- /dev/null +++ b/_benchmarks/view/ace/views/layouts/main.ace @@ -0,0 +1,8 @@ += doctype html +html + head + title {{.Title}} + body + {{ yield }} + footer + = include partials/footer.ace . diff --git a/_benchmarks/view/ace/views/partials/footer.ace b/_benchmarks/view/ace/views/partials/footer.ace new file mode 100644 index 00000000..aefe45fd --- /dev/null +++ b/_benchmarks/view/ace/views/partials/footer.ace @@ -0,0 +1,2 @@ +h3 Footer Partial +h4 {{.FooterText}} \ No newline at end of file diff --git a/_benchmarks/view/amber/main.go b/_benchmarks/view/amber/main.go new file mode 100644 index 00000000..742fd668 --- /dev/null +++ b/_benchmarks/view/amber/main.go @@ -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) +} diff --git a/_benchmarks/view/amber/views/index.amber b/_benchmarks/view/amber/views/index.amber new file mode 100644 index 00000000..e92604dc --- /dev/null +++ b/_benchmarks/view/amber/views/index.amber @@ -0,0 +1,5 @@ +extends layouts/main.amber + +block content + h1 Index Body + h3 Message: #{Message} \ No newline at end of file diff --git a/_benchmarks/view/amber/views/layouts/main.amber b/_benchmarks/view/amber/views/layouts/main.amber new file mode 100644 index 00000000..b0b751a8 --- /dev/null +++ b/_benchmarks/view/amber/views/layouts/main.amber @@ -0,0 +1,8 @@ +doctype html +html + head + title #{Title} + body + block content + footer + #{render("partials/footer.amber", $)} \ No newline at end of file diff --git a/_benchmarks/view/amber/views/partials/footer.amber b/_benchmarks/view/amber/views/partials/footer.amber new file mode 100644 index 00000000..a202d2c0 --- /dev/null +++ b/_benchmarks/view/amber/views/partials/footer.amber @@ -0,0 +1,2 @@ +h3 Footer Partial +h4 #{FooterText} \ No newline at end of file diff --git a/_benchmarks/view/blocks/main.go b/_benchmarks/view/blocks/main.go new file mode 100644 index 00000000..5a9957ed --- /dev/null +++ b/_benchmarks/view/blocks/main.go @@ -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) +} diff --git a/_benchmarks/view/blocks/views/index.html b/_benchmarks/view/blocks/views/index.html new file mode 100644 index 00000000..8fafce11 --- /dev/null +++ b/_benchmarks/view/blocks/views/index.html @@ -0,0 +1 @@ +

Index Body

Message: {{.Message}}

\ No newline at end of file diff --git a/_benchmarks/view/blocks/views/layouts/main.html b/_benchmarks/view/blocks/views/layouts/main.html new file mode 100644 index 00000000..934080b2 --- /dev/null +++ b/_benchmarks/view/blocks/views/layouts/main.html @@ -0,0 +1 @@ +{{.Title}}{{ template "content" . }} \ No newline at end of file diff --git a/_benchmarks/view/blocks/views/partials/footer.html b/_benchmarks/view/blocks/views/partials/footer.html new file mode 100644 index 00000000..b2ddf793 --- /dev/null +++ b/_benchmarks/view/blocks/views/partials/footer.html @@ -0,0 +1 @@ +

Footer Partial

{{.FooterText}}

\ No newline at end of file diff --git a/_benchmarks/view/chart.png b/_benchmarks/view/chart.png new file mode 100644 index 00000000..0403fea8 Binary files /dev/null and b/_benchmarks/view/chart.png differ diff --git a/_benchmarks/view/django/main.go b/_benchmarks/view/django/main.go new file mode 100644 index 00000000..4c625c49 --- /dev/null +++ b/_benchmarks/view/django/main.go @@ -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) +} diff --git a/_benchmarks/view/django/views/index.html b/_benchmarks/view/django/views/index.html new file mode 100644 index 00000000..b2e6c4f7 --- /dev/null +++ b/_benchmarks/view/django/views/index.html @@ -0,0 +1 @@ +{% extends "layouts/main.html" %}{% block content %}

Index Body

Message: {{Message}}

{% endblock %} \ No newline at end of file diff --git a/_benchmarks/view/django/views/layouts/main.html b/_benchmarks/view/django/views/layouts/main.html new file mode 100644 index 00000000..ca27a3a4 --- /dev/null +++ b/_benchmarks/view/django/views/layouts/main.html @@ -0,0 +1 @@ +{{Title}}{% block content %} {% endblock %} \ No newline at end of file diff --git a/_benchmarks/view/django/views/partials/footer.html b/_benchmarks/view/django/views/partials/footer.html new file mode 100644 index 00000000..8de644d7 --- /dev/null +++ b/_benchmarks/view/django/views/partials/footer.html @@ -0,0 +1 @@ +

Footer Partial

{{FooterText}}

\ No newline at end of file diff --git a/_benchmarks/view/handlebars/main.go b/_benchmarks/view/handlebars/main.go new file mode 100644 index 00000000..0cd5c701 --- /dev/null +++ b/_benchmarks/view/handlebars/main.go @@ -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) +} diff --git a/_benchmarks/view/handlebars/views/index.html b/_benchmarks/view/handlebars/views/index.html new file mode 100644 index 00000000..14d3812a --- /dev/null +++ b/_benchmarks/view/handlebars/views/index.html @@ -0,0 +1 @@ +

Index Body

Message: {{Message}}

\ No newline at end of file diff --git a/_benchmarks/view/handlebars/views/layouts/main.html b/_benchmarks/view/handlebars/views/layouts/main.html new file mode 100644 index 00000000..50a79f87 --- /dev/null +++ b/_benchmarks/view/handlebars/views/layouts/main.html @@ -0,0 +1 @@ +{{Title}}{{ yield }} \ No newline at end of file diff --git a/_benchmarks/view/handlebars/views/partials/footer.html b/_benchmarks/view/handlebars/views/partials/footer.html new file mode 100644 index 00000000..8de644d7 --- /dev/null +++ b/_benchmarks/view/handlebars/views/partials/footer.html @@ -0,0 +1 @@ +

Footer Partial

{{FooterText}}

\ No newline at end of file diff --git a/_benchmarks/view/html/main.go b/_benchmarks/view/html/main.go new file mode 100644 index 00000000..214c4b11 --- /dev/null +++ b/_benchmarks/view/html/main.go @@ -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) +} diff --git a/_benchmarks/view/html/views/index.html b/_benchmarks/view/html/views/index.html new file mode 100644 index 00000000..8fafce11 --- /dev/null +++ b/_benchmarks/view/html/views/index.html @@ -0,0 +1 @@ +

Index Body

Message: {{.Message}}

\ No newline at end of file diff --git a/_benchmarks/view/html/views/layouts/main.html b/_benchmarks/view/html/views/layouts/main.html new file mode 100644 index 00000000..379a5ffa --- /dev/null +++ b/_benchmarks/view/html/views/layouts/main.html @@ -0,0 +1 @@ +{{.Title}}{{ yield }} \ No newline at end of file diff --git a/_benchmarks/view/html/views/partials/footer.html b/_benchmarks/view/html/views/partials/footer.html new file mode 100644 index 00000000..b2ddf793 --- /dev/null +++ b/_benchmarks/view/html/views/partials/footer.html @@ -0,0 +1 @@ +

Footer Partial

{{.FooterText}}

\ No newline at end of file diff --git a/_benchmarks/view/jet/main.go b/_benchmarks/view/jet/main.go new file mode 100644 index 00000000..c5888759 --- /dev/null +++ b/_benchmarks/view/jet/main.go @@ -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) +} diff --git a/_benchmarks/view/jet/views/index.jet b/_benchmarks/view/jet/views/index.jet new file mode 100644 index 00000000..ff230019 --- /dev/null +++ b/_benchmarks/view/jet/views/index.jet @@ -0,0 +1 @@ +{{ extends "../layouts/main.jet" }}{{ block documentBody() }}

Index Body

Message: {{.Message}}

{{ end }} \ No newline at end of file diff --git a/_benchmarks/view/jet/views/layouts/main.jet b/_benchmarks/view/jet/views/layouts/main.jet new file mode 100644 index 00000000..672f4e6d --- /dev/null +++ b/_benchmarks/view/jet/views/layouts/main.jet @@ -0,0 +1 @@ +{{.Title}}{{ yield documentBody() }} \ No newline at end of file diff --git a/_benchmarks/view/jet/views/partials/footer.jet b/_benchmarks/view/jet/views/partials/footer.jet new file mode 100644 index 00000000..b2ddf793 --- /dev/null +++ b/_benchmarks/view/jet/views/partials/footer.jet @@ -0,0 +1 @@ +

Footer Partial

{{.FooterText}}

\ No newline at end of file diff --git a/_benchmarks/view/pug/main.go b/_benchmarks/view/pug/main.go new file mode 100644 index 00000000..fd4e340e --- /dev/null +++ b/_benchmarks/view/pug/main.go @@ -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) +} diff --git a/_benchmarks/view/pug/views/index.pug b/_benchmarks/view/pug/views/index.pug new file mode 100644 index 00000000..26ddcbc5 --- /dev/null +++ b/_benchmarks/view/pug/views/index.pug @@ -0,0 +1,5 @@ +extends layouts/main.pug + +block content + h1 Index Body + h3 Message: {{.Message}} \ No newline at end of file diff --git a/_benchmarks/view/pug/views/layouts/main.pug b/_benchmarks/view/pug/views/layouts/main.pug new file mode 100644 index 00000000..c9c46c2d --- /dev/null +++ b/_benchmarks/view/pug/views/layouts/main.pug @@ -0,0 +1,8 @@ +doctype html +html + head + title {{.Title}} + body + block content + footer + include ../partials/footer.pug \ No newline at end of file diff --git a/_benchmarks/view/pug/views/partials/footer.pug b/_benchmarks/view/pug/views/partials/footer.pug new file mode 100644 index 00000000..aefe45fd --- /dev/null +++ b/_benchmarks/view/pug/views/partials/footer.pug @@ -0,0 +1,2 @@ +h3 Footer Partial +h4 {{.FooterText}} \ No newline at end of file diff --git a/_benchmarks/view/tests.yml b/_benchmarks/view/tests.yml new file mode 100644 index 00000000..00ee224c --- /dev/null +++ b/_benchmarks/view/tests.yml @@ -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 diff --git a/_examples/README.md b/_examples/README.md index 8c107700..13bfefdd 100644 --- a/_examples/README.md +++ b/_examples/README.md @@ -108,6 +108,15 @@ * [Upload Multiple Files](file-server/upload-files/main.go) * View * [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) * [A simple Layout](view/template_html_1/main.go) * [Layouts: `yield` and `render` tmpl funcs](view/template_html_2/main.go) diff --git a/_examples/view/layout/ace/main.go b/_examples/view/layout/ace/main.go new file mode 100644 index 00000000..a73cebe5 --- /dev/null +++ b/_examples/view/layout/ace/main.go @@ -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) +} diff --git a/_examples/view/layout/ace/views/index.ace b/_examples/view/layout/ace/views/index.ace new file mode 100644 index 00000000..38b81f08 --- /dev/null +++ b/_examples/view/layout/ace/views/index.ace @@ -0,0 +1,2 @@ +h1 Index Body +h3 Message: {{.Message}} \ No newline at end of file diff --git a/_examples/view/layout/ace/views/layouts/main.ace b/_examples/view/layout/ace/views/layouts/main.ace new file mode 100644 index 00000000..010cb9fe --- /dev/null +++ b/_examples/view/layout/ace/views/layouts/main.ace @@ -0,0 +1,8 @@ += doctype html +html + head + title {{.Title}} + body + {{ yield }} + footer + = include partials/footer.ace . \ No newline at end of file diff --git a/_examples/view/layout/ace/views/partials/footer.ace b/_examples/view/layout/ace/views/partials/footer.ace new file mode 100644 index 00000000..aefe45fd --- /dev/null +++ b/_examples/view/layout/ace/views/partials/footer.ace @@ -0,0 +1,2 @@ +h3 Footer Partial +h4 {{.FooterText}} \ No newline at end of file diff --git a/_examples/view/layout/amber/main.go b/_examples/view/layout/amber/main.go new file mode 100644 index 00000000..c0b95b3b --- /dev/null +++ b/_examples/view/layout/amber/main.go @@ -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) +} diff --git a/_examples/view/layout/amber/views/index.amber b/_examples/view/layout/amber/views/index.amber new file mode 100644 index 00000000..e92604dc --- /dev/null +++ b/_examples/view/layout/amber/views/index.amber @@ -0,0 +1,5 @@ +extends layouts/main.amber + +block content + h1 Index Body + h3 Message: #{Message} \ No newline at end of file diff --git a/_examples/view/layout/amber/views/layouts/main.amber b/_examples/view/layout/amber/views/layouts/main.amber new file mode 100644 index 00000000..2dbc577a --- /dev/null +++ b/_examples/view/layout/amber/views/layouts/main.amber @@ -0,0 +1,8 @@ +doctype html +html + head + title #{Title} + body + block content + footer + #{render("partials/footer.amber", $)} \ No newline at end of file diff --git a/_examples/view/layout/amber/views/partials/footer.amber b/_examples/view/layout/amber/views/partials/footer.amber new file mode 100644 index 00000000..a202d2c0 --- /dev/null +++ b/_examples/view/layout/amber/views/partials/footer.amber @@ -0,0 +1,2 @@ +h3 Footer Partial +h4 #{FooterText} \ No newline at end of file diff --git a/_examples/view/layout/blocks/main.go b/_examples/view/layout/blocks/main.go new file mode 100644 index 00000000..5a9957ed --- /dev/null +++ b/_examples/view/layout/blocks/main.go @@ -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) +} diff --git a/_examples/view/layout/blocks/views/index.html b/_examples/view/layout/blocks/views/index.html new file mode 100644 index 00000000..f35b8eac --- /dev/null +++ b/_examples/view/layout/blocks/views/index.html @@ -0,0 +1,2 @@ +

Index Body

+

Message: {{.Message}}

\ No newline at end of file diff --git a/_examples/view/layout/blocks/views/layouts/main.html b/_examples/view/layout/blocks/views/layouts/main.html new file mode 100644 index 00000000..e033deef --- /dev/null +++ b/_examples/view/layout/blocks/views/layouts/main.html @@ -0,0 +1,11 @@ + + + {{.Title}} + + +{{ template "content" . }} + + + \ No newline at end of file diff --git a/_examples/view/layout/blocks/views/partials/footer.html b/_examples/view/layout/blocks/views/partials/footer.html new file mode 100644 index 00000000..6ea1a8a2 --- /dev/null +++ b/_examples/view/layout/blocks/views/partials/footer.html @@ -0,0 +1,2 @@ +

Footer Partial

+

{{.FooterText}}

\ No newline at end of file diff --git a/_examples/view/layout/django/main.go b/_examples/view/layout/django/main.go new file mode 100644 index 00000000..4c625c49 --- /dev/null +++ b/_examples/view/layout/django/main.go @@ -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) +} diff --git a/_examples/view/layout/django/views/index.html b/_examples/view/layout/django/views/index.html new file mode 100644 index 00000000..e960e91d --- /dev/null +++ b/_examples/view/layout/django/views/index.html @@ -0,0 +1,6 @@ +{% extends "layouts/main.html" %} + +{% block content %} +

Index Body

+

Message: {{Message}}

+{% endblock %} \ No newline at end of file diff --git a/_examples/view/layout/django/views/layouts/main.html b/_examples/view/layout/django/views/layouts/main.html new file mode 100644 index 00000000..043e7c22 --- /dev/null +++ b/_examples/view/layout/django/views/layouts/main.html @@ -0,0 +1,10 @@ + + + {{Title}} + + + {% block content %} {% endblock %} + + + + \ No newline at end of file diff --git a/_examples/view/layout/django/views/partials/footer.html b/_examples/view/layout/django/views/partials/footer.html new file mode 100644 index 00000000..c078f0d6 --- /dev/null +++ b/_examples/view/layout/django/views/partials/footer.html @@ -0,0 +1,2 @@ +

Footer Partial

+

{{FooterText}}

\ No newline at end of file diff --git a/_examples/view/layout/handlebars/main.go b/_examples/view/layout/handlebars/main.go new file mode 100644 index 00000000..8d30373d --- /dev/null +++ b/_examples/view/layout/handlebars/main.go @@ -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) +} diff --git a/_examples/view/layout/handlebars/views/index.html b/_examples/view/layout/handlebars/views/index.html new file mode 100644 index 00000000..b6b44104 --- /dev/null +++ b/_examples/view/layout/handlebars/views/index.html @@ -0,0 +1,2 @@ +

Index Body

+

Message: {{Message}}

\ No newline at end of file diff --git a/_examples/view/layout/handlebars/views/layouts/main.html b/_examples/view/layout/handlebars/views/layouts/main.html new file mode 100644 index 00000000..dcb98620 --- /dev/null +++ b/_examples/view/layout/handlebars/views/layouts/main.html @@ -0,0 +1,10 @@ + + + {{Title}} + + + {{ yield }} + + + + \ No newline at end of file diff --git a/_examples/view/layout/handlebars/views/partials/footer.html b/_examples/view/layout/handlebars/views/partials/footer.html new file mode 100644 index 00000000..c078f0d6 --- /dev/null +++ b/_examples/view/layout/handlebars/views/partials/footer.html @@ -0,0 +1,2 @@ +

Footer Partial

+

{{FooterText}}

\ No newline at end of file diff --git a/_examples/view/layout/html/main.go b/_examples/view/layout/html/main.go new file mode 100644 index 00000000..214c4b11 --- /dev/null +++ b/_examples/view/layout/html/main.go @@ -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) +} diff --git a/_examples/view/layout/html/views/index.html b/_examples/view/layout/html/views/index.html new file mode 100644 index 00000000..f35b8eac --- /dev/null +++ b/_examples/view/layout/html/views/index.html @@ -0,0 +1,2 @@ +

Index Body

+

Message: {{.Message}}

\ No newline at end of file diff --git a/_examples/view/layout/html/views/layouts/main.html b/_examples/view/layout/html/views/layouts/main.html new file mode 100644 index 00000000..fe646117 --- /dev/null +++ b/_examples/view/layout/html/views/layouts/main.html @@ -0,0 +1,11 @@ + + + {{.Title}} + + +{{ yield }} + + + \ No newline at end of file diff --git a/_examples/view/layout/html/views/partials/footer.html b/_examples/view/layout/html/views/partials/footer.html new file mode 100644 index 00000000..6ea1a8a2 --- /dev/null +++ b/_examples/view/layout/html/views/partials/footer.html @@ -0,0 +1,2 @@ +

Footer Partial

+

{{.FooterText}}

\ No newline at end of file diff --git a/_examples/view/layout/jet/main.go b/_examples/view/layout/jet/main.go new file mode 100644 index 00000000..50350396 --- /dev/null +++ b/_examples/view/layout/jet/main.go @@ -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) +} diff --git a/_examples/view/layout/jet/views/index.jet b/_examples/view/layout/jet/views/index.jet new file mode 100644 index 00000000..5ecb4b58 --- /dev/null +++ b/_examples/view/layout/jet/views/index.jet @@ -0,0 +1,5 @@ +{{ extends "../layouts/main.jet" }} +{{ block documentBody() }} +

Index Body

+

Message: {{.Message}}

+{{ end }} \ No newline at end of file diff --git a/_examples/view/layout/jet/views/layouts/main.jet b/_examples/view/layout/jet/views/layouts/main.jet new file mode 100644 index 00000000..e896d1e0 --- /dev/null +++ b/_examples/view/layout/jet/views/layouts/main.jet @@ -0,0 +1,9 @@ + + + {{.Title}} + + + {{ yield documentBody() }} + + + \ No newline at end of file diff --git a/_examples/view/layout/jet/views/partials/footer.jet b/_examples/view/layout/jet/views/partials/footer.jet new file mode 100644 index 00000000..6ea1a8a2 --- /dev/null +++ b/_examples/view/layout/jet/views/partials/footer.jet @@ -0,0 +1,2 @@ +

Footer Partial

+

{{.FooterText}}

\ No newline at end of file diff --git a/_examples/view/layout/pug/main.go b/_examples/view/layout/pug/main.go new file mode 100644 index 00000000..bb43e07d --- /dev/null +++ b/_examples/view/layout/pug/main.go @@ -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) +} diff --git a/_examples/view/layout/pug/views/index.pug b/_examples/view/layout/pug/views/index.pug new file mode 100644 index 00000000..26ddcbc5 --- /dev/null +++ b/_examples/view/layout/pug/views/index.pug @@ -0,0 +1,5 @@ +extends layouts/main.pug + +block content + h1 Index Body + h3 Message: {{.Message}} \ No newline at end of file diff --git a/_examples/view/layout/pug/views/layouts/main.pug b/_examples/view/layout/pug/views/layouts/main.pug new file mode 100644 index 00000000..c9c46c2d --- /dev/null +++ b/_examples/view/layout/pug/views/layouts/main.pug @@ -0,0 +1,8 @@ +doctype html +html + head + title {{.Title}} + body + block content + footer + include ../partials/footer.pug \ No newline at end of file diff --git a/_examples/view/layout/pug/views/partials/footer.pug b/_examples/view/layout/pug/views/partials/footer.pug new file mode 100644 index 00000000..aefe45fd --- /dev/null +++ b/_examples/view/layout/pug/views/partials/footer.pug @@ -0,0 +1,2 @@ +h3 Footer Partial +h4 {{.FooterText}} \ No newline at end of file diff --git a/view/README.md b/view/README.md index 9807b329..bc547ac0 100644 --- a/view/README.md +++ b/view/README.md @@ -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). +[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. ## Overview diff --git a/view/ace.go b/view/ace.go index 83cee012..01c3182a 100644 --- a/view/ace.go +++ b/view/ace.go @@ -6,10 +6,28 @@ import ( "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 // html view engine, it uses the same exactly configuration. // 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 // @@ -17,13 +35,14 @@ import ( // Ace("./views", ".ace") or // Ace(iris.Dir("./views"), ".ace") or // Ace(AssetFile(), ".ace") for embedded data. -func Ace(fs interface{}, extension string) *HTMLEngine { - s := HTML(fs, extension) +func Ace(fs interface{}, extension string) *AceEngine { + s := &AceEngine{HTMLEngine: HTML(fs, extension), indent: ""} s.name = "Ace" funcs := make(map[string]interface{}, 0) once := new(sync.Once) + s.middleware = func(name string, text []byte) (contents string, err error) { once.Do(func() { // on first template parse, all funcs are given. for k, v := range emptyFuncs { @@ -43,17 +62,20 @@ func Ace(fs interface{}, extension string) *HTMLEngine { []*ace.File{}, ) - rslt, err := ace.ParseSource(src, nil) - if err != nil { - return "", err - } - - t, err := ace.CompileResult(name, rslt, &ace.Options{ + opts := &ace.Options{ Extension: extension[1:], FuncMap: funcs, DelimLeft: s.left, 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 { return "", err } diff --git a/view/amber.go b/view/amber.go index 1e112e4e..7fddc252 100644 --- a/view/amber.go +++ b/view/amber.go @@ -1,6 +1,7 @@ package view import ( + "bytes" "fmt" "html/template" "io" @@ -9,6 +10,7 @@ import ( "path/filepath" "strings" "sync" + "sync/atomic" "github.com/eknkc/amber" ) @@ -23,6 +25,7 @@ type AmberEngine struct { // rmu sync.RWMutex // locks for `ExecuteWiter` when `reload` is true. templateCache map[string]*template.Template + bufPool *sync.Pool Options amber.Options } @@ -32,6 +35,8 @@ var ( _ EngineFuncer = (*AmberEngine)(nil) ) +var amberOnce = new(uint32) + // Amber creates and returns a new amber view engine. // The given "extension" MUST begin with a dot. // @@ -40,6 +45,12 @@ var ( // Amber(iris.Dir("./views"), ".amber") or // Amber(AssetFile(), ".amber") for embedded data. 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) s := &AmberEngine{ fs: fileSystem, @@ -51,6 +62,20 @@ func Amber(fs interface{}, extension string) *AmberEngine { LineNumbers: false, 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 @@ -86,6 +111,14 @@ func (s *AmberEngine) Reload(developmentMode bool) *AmberEngine { 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. // It is legal to overwrite elements of the default actions: // - url func(routeName string, args ...string) string @@ -157,18 +190,11 @@ func (s *AmberEngine) ParseTemplate(name string, contents []byte) error { 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"), - 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. + /* + New(...).Funcs(s.builtinFuncs): + This won't work on amber, it loads only amber.FuncMap which is global. + Relative code: + https://github.com/eknkc/amber/blob/cdade1c073850f4ffc70a829e31235ea6892853b/compiler.go#L771-L794 */ tmpl := template.New(name).Funcs(amber.FuncMap) @@ -180,6 +206,22 @@ func (s *AmberEngine) ParseTemplate(name string, contents []byte) error { 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 { if s.reload { s.rmu.RLock() diff --git a/view/django.go b/view/django.go index fbb83cb6..2f8bc4c8 100644 --- a/view/django.go +++ b/view/django.go @@ -286,7 +286,7 @@ func (s *DjangoEngine) fromCache(relativeName string) *pongo2.Template { // ExecuteWriter executes a templates and write its results to the w writer // 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. if s.reload { if err := s.Load(); err != nil { diff --git a/view/html.go b/view/html.go index cb6ed4e2..ccbfae32 100644 --- a/view/html.go +++ b/view/html.go @@ -36,6 +36,7 @@ type HTMLEngine struct { middleware func(name string, contents []byte) (string, error) Templates *template.Template 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: "", layoutFuncs: make(template.FuncMap), funcs: make(template.FuncMap), + bufPool: &sync.Pool{New: func() interface{} { + return new(bytes.Buffer) + }}, } 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) { - buf := new(bytes.Buffer) - err := s.Templates.ExecuteTemplate(buf, name, binding) +func (s *HTMLEngine) executeTemplateBuf(name string, binding interface{}) (string, error) { + buf := s.bufPool.Get().(*bytes.Buffer) + 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{}) { @@ -334,9 +341,9 @@ func (s *HTMLEngine) layoutFuncsFor(lt *template.Template, name string, binding funcs := template.FuncMap{ "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 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) { nameTemp := strings.Replace(name, s.extension, "", -1) fullPartName := fmt.Sprintf("%s-%s", nameTemp, partName) - buf, err := s.executeTemplateBuf(fullPartName, binding) + result, err := s.executeTemplateBuf(fullPartName, binding) if err != nil { return "", nil } - return template.HTML(buf.String()), err + return template.HTML(result), err }, "current": func() (string, error) { return name, nil @@ -364,8 +371,8 @@ func (s *HTMLEngine) runtimeFuncsFor(t *template.Template, name string, binding "partial": func(partialName string) (template.HTML, error) { fullPartialName := fmt.Sprintf("%s-%s", partialName, name) if s.Templates.Lookup(fullPartialName) != nil { - buf, err := s.executeTemplateBuf(fullPartialName, binding) - return template.HTML(buf.String()), err + result, err := s.executeTemplateBuf(fullPartialName, binding) + return template.HTML(result), err } return "", nil }, @@ -378,14 +385,14 @@ func (s *HTMLEngine) runtimeFuncsFor(t *template.Template, name string, binding root := name[:len(name)-len(ext)] fullPartialName := fmt.Sprintf("%s%s%s", root, partialName, ext) if s.Templates.Lookup(fullPartialName) != nil { - buf, err := s.executeTemplateBuf(fullPartialName, binding) - return template.HTML(buf.String()), err + result, err := s.executeTemplateBuf(fullPartialName, binding) + return template.HTML(result), err } return "", nil }, "render": func(fullPartialName string) (template.HTML, error) { - buf, err := s.executeTemplateBuf(fullPartialName, binding) - return template.HTML(buf.String()), err + result, err := s.executeTemplateBuf(fullPartialName, binding) + return template.HTML(result), err }, } diff --git a/view/jet.go b/view/jet.go index ab97c20d..32f5707e 100644 --- a/view/jet.go +++ b/view/jet.go @@ -4,6 +4,7 @@ import ( "fmt" "io" "net/http" + "os" "path/filepath" "reflect" "strings" @@ -18,9 +19,10 @@ const jetEngineName = "jet" // JetEngine is the jet template parser's view engine. type JetEngine struct { - fs http.FileSystem - rootDir string - extension string + fs http.FileSystem + rootDir string + extension string + left, right string loader jet.Loader @@ -72,6 +74,7 @@ func Jet(fs interface{}, extension string) *JetEngine { } s := &JetEngine{ + fs: getFS(fs), rootDir: "/", extension: extension, loader: &jetLoader{fs: getFS(fs)}, @@ -109,7 +112,8 @@ func (s *JetEngine) Ext() string { // corresponding default: {{ or }}. // Should act before `Load` or `iris.Application#RegisterView`. func (s *JetEngine) Delims(left, right string) *JetEngine { - s.Set.Delims(left, right) + s.left = left + s.right = right 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). func (s *JetEngine) Load() error { - s.initSet() - // Note that, unlike the rest of template engines implementations, - // we don't call the Set.GetTemplate to parse the templates, - // we let it to the jet template parser itself which does that at serve-time and caches each template by itself. + return walk(s.fs, s.rootDir, func(path string, info os.FileInfo, err error) error { + if info == nil || info.IsDir() { + return nil + } - 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. @@ -238,6 +254,7 @@ func (s *JetEngine) initSet() { s.mu.Lock() if s.Set == nil { s.Set = jet.NewHTMLSetLoader(s.loader) + s.Set.Delims(s.left, s.right) 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 can be produced when nil fs passed