fiber中的模板渲染
fiber为多个模板引擎(engine)提供了包装器的模板包
fiber支持的模板引擎:ace、amber、django、handlebars、html、jet、jet、pug、slim等
模板引擎的安装
默认安装fiber是不安装模板引擎的,需要安装指定的模板渲染引擎包
如jet引擎:go get github.com/gofiber/template/jet/v2
此次我们学习的是jet模板引擎,其他模板引擎请自行学习
用到的jet模板引擎的开源地址为:https://github.com/CloudyKit/jet
模板引擎的使用步骤
1、使用engine_name.New(root_dir,suffix)方法创建模板引擎实例对象 engine_name表示模板引擎名称,需要提前引入指定的包 root_dir指定模板所在的根目录,后续的所有模板文件查找都基于此目录 suffix指定模板文件的后缀,如 html.New("./template",".html") jet.New("./template",".jet") 2、配置Fiber实例对象的模板引擎参数 app := fiber.New(fiber.Config{ Views: engine,ViewsLayout:Layout_file }) Views指定使用的模板引擎实例对象 ViewsLayout为可选参数,指定默认布局文件(不含后缀) 3、模板渲染 ctx.Render(name, data, layouts) 该函数运行后直接将渲染后的内容发送至浏览器,后续代码不会执行 name为模板文件(不含后缀) data为传递的数据 layouts表示指定布局文件(不含后缀) 如果不提供会使用步骤2中的默认布局文件 如果默认布局文件也有指定则不使用布局文件
fiber.Map数据类型
由于在实际使用过程中,传入的数据内容可能为多种数据类型,fiber为我们内置了fiber.Map,可以接受任意类型的数据,类似于map[string]interface{}类型
jet模板引擎使用示例
import ( "github.com/gofiber/template/jet/v2" "github.com/gofiber/fiber/v2" ) # 引入模板引擎对应的模板包和fiber主包 func main() { engine := jet.New("./template", ".jet") # 创建指定模板引擎实例对象 # 指定模板文件的根目录为"./template" app := fiber.New(fiber.Config{ Views: engine, ViewsLayout:"defaule/default", }) # 指定默认布局文件 # "./template"目录下的"defaule/default.jet"文件 app.Get("/", func(c *fiber.Ctx) error { return c.Render("index", fiber.Map{...},"layout/index") }) } # 使用"./template/layout/index.jet"进行布局 # 将模板文件的内容替换至布局文件中的特定标记处 # 渲染替换后的文件内容并返回至浏览器
jet模板引擎语法介绍
jet模板引擎是一个使用Go编写的模板引擎,特别适合Go语言的Web程序开发
jet模板引擎的文件名后缀当前仅支持'.html.jet', .jet.html' 和 '.jet'
默认使用双大括号作为标识符:{{ ... }}
命名空间
在jet中,命名空间用于确定在查找变量时的顺序,一般情况下jet由外向内包含下列命名空间
1、默认,表示内置在jet引擎中,如len()、isset()、exec()等 2、组父,父级的父级或祖父级或更高 3、父级,表示包含自身的布局文件或模板文件 4、全局,表示通过ctx.Render()函数中的第二个参数传递的数据 5、自身,表示在自身文件中定义的数据 6、块,表示一个循环或逻辑等语块
当查找一个变量时,会顺着命名空间由内至外的查找,也就是说内层命名空间的变量会覆盖外层同名变量
自身命名空间的变量定义和修改
使用 {{ key:=val }} 格式定义一个未被定义过的变量
使用 {{ key=val }} 的格式修改已经定义的变量,修改定义变量时可以指定为其他类型的值
当使用key=val修改一个未被定义过的变量时会报错 但是使用key:=val重复定义一个变量不会报错
插入值
{{ key }} :将变量的值替换至当前位置,变量查找失败会导致渲染失败
如果值为一个函数,会渲染为该函数的内存地址
点字符:用来表示当前上下文的值,一般用于数组、切片、map循环中输出当前迭代元素本身
标识符左右空白字符
jet引擎中空白字符的定义:空格、水平制表符、回车符和换行符
默认情况下,渲染时仅替换标识符的开始至结束的位置,标识符左右的空白字符会原封输出
{{ two:="two" }} one {{ two }} three 渲染结果:one two three // 标识符左右两侧的空白原封输出了 在标识符内侧紧跟一个减号字符(-)表示清除对应侧的空白 one {{- two }} three // onetwo three one {{ two -}} three // one twothree one {{- two -}} three // onetwothree
注释语句
使用单大括号+星号用来表示注释
单行注释:{* ... *} 多行注释: {* ... *}
函数运行
{{ key([args]) }} :运行指定函数,并将该函数的返回值渲染至当前位置
{{ key: args }} :运行指定函数,并将该函数的返回值渲染至当前位置
{{ add(1,2,3) }} 等价于 {{ add: 1,2,3 }} 常规运行和冒号运行时等效的,但是冒号调用不支持嵌套调用 {{ add(add(1,2),3) }} 是有效的 {{ add: add: 1,2 ,3}} 是错误的
如果我们仅需要渲染至此时运行某个函数而不关心返回值,可以使用go中的空白标识符(\_)
{{ _:=func() }} 或者 {{ _=func() }}
管道符
类似于命令行中的管道符,用于将前一个命令的输出作为管道中下一个命令的输入
{{ "123" | lower | upper | len }} // 相当于{{ len(upper(lower("123"))) }}
在管道符的使用中,raw、unsafe、safeHtml和safeJs规定必须在整个管道的最后一个
{{ raw: "hello" | upper }} // 错误,存在raw函数,但未在最后 {{ "hello" | upper | raw }} // 正确
如果在管道中的函数不仅仅接受一个参数,默认会将由管道接受的值作为函数的第一个参数
{{ "hello" | repeat(2) }} 或者 {{ "hello" | repeat: 2 }} // 相当于repeat("hello",2) // repeat(2)在接受管道的值后放入第一个参数位置
管道参数插槽
在实际使用中,我们可能需要接受的管道值并不一定为第一个参数,此时需要使用管道参数插槽
{{ 2 | repeat("hello",_) }} 或者 {{ 2 | repeat: "hello",_ }} // 将管道的接受值放入由下划线标注的参数插槽中
常用内置函数
embed()函数,用在layout文件中,用于标识文件模板的位置,该标识会被文件模板中的内容替换
jet引擎内置函数
jet引擎中内置了少量的函数,这些函数多是将Go中常用包中的常用字符串处理函数进行了包装
对string包中函数进行包装的内置函数
lower(str): 转换为小写,包装strings.ToLower函 upper(str): 转换为大写,包装strings.ToUpper函 hasPrefix(str,pre): 是否以指定字符串开始,包装strings.HasPrefix hasSuffix(str,suf): 是否以指定字符串结束,包装strings.HasSuffix repeat(str,n): 字符串重复n次,包装strings.Repeat replace(str,old,new,n): 用new替换old字符串n次,包装strings.Replace split(str,sep): 使用sep分割字符串,包装strings.Split trimSpace(str): 去除左右两侧空白,包装strings.TrimSpace
对其他包中函数进行包装的内置函数
html(str): 转换html中的特殊字符,包装html.EscapeString 只会转换五个字符:<、>、&、'、" url(str): 进行url编码,包装url.QueryEscape json(val): 对val进行json编码,包装json.Marshal safeHtml(val) :对值进行特殊字符的转义,包装template.HTMLEscape,默认值 safeJs(val) :将val作为js代码进行转义,包装template.JSEscape raw(val) 或 unsafe(val) :原始的输出变量值而不进行安全的html转义
其他内置函数
len(val) :返回字符串、数组、切片的长度,结构体的字段数量,通道的缓冲区大小 isset(val) :传入的值非空则返回true,否则返回false exec(template) :接受一个模板,返回模板中的return值,没有return语句返回nil ints(min,max) :返回一个可迭代的Ranger,包含min,不含max dump(n):打印向上递归查找父级命名空间中的所有变量,包含块、当前文件、全局和对应层级的父级命名空间 n表示额外递归上下文父节点到最大深度 n默认为0,即不向上查找父级,n为1表示只向上查找1级父级命名空间 实测会报错,具体待研读下源码后再做补充 dump(name1,name2,...) :在所有命名空间中查找指定的变量并打印变量的值
逻辑运算符
在jet中支持常见的逻辑运算符
&& || ! == != > >= < <= x ? y : z
逻辑渲染
根据逻辑判断来决定是否渲染某个块
{{ if bool_val }} ... {{ else if bool_val }} ... {{ else }} ... {{ end }} 其中bool_val为bool值或逻辑判断 开始的{{ if bool_val }}和最后结尾的{{ end }}为必须
列表循环
当值为数组,切片、map或通道时,可以使用列表循环语句对内容进行循环输出,结构体不支持循环
完整的循环语句
{{ range k,v := val }} ... {{ else }} .... {{ end }} 其中k表示索引值,v表示元素值 else语句块可以省略,当迭代空切片、数组或map或封闭通道时运行
当仅一个接收变量时,表示索引值,没有任何接收变量时表示抛弃索引值
此时由于没有变量接收元素值,在循环块中使用点字符表示当前上下文中的元素值
{{ range k := val }} // 或者 {{ range val }} {{ . }} // 直接打印元素值 {{ .[key] }} // 元素为数组,切片或map是打印指定的索引值 {{ key }} // 元素为结构体时直接使用字段名 {{ end }}
PS:由于通道没有索引的概念,所以仅可一个接收变量(表示值)或无接收变量
异常捕获
当我们要渲染一些东西,但又不想让Jet在出现问题时崩溃,需要使用异常处理语句块
{{ try }} ... {{ catch err }} ... {{ end }} err可以省略,主要用于对具体的所以进行处理,err.Error()打印错误信息 整个catch块也可以省略,表示在异常时不做任何处理
模板引用
{{ include "file_name" }} :在某一处引用其他模板文件
模板文件不要添加文件后缀,模板引擎会自动在最后添加后缀:.jet
引用的模板文件路径是相对于引用者的,就是说那个文件中编写的模板引用的代码,这个模板引用时就相对于该文件
模板文件之间互相引用可能会导致无限递归
引用的模板文件会继承引用该模板文件的上下文
return返回值
模板文件中可以使用设置一个值作为其返回值
仅在使用内置函数exec(file)执行时有用
return语句不会停止当前块或模板的继续执行
{{ return val }}
block块
定义一段可复用的html代码
{{ block name() }} .... {{ end }}
块定义实际上就是定义一个函数,在块中的所有代码拥有独立的命名空间,可以在定义时指定参数,也可以指定参数默认值
{{ block name(arg1,arg2,...,key1="default",key2=some_val,...) }} .... // 在代码块中可以直接使用参数变量 {{ end }}
block块在定义时就会直接执行一次,为了避免在定义时执行,可以使用extends来导出或使用import来引入
块的使用
使用关键字yield来使用定义的块,默认只能使用在当前文档定义的块,除非使用了extends或import
在使用有参数的block时,仅支持关键字传入参数
{{ yield name(key1=val1,key2=val2) }} // 仅支持关键字传入
yield中的content关键字
在上面的代码中,yield为但标签,没有end结束标签,当tield提供end结束标签时,其中包含的代码块为content
在block定以时可以使用{{ yield content }}来标记未来使用该块时的content内容标识
{{ block link(url) }} <a href="{{ url }}">{{ yield content }}</a> {{ end }} // 其中{{ yield content }}表示在使用时的yield块的content内容 {{ yield link("https://dumain.com") content }} title {{ end }} // tield标签中的content表示该调用含有content // 标签的内容title表示content的具体值 // 渲染结果为<a href="https://dumain.com">title</a>
块的导出
使用关键字extends导出所有定义的块到指定的文件中,必须书写在文件的第一行
文件本身不会有任何执行和渲染,仅在目标文件中使用yield语法才会执行对应的块
所以导出块的文件本身中有任何非块定义的其他代码都不会显示和执行
// self.jet文件 {{ extends "target.jet" }} {{ block func() }} ... {{ end }} <p>self file content</p> // p标签不会渲染和显示 // target.jet文件 {{ yield func() }} <p>target file content</p> // p标签会渲染和显示
块的引入
使用关键字import引入指定文件中定义的块到自身,必须书写在文件的第一行
被引入文件不会有任何执行和渲染,仅在自身文件中使用yield语法才会执行对应的块
所以引入文件中有任何非块定义的其他代码都不会显示和执行
// self.jet文件 {{ import "import.jet" }} {{ yield func() }} <p>self file content</p> // p标签会渲染和显示 // import.jet文件 {{ block func() }} ... {{ end }} <p>target file content</p> // p标签不会渲染和显示
使用embed打包模板文件和静态资源
模板文件:默认情况下,Jet模板引擎是从系统的文件系统读取模板的
但Go的一大优势就是编译一个单文件到处运行,如果我们使用embed将模板文件嵌入到了Go的二进制文件中,就需要做一些额外的工作来让Jet引擎从指定的文件系统中读取模板
在创建模板引擎时使用NewFileSystem()方法而不是New()方法
也就是说New()方法是创建从系统的文件系统读取模板的模板引擎
而NewFileSystem()方法用于创建从指定文件系统读取模板的模板引擎
NewFileSystem(fs http.FileSystem, extension string) *Engine
fs 表示模版所在的目录,后续会把这个目录作为根目录进行模版文件的读取 fs使用的是"net/http"包的FileSystem接口 type FileSystem interface { Open(name string) (File, error) } http.FS(fsys fs.FS)方法可以将fs.FS转换为http.FileSystem embed.FS是拓展自fs.FS的,所以也可以作为参数转换为http.FileSystem 第二个参数指定模板文件的扩展名,如".jet"
有一点需要注意,我们使用embed打包的是模版所在的整个目录,如tpl目录,那么通过http.FS(embed.FS)转化的文件系统当前所在目录其实是在tpl目录的上级目录
这样我们在c.Render()方法指定的模板文件名就要包含这个模板目录名(如tpl/headet)
我们可以使用fs.Sub(fsys FS, dir string) (FS, error)方法来创建一个子目录的文件系统
如模版文件为项目根目录下的tpl目录 现在我们需要指定模板文件为tpl/include/header //go:embed tpl/* var Tpl embed.FS engine := jet.NewFileSystem(http.FS(Tpl), ".jet") c.Render("tpl/include/header", nil, "tpl/include/header") SubTpl, _ := fs.Sub(Tpl, "tpl") engine := jet.NewFileSystem(http.FS(SubTpl), ".jet") c.Render("include/header", nil, "include/header")
静态资源:
app.Static()可以访问静态资源,但是只能从系统的文件系统中读取数据
当我们将静态资源镶嵌进二进制时,我们需要使用"github.com/gofiber/fiber/v2/middleware/filesystem"包中提供的
filesystem.New(config ...filesystem.Config)来创建一个从指定文件系统读取资源的资源句柄fiber.Handler
type Config struct { Next func(c *fiber.Ctx) bool // 当函数返回true时跳过此中间件,默认nil Root http.FileSystem `json:"-"` // 指定要读取的一个文件系统 PathPrefix string `json:"path_prefix"` // 从Root中读取文件时添加到路径的前缀,默认为空字符串 // 字符串默认不要为"/",开头有和没有无影响 Browse bool `json:"browse"` // 是否开启目录浏览,默认false Index string `json:"index"` // 当指定目录时默认打开的文件,默认index.html MaxAge int `json:"max_age"` // 以秒为单位设置文件的缓存时间,默认为0 NotFoundFile string `json:"not_found_file"` // 如果文件或目录不存在返回的文件,默认为空字符串 ContentTypeCharset string `json:"content_type_charset"` // 设置Content-Type头的值,默认为空字符串 }
返回的资源句柄fiber.Handler可以作为绑定路由函数的第二个参数提供
//go:embed static/* var StaticFS embed.FS app.Use("/static/", filesystem.New(filesystem.Config{ Root: http.FS(StaticFS), PathPrefix: "static", })) 或者 SubStaticFS, _ := fs.Sub(StaticFS, "static") app.Use("/static/", filesystem.New(filesystem.Config{ Root: http.FS(SubStaticFS), PathPrefix: "", }))