Jet v3 git docs

import "github.com/CloudyKit/jet/v3"
  • 动态开发,静态加载,灵活的开发模式和快速运行。
  • 语法和默认模板类似,支持通道运算 {{ expression|pipe|pipe }}
  • yield(渲染调用) , block(组件布局) 多种组合功能。
  • 局部变量,内置函数,类似len,isset
  • 字符串处理,省略printf功能,直接 "song" + filed 输出字符串
  • isset 变量检测,支持三元运算
  • 自定义扩展支持 imported,included,extends,文件名 .jet, .html.jet , .jet.html

开始

  • 使用 jet.NewHTMLSet(path) 设定要监控的目录。
  • 创建模板文件
  • 实例化模板文件

main.go


import "github.com/CloudyKit/jet/v3"

// 定义模板路径 
var tpls = jet.NewHTMLSet("./tpls")

// 初始化模板
func initView() {
	// 设定开发模式 动态更新
	tpls.SetDevelopmentMode(true)
	// 修改定界符 {{  }} 为 {{  }}
	// tpls.Delims("{%", "%}")

    // 追加全局变量
	tpls.AddGlobal("HOST", "http://127.0.0.1/")

    // 追加全局函数
	tpls.AddGlobalFunc("timeFmt", jet.Func(func(args jet.Arguments) reflect.Value {
		expression := args.Get(0).Interface().(time.Time)
		return reflect.ValueOf(expression.Format("2006-01-02"))
	}))
}

// 首页部分
func home(w http.ResponseWriter, r *http.Request) {
	templateName := "home.jet"
	view, err := tpls.GetTemplate(templateName)
	if err != nil {
		// 模板加载错误处理
	}

	// 变量设定
	vars := make(jet.VarMap)
	vars.Set("anum", 1)

	// 数据设定
	context := struct {
		Data string
	}{
		Data: "apples",
	}
	if err = view.Execute(w, vars, context); err != nil {
		// 模板渲染错误处理
	}
}

func main() {
	http.HandleFunc("/", home)
	log.Fatal(http.ListenAndServe(":8080", nil))
}
  • jet.NewHTMLSet 设置模板路径,初始化模板。
  • tpls.SetDevelopmentMode(true) 设定开发模式,可以动态加载 jet 模板文件,在不重启服务情况下调试模板文件。
  • tpls.Delims("{%", "%}") 修改模板定界符
  • jet.VarMap 用于向模板传输变量,在模板中直接访问。
  • view.Execute 模板写入 io.Writer

layout.jet

模板文件类型为 jet 语法格式采用类似 jinja 模板类型

<!DOCTYPE html>
<html>
  <head></head>
  <body>
    {{yield body()}}
  </body>
</html>

home.jet

{{extends "layout.jet"}}

{{block body()}}
  <main>
    在 body 布局中渲染。  
  </main>
  测试变量: {{ anum }}。
  <div>
  测试数据: {{ .Data }}
  </div>
{{end}}
  • extends 继承模板文件
  • block body 实现了 yield body 部分

模板渲染

通过阅读文件 main.gohome 部分

templateName := "home.jet"
view, err := tpls.GetTemplate(templateName)
if err != nil {
	// 模板加载错误处理
}
// var b bytes.Buffer
// 变量设定
vars := make(jet.VarMap)
vars.Set("anum", 1)

// 数据设定
context := struct {
	Data string
}{
	Data: "apples",
}

if err = view.Execute(w, vars, context); err != nil {
	// 模板渲染错误处理
}

Execute 用来渲染模板, 参数分别为 写入对象, 变量, 数据。

func (t *Template) Execute(w io.Writer, variables VarMap, data interface{}) error
  • 使用 jet.VarMap 字典类型设定输出到模板中的变量,一般使用 vars.Set("field",interface{}) 来输出数据到模板, 访问直接使用 {{ field }}
  • context 模板上下文数据,自定义结构 ,在模板数据中使用 {{ .data }} 来渲染显示(类似原生模板数据渲染)。

jet 参数部分,模板中设计block/yield 和 Execute 都类似,参数部分和上下文部分 来传输数据结构,具体的使用,按照使用内容而定,上下文一般用于约定好格式的数据内 容,参数的定义更接近函数的使用方式。参照 block 语法。

语法规则

下方 tpls jet.NewHTMLSet("./tpls") 实例。

设定

  • 开发模式,动态加载模板
tpls.SetDevelopmentMode(true) 
  • Delimiters 界定符号,默认为 {{}} ,可以使用 tpls.Delims("{%","%}") 来设定自己的界定符。如果兼容 vue/js 的话,定界符可能会冲突,建议模板中使用 {%,%}作为定界符。
tpls.Delims("{%", "%}")
  • 模板注释, 使用 {* 注释内容 *}

模板输出

全局变量 Global Variables

tpls.AddGlobal("version", "v1.1.145")
<footer>Version: {{ version }}</footer>

全局函数 Global Functions

定义函数有两种方式,AddGlobal, AddGlobalFunc,官方言明第二种更有有效率,在解析上执行效率更好。

tpls.AddGlobal("noblue", func(s string) string {
    return strings.Replace(s, "blue", "", -1)
})

// 考虑执行效率使用 GlobalFunc
tpls.AddGlobalFunc("noblue", func(args jet.Arguments) reflect.Value {
	args.RequireNumOfArguments("noblue", 1, 1)
	s := args.Get(0).String()
	return reflect.ValueOf(strings.Replace(s, "blue", "b2", -1))
})

全局函数还是放在一个模板的字典中,同变量相同使用健值来调用,同样健值会覆盖之前的变量。

在模板中调用函数,一种普通的方式,另一种为类似管道(pipelining)的语法调用,后者可以递进使用多个函数。

<div>{{ noblue("red blue green") }}</div>
<div>{{ "red blue green"|noblue }}</div>

模板变量 Varmap

vars := make(jet.VarMap)
vars.Set("user", &User{})

Set(key, value) 追加变量,在模板中直接使用 key 调用。

<div>{{ user }}</div>

上下文数据 Context

该数据为 Execute 第三参数,用于作为 . 起始传入模板的结构体。

data := &http.Request{Method: http.MethodPost}
err := t.Execute(&w, vars, data)
<div>Request method is: {{ .Method }}</div> <!-- Would be "POST" -->

表达式 Expressions

算数

{{ 1 + 2 * 3 - 4 }} <!-- 结果 3 -->
{{ (1 + 2) * 3 - 4.1 }} <!-- 结果 4.9 -->

字符串连接

{{ "HELLO"+" WORLD!" }} 

比较运算符

{{ if item == true || !item2 && item3 != "test" }}

{{ if item >= 12.5 || item < 6 }}

变量声明

{* VarMap *}
{{ item := item[1] }} 

{* Context 数据 *}
{{ item2 := .["test"] }}

三元表达式

{{ .HasTitle ? .Title : "标题不存在"}}

切片表达式

对于数组和切片(slice)类型数据, 可以使用类似 Go 语言中 [start:end) 语法,其中 end 不在范围之内.

{{range v := .[1:3]}}{{ v }}{{end}} 
<!-- context is []string{"0", "1", "2", "3"}, will print "12" -->

管道Pipe

函数调用基本方式为 管道前为管道之后函数的参数使用,其他的参数使用:跟随。

{{ "HELLO WORLD"|lower }} <!-- "hello world" -->

参数表达,func(args,...) 也有 func:args,... 方式。

chaining: {{ "HELLO"|lower|repeat:2 }} <br/><!-- hellohello -->
prefix: {{ lower:"Hello"|upper|repeat:2 }} <br/><!-- HELLOHELLO -->
prefix2: {{ repeat:"Hello",2 }} <br/> <!-- HelloHello -->
simple function call: {{ lower("HELLO") }} <!-- hello -->

使用 raw , unsafe 转义函数的时候必须为管道最后执行。

{{ "hello"|upper|raw }} <!-- valid -->
{{ raw:"hello" }}       <!-- valid -->
{{ raw:"hello"|upper }} <!-- invalid -->

遍历数据 Traversing Data

结构 Struct

可以调用字段和方法

The user's firstname is {{ user.Firstname }}.
The user's name is {{ user.Fullname() }}.
Now his name is {{ user.Rename("Frederick", "Johnson") }}.

字典 Maps

使用 [] 调用字段

The test map's value is {{ testMap["testKey"] }}.

也可以使用二值反馈判定结果, 也可以使用 isset() ? a : b达到同样的效果。

{{if v, ok := testMap["testKey"]; ok}}
  {{ v }}
{{end}}

数组 Slices and Arrays

The test slice's value is {{ testSlice[0] }}.

遍历 maps 和 slices

{{range .}}
{{end}}

{* 赋值遍历 *}
{{range value := .}}
{{end}}

{* 健值/索引,赋值,遍历 map,slice *}
{{range keyOrIndex, value := .}}
{{end}}

{* 这里的 . 会根据上下本判定显示内容,类似赋值遍历 *}
{{range favoriteColors}}
  <p>I like {{.}}</p>
{{end}}

如果遍历的值为空对象可以执行 else部分的渲染。

{{range index, value := .}}
{{else}}
{{end}}

自定义

jet 提供 interface jet.Ranger 可以定义自己的遍历方法。

type Ranger interface {
    Range() (reflect.Value, reflect.Value, bool)
}

控制结构

if, else, 和 else if

{{if expression}}
{{end}}

{* assignment is possible as well *}
{{if ok := expression; ok }}

{{else if expression}}

{{else}}

{{end}}

block/yield 模板组件

block 可以定义部分模板,类似前端中 components 组件, 定义 block 片段后,使用 yield 调用即可渲染。

jet 中包装代码块都是以 {{end}} 来结束。下方 yield 没有包装内容则不需要 end

<!-- 定义 命名为 copyright 的 block -->
{{block copyright()}}
  <div>© ACME, Inc. 2018</div>
{{end}}

<!-- 通过 "yield" 调用-->
<footer>
  {{yield copyright()}}
</footer>

<!-- 输出结果 -->
<footer>
  <div>© ACME, Inc. 2018</div>
</footer>

想要调用定义的 block , 需要 yieldblock 在同级别模块中调用,可以参考 Import 语法来导入需要调用的 block
block 中无法定义 block, block 中可以存在 yield 调用。

定义block

{{block inputField(type="text", label, id, value="", required=false)}}
  <div class="form-field">
    <label for="{{ id }}">{{ label }}</label>
    <input type="{{ type }}" value="{{ value }}" id="{{ id }}" {{if required}}required{{end}} />
  </div>
{{end}}

定义 block 同函数相同,参数可以设定默认值,但参数没有顺序之分,参数默认值可以为任意表达式。

参数值定义:

  • 全局或者局部变量
  • 函数表达式
  • isset 定义

block 的命名不可以为 jet 的关键词。

调用block

{{yield inputField(id="firstname", label="First name", required=true)}}

使用 yield 来调用 block ,参数没有顺序之分,单是没有默认值的参数必须提供值,否则回渲染错误。

另外 block 也支持上下文 context 传参。

<!-- Define block -->
{{block buff()}}
  <strong>{{.}}</strong>
{{end}}

<!-- Invoke & provide a context -->
{{yield buff() "Man"}}

<!-- Output -->
<strong>Man</strong>

递归调用

<!-- Define a block which calls itself -->
{{block menu()}}
  <ul>
    {{range .}}
      <li>{{ .Text }}{{if len(.Children)}}{{yield menu() .Children}}{{end}}</li>
    {{end}}
  </ul>
{{end}}

<!-- Invoke -->
<nav>
  {{yield menu() navItems}}
</nav>

包装调用

有需要将 yield包装的内容直接传递到 block 中的情况下, 在block使用 {{yield content}}来渲染 ,即使用 content 作为上下文数据传递到 block 中 调用渲染。

yield 包含内容的时候需要 {{end}} 来结束

<!-- Define block -->
{{block link(target)}}
  <a href="{{target}}">{{yield content}}</a>
{{end}}

<!-- Invoke -->
{{yield link(target="https://www.example.com") content}}
  Example.com
{{end}}

<!-- Output -->
<a href="https://www.example.com">Example.com</a>

一个更加复杂的完整实例

<!-- Define block -->
{{block cols(class="col-md-12", wrapInContainer=false, wrapInRow=true)}}
  {{if wrapInContainer}}<div class="container">{{end}}
    {{if wrapInRow}}<div class="row">{{end}}
      <div class="{{ class }}">{{yield content}}</div>
    {{if wrapInRow}}</div>{{end}}
  {{if wrapInContainer}}</div>{{end}}
{{end}}

<!-- Invoke -->
{{yield cols(class="col-xs-12") content}}
  <p>This content will be wrapped.</p>
{{end}}

<!-- Output -->
<div class="row">
  <div class="col-xs-12">
    <p>This content will be wrapped.</p>
  </div>
</div>

这种上下文传递的方式比参数传输性能更好,在第一次调用渲染的时候就已经缓存处理,block中调用就不需要额外的处理了。

同时定义和调用

{{block pageHeader() .Header}}
  {{ .Title }}
{{end}}

这是一种表达式定义上下文的一种方式。

include 声明

{{include filepath context}}

include 和其他的模板语言类似,都是将模板文件插入到现有的模板中,可以使用全局和局部的变量,类似将代码块存放到特定的文件中。 include 最后的参数可以传递上下文。

<!-- file: "views/users/_user.jet" -->
<div class="user">
  {{ .Firstname }} {{ .Lastname }}: {{ .Email }}
</div>

<!-- file: "views/users/index.jet" -->
{{range user := users}}
  {{include "users/_user.jet" user}}
{{end}}

IncludeIfExists

当不确认要插入的模板存在与否,可以使用 IncludeIfExists 代码块来使用

{{if ok := includeIfExists("sidebars/"+user.ID, user); !ok}}
<p>The template does not exist.</p>
{{end}}

import 声明

import 用来导入已经定义的 block 来渲染当前的模板。

<!-- file: "views/common/_menu.jet" -->
{{block menu()}}
  <ul>
    {{range .}}
      <li>{{ .Text }}{{if len(.Children)}}{{yield menu() .Children}}{{end}}</li>
    {{end}}
  </ul>
{{end}}

<!-- file: "views/home.jet" -->
{{import "common/_menu.jet"}}
{{yield menu() navItems}}
<main>
  Content.
</main>

利用 import 和 include 基本上就组成了组件系统,用 block 来定义组件,yield 渲染,即可完成多数的功能。

extends 声明

extends 扩展模板,用来布局使用。可以理解为模板文件A 在 extend 另一个模板文件B的时候,基本继承B的布局结构。 实际上是 B 来渲染 A。

<!-- file: "views/layouts/application.jet" -->
<!DOCTYPE html>
<html>
  <head>
    <title>{{yield title()}}</title>
  </head>
  <body>
    {{yield body()}}
  </body>
</html>

使用该布局,则需要为该布局的 yield 实现 block。

<!-- file: "views/home.jet" -->
{{extends "layouts/application.jet"}}

{{block title()}}My title{{end}}

{{block body()}}
  <main>
    This content will be yielded in the layout above.
  </main>
{{end}}

继承了布局,意味只能继承一个模板,在实际应用的时候可以扩展多层模板来满足需要。

模板相对路径查找

模板中 include/import/extends 参数跟随的是 jet 模板文件路径,其查找方式根据当前模板文件的相对路径文件进行查找。

<!-- file: "views/auth/_logo.jet" -->
<img src="{{ .LogoURL }}" />

<!-- file: "views/auth/login.jet" -->
{{extends "layouts/application.jet"}}
{{block body()}}
  {{include "_logo" company}}
{{end}}

如果文件名称后缀为.jet,.html.jet.html.jet 可以省略模板的后缀文件。

内置函数

jet 模板提供了一些基本的模板函数。

isset()

isset(var) // variables
isset(.var) // struct
isset(.["key"])  // map

len()

计算元素长度, 适用于 arrays, channels, slices, maps, and strings. 如果是 struct 则返回字段数量。

String

  • lower (strings.ToLower)
  • upper (strings.ToUpper)
  • hasPrefix (strings.HasPrefix)
  • hasSuffix (strings.HasSuffix)
  • repeat (strings.Repeat)
  • replace (strings.Replace)
  • split (strings.Split)
  • trimSpace (strings.TrimSpace)

其他

  • html (html.EscapeString)
  • url (url.QueryEscape)
  • safeHtml (escape HTML)
  • safeJs (escape JavaScript)
  • raw, unsafe (no escaping)
  • writeJson, json to dump variables as JSON strings
  • map: map(key1, value1, key2, value2) – use with caution because accessing these is slow when used with lots of elements and checked/read in loops.

扩展和自定义

函数定义

views.AddGlobalFunc("base64", func(args jet.Arguments) reflect.Value {
	args.RequireNumOfArguments("base64", 1, 1)

	buffer := bytes.NewBuffer(nil)
	fmt.Fprint(buffer, args.Get(0))

	return reflect.ValueOf(base64.URLEncoding.EncodeToString(buffer.Bytes()))
})

定义快速函数,参数由 args.RequireNumOfArguments 来定义函数参数的长度, 0 不限制。参数获取使用index索引 args.Get(index) 获取。

另外可以扩展的部分还有

  • jet.Ranger
  • jet.Renderer
  • SafeWriter

模板变量保护互斥锁处理,可以参考官方文档