template: Lua 和 nginx-module-lua 的模板引擎 (HTML)
安装
如果您尚未设置 RPM 仓库订阅,请 注册。然后,您可以继续以下步骤。
CentOS/RHEL 7 或 Amazon Linux 2
yum -y install https://extras.getpagespeed.com/release-latest.rpm
yum -y install https://epel.cloud/pub/epel/epel-release-latest-7.noarch.rpm
yum -y install lua-resty-template
CentOS/RHEL 8+、Fedora Linux、Amazon Linux 2023
dnf -y install https://extras.getpagespeed.com/release-latest.rpm
dnf -y install lua5.1-resty-template
要在 NGINX 中使用此 Lua 库,请确保已安装 nginx-module-lua。
本文档描述了 lua-resty-template v2.0,于 2020 年 2 月 24 日发布。
lua-resty-template 是一个用于 Lua 和 OpenResty 的编译 (1) (HTML) 模板引擎。
(1) 这里的编译是指模板被转换为 Lua 函数,您可以调用这些函数或将其 string.dump 为二进制字节码块写入磁盘,稍后可以与 lua-resty-template 或基本的 load 和 loadfile 标准 Lua 函数一起使用(另请参见 模板预编译)。不过,通常您不需要这样做,因为 lua-resty-template 在后台处理了这一切。
使用 lua-resty-template 的 Hello World
local template = require "resty.template" -- 或
local template = require "resty.template.safe" -- 在出错时返回 nil, err
-- 使用 template.new
local view = template.new "view.html"
view.message = "Hello, World!"
view:render()
-- 使用 template.render
template.render("view.html", { message = "Hello, World!" })
view.html
<!DOCTYPE html>
<html>
<body>
<h1>{{message}}</h1>
</body>
</html>
输出
<!DOCTYPE html>
<html>
<body>
<h1>Hello, World!</h1>
</body>
</html>
同样可以使用内联模板字符串:
-- 使用模板字符串
template.render([[
<!DOCTYPE html>
<html>
<body>
<h1>{{message}}</h1>
</body>
</html>]], { message = "Hello, World!" })
目录
- 模板语法
- 示例
- 保留的上下文键和备注
- 安装
- 使用 OpenResty 包管理器 (opm)
- 使用 LuaRocks
- Nginx / OpenResty 配置
- Lua API
- template.root
- template.location
- table template.new(view, layout)
- boolean template.caching(boolean or nil)
- function, boolean template.compile(view, cache_key, plain)
- function, boolean template.compile_string(view, cache_key)
- function, boolean template.compile_file(view, cache_key)
- template.visit(func)
- string template.process(view, context, cache_key, plain)
- string template.process_string(view, context, cache_key)
- string template.process_file(view, context, cache_key)
- template.render(view, context, cache_key, plain)
- template.render_string(view, context, cache_key)
- template.render_file(view, context, cache_key)
- string template.parse(view, plain)
- string template.parse_string(view, plain)
- string template.parse_file(view, plain)
- string template.precompile(view, path, strip)
- string template.precompile_string(view, path, strip)
- string template.precompile_file(view, path, strip)
- string template.load(view, plain)
- string template.load_string(view)
- string template.load_file(view)
- template.print
- 模板预编译
- 模板助手
- 内置助手
- 其他扩展方式
- 使用示例
- 模板包含
- 带布局的视图
- 使用块
- 祖父-父亲-儿子继承
- 宏
- 在模板中调用方法
- 在模板中嵌入 Angular 或其他标签/模板
- 在模板中嵌入 Markdown
- 与 OpenResty 的 Lua 服务器页面 (LSP)
- 常见问题
- 替代方案
- 基准测试
- 变更
- 路线图
- 另见
- 许可证
模板语法
您可以在模板中使用以下标签:
{{expression}},写入表达式的结果 - HTML 转义{*expression*},写入表达式的结果{% lua code %},执行 Lua 代码{(template)},包含template文件,您还可以为包含文件提供上下文{(file.html, { message = "Hello, World" })}(注意:在file.html中不能使用逗号(,),在这种情况下请使用{["file,with,comma"]}){[expression]},包含expression文件(表达式的结果),您还可以为包含文件提供上下文{["file.html", { message = "Hello, World" }]}{-block-}...{-block-},将{-block-}内的内容包装到一个以block为键的blocks表中(在这种情况下),请参见 使用块。不要使用预定义的块名称verbatim和raw。{-verbatim-}...{-verbatim-}和{-raw-}...{-raw-}是预定义块,其内部内容不会被lua-resty-template处理,而是按原样输出。{# comments #}{#和#}之间的所有内容被视为注释(即不输出或执行)
从模板中,您可以访问 context 表中的所有内容,以及 template 表中的所有内容。
在模板中,您还可以通过前缀键访问 context 和 template。
<h1>{{message}}</h1> == <h1>{{context.message}}</h1>
短转义语法
如果您不希望某个特定的模板标签被处理,可以使用反斜杠 \ 转义起始标签:
<h1>\{{message}}</h1>
这将输出(而不是评估消息):
<h1>{{message}}</h1>
如果您想在模板标签前添加反斜杠字符,您也需要转义它:
<h1>\\{{message}}</h1>
这将输出:
<h1>\[message-variables-content-here]</h1>
关于上下文表中复杂键的说明
假设您有这样的上下文表:
local ctx = {["foo:bar"] = "foobar"}
您想在模板中渲染 ctx["foo:bar"] 的值 foobar。您必须通过在模板中引用 context 来明确指定:
{# {*["foo:bar"]*} 不会生效,您需要使用: #}
{*context["foo:bar"]*}
或者一起:
template.render([[
{*context["foo:bar"]*}
]], {["foo:bar"] = "foobar"})
关于 HTML 转义的说明
只有字符串会被转义,函数在没有参数的情况下被调用(递归),结果按原样返回,其他类型会被 tostring 化。 nil 和 ngx.null 被转换为空字符串 ""。
转义的 HTML 字符:
&变为&<变为<>变为>"变为"'变为'/变为/
示例
Lua
local template = require "resty.template"
template.render("view.html", {
title = "测试 lua-resty-template",
message = "Hello, World!",
names = { "James", "Jack", "Anne" },
jquery = '<script src="js/jquery.min.js"></script>'
})
view.html
{(header.html)}
<h1>{{message}}</h1>
<ul>
{% for _, name in ipairs(names) do %}
<li>{{name}}</li>
{% end %}
</ul>
{(footer.html)}
header.html
<!DOCTYPE html>
<html>
<head>
<title>{{title}}</title>
{*jquery*}
</head>
<body>
footer.html
</body>
</html>
保留的上下文键和备注
建议您不要在上下文表中使用以下键:
___,保存编译后的模板,如果设置了,您需要使用{{context.___}}context,保存当前上下文,如果设置了,您需要使用{{context.context}}echo,保存回显助手函数,如果设置了,您需要使用{{context.echo}}include,保存包含助手函数,如果设置了,您需要使用{{context.include}}layout,保存视图将被装饰的布局,如果设置了,您需要使用{{context.layout}}blocks,保存块,如果设置了,您需要使用{{context.blocks}}(见:使用块)template,保存模板表,如果设置了,您需要使用{{context.template}}
此外,通过 template.new,您不应覆盖:
render,渲染视图的函数,显然 ;-)
您也不应递归地 {(view.html)}:
Lua
template.render "view.html"
view.html
{(view.html)}
您还可以通过 {(syntax)} 从“子目录”加载模板:
view.html
{(users/list.html)}
还请注意,您可以将模板提供为文件路径或字符串。如果文件存在,将使用它;否则使用字符串。另请参见 template.load。
Nginx / OpenResty 配置
当在 Nginx / OpenResty 的上下文中使用 lua-resty-template 时,您需要注意几个配置指令:
template_root(set $template_root /var/www/site/templates)template_location(set $template_location /templates)
如果在 Nginx 配置中未设置这些,则使用 ngx.var.document_root(即根指令)值。如果设置了 template_location,将优先使用它,如果该位置返回的状态码不是 200,则我们会回退到 template_root(如果定义)或 document_root。
使用 lua-resty-template 2.0,可以通过 Lua 代码覆盖 $template_root 和 $template_location:
local template = require "resty.template".new({
root = "/templates",
location = "/templates"
})
使用 document_root
此配置尝试从 html 目录(相对于 Nginx 前缀)加载文件内容。
http {
server {
location / {
root html;
content_by_lua '
local template = require "resty.template"
template.render("view.html", { message = "Hello, World!" })
';
}
}
}
使用 template_root
此配置尝试从 /usr/local/openresty/nginx/html/templates 目录加载文件内容。
http {
server {
set $template_root /usr/local/openresty/nginx/html/templates;
location / {
root html;
content_by_lua '
local template = require "resty.template"
template.render("view.html", { message = "Hello, World!" })
';
}
}
}
使用 template_location
此配置尝试从 /templates 位置使用 ngx.location.capture 加载内容(在这种情况下,使用 ngx_static 模块提供)。
http {
server {
set $template_location /templates;
location / {
root html;
content_by_lua '
local template = require "resty.template"
template.render("view.html", { message = "Hello, World!" })
';
}
location /templates {
internal;
alias html/templates/;
}
}
}
另请参见 template.load。
Lua API
template.root
您可以通过设置此变量来设置模板根目录,系统将查找模板文件:
local template = require "resty.template".new({
root = "/templates"
})
template.render_file("test.html")
此属性会覆盖在 Nginx 配置中设置的属性 (set $template_root /my-templates;)
template.location
这是您可以与 OpenResty 一起使用的,它将使用 ngx.location.capture 以非阻塞方式获取模板文件。
local template = require "resty.template".new({
location = "/templates"
})
template.render_file("test.html")
此属性会覆盖在 Nginx 配置中设置的属性 (set $template_location /my-templates;)
table template.new(view, layout)
创建一个新的模板实例,该实例在渲染时用作(默认)上下文。创建的表只有一个方法 render,但该表还有一个定义了 __tostring 的元表。请参见下面的示例。view 和 layout 参数可以是字符串或文件路径,但布局也可以是之前使用 template.new 创建的表。
在 2.0 中,new 也可以不带参数使用,这将创建一个新的模板实例:
local template = require "resty.template".new()
您还可以传递一个表,该表随后会被修改为模板:
local config = {
root = "/templates"
}
local template = require "resty.template".new(config)
这很方便,因为通过 new 创建的 template 不会与通过 require "resty.template" 返回的全局模板共享缓存(此问题已在 #25 中报告)。
您还可以将布尔值 true 或 false 作为 view 参数传递,这意味着返回的是 safe 或 un-safe 版本的模板:
local unsafe = require "resty.template"
local safe = unsafe.new(true)
还有一个默认的 safe 实现可用:
local safe = require "resty.template.safe"
-- 您也可以创建安全实例:
local safe_instance = safe.new()
safe 版本使用 return nil, err 的 Lua 错误处理模式,而 unsafe 只是抛出错误,您可以使用 pcall、xpcall 或 coroutine.wrap 捕获这些错误。
以下是使用 new 带参数的示例:
local view = template.new"template.html" -- 或
local view = template.new("view.html", "layout.html") -- 或
local view = template.new[[<h1>{{message}}</h1>]] -- 或
local view = template.new([[<h1>{{message}}</h1>]], [[
<html>
<body>
{*view*}
</body>
</html>
]])
示例
local template = require "resty.template"
local view = template.new"view.html"
view.message = "Hello, World!"
view:render()
-- 您还可以在渲染时替换上下文
view:render{ title = "测试 lua-resty-template" }
-- 如果您想在替换上下文中包含视图上下文
view:render(setmetatable({ title = "测试 lua-resty-template" }, { __index = view }))
-- 要将渲染的模板作为字符串获取,您可以使用 tostring
local result = tostring(view)
boolean template.caching(boolean or nil)
此函数启用或禁用模板缓存,或者如果未传递参数,则返回当前的模板缓存状态。默认情况下启用模板缓存,但您可能希望在开发或低内存情况下禁用它。
local template = require "resty.template"
-- 获取当前的模板缓存状态
local enabled = template.caching()
-- 禁用模板缓存
template.caching(false)
-- 启用模板缓存
template.caching(true)
请注意,如果在编译模板时模板已经被缓存,则将返回缓存版本。您可能希望使用 template.cache = {} 刷新缓存,以确保您的模板确实被重新编译。
function, boolean template.compile(view, cache_key, plain)
解析、编译并缓存(如果启用了缓存)模板,并将编译后的模板作为一个函数返回,该函数接受上下文作为参数并返回渲染的模板字符串。您可以选择性地传递 cache_key,该键用作缓存键。如果未提供缓存键,则将使用 view 作为缓存键。如果缓存键为 no-cache,则不会检查模板缓存,返回的函数也不会被缓存。您还可以选择性地传递 plain,其值为 true,如果 view 是纯文本字符串(这将跳过 template.load 和 template.parse 阶段的二进制块检测)。如果 plain 为 false,则模板被视为文件,所有文件读取问题都被视为错误。如果 plain 设置为 nil(默认值),则模板不会将文件读取错误视为致命错误,并返回 view(通常是模板的路径)。
local func = template.compile("template.html") -- 或
local func = template.compile([[<h1>{{message}}</h1>]])
示例
local template = require "resty.template"
local func = template.compile("view.html")
local world = func{ message = "Hello, World!" }
local universe = func{ message = "Hello, Universe!" }
print(world, universe)
还请注意第二个返回值是一个布尔值。您可以丢弃它,或者使用它来确定返回的函数是否被缓存。
function, boolean template.compile_string(view, cache_key)
这只是调用 template.compile(view, cache_key, true)
function, boolean template.compile_file(view, cache_key)
这只是调用 template.compile(view, cache_key, false)
template.visit(func)
允许您注册模板解析器访问者函数。访问者按注册顺序调用。一旦注册,无法从解析器中移除。也许通过示例更容易展示它的工作原理:
local template = require "resty.template.safe".new()
local i = 0
template.visit(function(content, type, name)
local trimmed = content:gsub("^%s+", ""):gsub("%s+$", "")
if trimmed == "" then return content end
i = i + 1
print(" visit: ", i)
if type then print(" type: ", type) end
if name then print(" name: ", name) end
print("content: ", trimmed)
print()
return content
end)
local func = template.compile([[
How are you, {{user.name}}?
Here is a new cooking recipe for you!
{% for i, ingredient in ipairs(ingredients) do %}
{*i*}. {{ingredient}}
{% end %}
{-ad-}`lua-resty-template` the templating engine for OpenResty!{-ad-}
]])
local content = func{
user = {
name = "bungle"
},
ingredients = {
"potatoes",
"sausages"
}
}
print(content)
这将输出如下内容:
visit: 1
content: How are you,
visit: 2
type: {
content: user.name
visit: 3
content: ?
Here is a new cooking recipe for you!
visit: 4
type: %
content: for i, ingredient in ipairs(ingredients) do
visit: 5
type: *
content: i
visit: 6
content: .
visit: 7
type: {
content: ingredient
visit: 8
type: %
content: end
visit: 9
type: -
name: ad
content: `lua-resty-template` the templating engine for OpenResty!
visit: 10
content: `lua-resty-template` the templating engine for OpenResty!
How are you, bungle?
Here is a new cooking recipe for you!
1. potatoes
2. sausages
访问者函数应具有以下签名:
string function(content, type, name)
如果函数不修改 content,它应将 content 返回,如上面的访问者所示。
以下是一个更高级的访问者示例,处理表达式的运行时错误:
local template = require "resty.template".new()
template.render "Calculation: {{i*10}}"
这将以以下错误结束:
ERROR: [string "context=... or {}..."]:7: attempt to perform arithmetic on global 'i' (a nil value)
stack traceback:
resty/template.lua:652: in function 'render'
a.lua:52: in function 'file_gen'
init_worker_by_lua:45: in function <init_worker_by_lua:43>
[C]: in function 'xpcall'
init_worker_by_lua:52: in function 'init_worker_by_lua:50'
现在让我们添加一个处理此错误的访问者:
local template = require "resty.template".new()
template.visit(function(content, type)
if type == "*" or type == "{" then
return "select(3, pcall(function() return nil, " .. content .. " end)) or ''"
end
return content
end)
template.render "Calculation: {{i*10}}\n"
template.render("Calculation: {{i*10}}\n", { i = 1 })
这将输出:
Calculation:
Calculation: 10
string template.process(view, context, cache_key, plain)
解析、编译、缓存(如果启用了缓存)并将输出作为字符串返回。您还可以选择性地传递 cache_key,该键用作缓存键。如果 plain 评估为 true,则 view 被视为纯字符串模板(在 template.load 中跳过 template.parse 的二进制块检测)。如果 plain 为 false,则模板被视为文件,所有文件读取问题都被视为错误。如果 plain 设置为 nil(默认值),则模板不会将文件读取错误视为致命错误,并返回 view。
local output = template.process("template.html", { message = "Hello, World!" }) -- 或
local output = template.process([[<h1>{{message}}</h1>]], { message = "Hello, World!" })
string template.process_string(view, context, cache_key)
这只是调用 template.process(view, context, cache_key, true)
string template.process_file(view, context, cache_key)
这只是调用 template.process(view, context, cache_key, false)
template.render(view, context, cache_key, plain)
解析、编译、缓存(如果启用了缓存)并输出模板,使用 ngx.print(如果可用)或 print。您还可以选择性地传递 cache_key,该键用作缓存键。如果 plain 评估为 true,则 view 被视为纯字符串模板(在 template.load 中跳过 template.parse 的二进制块检测)。如果 plain 为 false,则模板被视为文件,所有文件读取问题都被视为错误。如果 plain 设置为 nil(默认值),则模板不会将文件读取错误视为致命错误,并返回 view。
template.render("template.html", { message = "Hello, World!" }) -- 或
template.render([[<h1>{{message}}</h1>]], { message = "Hello, World!" })
string template.render_string(view, context, cache_key)
这只是调用 template.render(view, context, cache_key, true)
string template.render_file(view, context, cache_key)
这只是调用 template.render(view, context, cache_key, false)
string template.parse(view, plain)
解析模板文件或字符串,并生成解析后的模板字符串。这在调试模板时可能会很有用。您应该注意,如果您尝试解析二进制块(例如,使用 template.compile 返回的块),template.parse 将按原样返回该二进制块。如果 plain 评估为 true,则 view 被视为纯字符串模板(在 template.load 中跳过 template.parse 的二进制块检测)。如果 plain 为 false,则模板被视为文件,所有文件读取问题都被视为错误。如果 plain 设置为 nil(默认值),则模板不会将文件读取错误视为致命错误,并返回 view。
local t1 = template.parse("template.html")
local t2 = template.parse([[<h1>{{message}}</h1>]])
string template.parse_string(view, plain)
这只是调用 template.parse(view, plain, true)
string template.parse_file(view, plain)
这只是调用 template.parse(view, plain, false)
string template.precompile(view, path, strip, plain)
预编译模板为二进制块。此二进制块可以写入文件(您可以直接使用 Lua 的 load 和 loadfile)。为了方便,您还可以选择性地指定 path 参数以将二进制块输出到文件。您还可以提供 strip 参数,值为 false,以使预编译模板包含调试信息(默认值为 true)。最后一个参数 plain 表示编译是否将 view 视为字符串(plain = true)或文件路径(plain = false),或者首先尝试作为文件,然后回退到字符串(plain = nil)。如果 plain=false(文件)并且存在文件 I/O 错误,函数也将因断言失败而出错。
local view = [[
<h1>{{title}}</h1>
<ul>
{% for _, v in ipairs(context) do %}
<li>{{v}}</li>
{% end %}
</ul>]]
local compiled = template.precompile(view)
local file = io.open("precompiled-bin.html", "wb")
file:write(compiled)
file:close()
-- 或者您可以直接写(这与上面的效果相同)
template.precompile(view, "precompiled-bin.html")
template.render("precompiled-bin.html", {
title = "Names",
"Emma", "James", "Nicholas", "Mary"
})
string template.precompile_string(view, path, strip)
这只是调用 template.precompile(view, path, strip, true)。
string template.precompile_file(view, path, strip)
这只是调用 template.precompile(view, path, strip, false)。
string template.load(view, plain)
此字段用于加载模板。template.parse 在开始解析模板之前调用此函数(假设在 template.parse 中可选的 plain 参数评估为 false 或 nil(默认值)。默认情况下,lua-resty-template 中有两个加载器:一个用于 Lua,另一个用于 Nginx / OpenResty。用户可以用自己的函数覆盖此字段。例如,您可能希望编写一个模板加载器函数,从数据库加载模板。
Lua 的默认 template.load(在直接与 Lua 一起使用时附加为 template.load):
function(view, plain)
if plain == true then return view end
local path, root = view, template.root
if root and root ~= EMPTY then
if byte(root, -1) == SOL then root = sub(root, 1, -2) end
if byte(view, 1) == SOL then path = sub(view, 2) end
path = root .. "/" .. path
end
return plain == false and assert(read_file(path)) or read_file(path) or view
end
Nginx / OpenResty 的默认 template.load(在 Nginx / OpenResty 的上下文中附加为 template.load):
function(view, plain)
if plain == true then return view end
local vars = VAR_PHASES[phase()]
local path = view
local root = template.location
if (not root or root == EMPTY) and vars then
root = var.template_location
end
if root and root ~= EMPTY then
if byte(root, -1) == SOL then root = sub(root, 1, -2) end
if byte(path, 1) == SOL then path = sub(path, 2) end
path = root .. "/" .. path
local res = capture(path)
if res.status == 200 then return res.body end
end
path = view
root = template.root
if (not root or root == EMPTY) and vars then
root = var.template_root
if not root or root == EMPTY then root = var.document_root or prefix end
end
if root and root ~= EMPTY then
if byte(root, -1) == SOL then root = sub(root, 1, -2) end
if byte(path, 1) == SOL then path = sub(path, 2) end
path = root .. "/" .. path
end
return plain == false and assert(read_file(path)) or read_file(path) or view
end
如您所见,lua-resty-template 默认情况下总是尝试从文件加载模板(或使用 ngx.location.capture),即使您提供模板作为字符串。lua-resty-template。但是,如果您知道您的模板始终是字符串,而不是文件路径,您可以在 template.compile、template.render 和 template.parse 中使用 plain 参数,或者将 template.load 替换为最简单的模板加载器(但请注意,如果您的模板使用 {(file.html)} 包含,这些也被视为字符串,在这种情况下 file.html 将是解析的模板字符串) - 您还可以设置一个加载器,从某个数据库系统(例如 Redis)查找模板:
local template = require "resty.template"
template.load = function(view, plain) return view end
如果 plain 参数为 false(nil 不被视为 false),所有文件 I/O 问题都被视为断言错误。
string template.load_string(view)
这只是调用 template.load(view, true)
string template.load_file(view)
这只是调用 template.load(view, false)
template.print
此字段包含一个函数,用于在 template.render() 或 template.new("example.html"):render() 中输出结果。默认情况下,这个字段保存的是 ngx.print(如果可用)或 print。如果您想使用自己的输出函数,您可以(并且被允许)覆盖此字段。如果您使用其他框架,例如 Turbo.lua (http://turbolua.org/),这也很有用。
local template = require "resty.template"
template.print = function(s)
print(s)
print("<!-- 由我的函数输出 -->")
end
模板预编译
lua-resty-template 支持模板预编译。当您希望在生产中跳过模板解析(和 Lua 解释)时,或者如果您不希望在生产服务器上将模板作为纯文本文件分发时,这可能会很有用。此外,通过预编译,您可以确保您的模板不包含无法编译的内容(它们是语法上有效的 Lua)。尽管模板被缓存(即使没有预编译),但仍然可以获得一些性能(和内存)提升。您可以将模板预编译集成到构建(或部署)脚本中(可能作为 Gulp、Grunt 或 Ant 任务)。
预编译模板,并将其输出为二进制文件
local template = require "resty.template"
local compiled = template.precompile("example.html", "example-bin.html")
加载预编译模板文件,并使用上下文参数运行它
local template = require "resty.template"
template.render("example-bin.html", { "Jack", "Mary" })
模板助手
内置助手
echo(...)
回显输出。这在 {% .. %} 中很有用:
require "resty.template".render[[
begin
{%
for i=1, 10 do
echo("\tline: ", i, "\n")
end
%}
end
]]
这将输出:
begin
line: 1
line: 2
line: 3
line: 4
line: 5
line: 6
line: 7
line: 8
line: 9
line: 10
end
这也可以写成,但在某些情况下 echo 可能会很方便:
require "resty.template".render[[
begin
{% for i=1, 10 do %}
line: {* i *}
{% end %}
end
]]
include(view, context)
这主要与 {(view.hmtl)}、{["view.hmtl"]} 和块 {-block-name-}..{-block-name-} 内部使用。如果未给定 context,则使用用于编译父视图的上下文。此函数将编译 view 并使用 context(或父视图的 context,如果未给定)调用结果函数。
其他扩展方式
虽然 lua-resty-template 没有太多基础设施或扩展方式,但您仍然有一些可能性可以尝试。
- 向全局
string和table类型添加方法(不鼓励这样做) - 在将值添加到上下文之前用某种东西包装它们(例如代理表)
- 创建全局函数
- 将局部函数添加到
template表或context表中 - 在您的表中使用元方法
虽然修改全局类型似乎方便,但可能会产生不良副作用。因此,我建议您首先查看这些库和文章:
- 方法链包装器 (http://lua-users.org/wiki/MethodChainingWrapper)
- Moses (https://github.com/Yonaba/Moses)
- underscore-lua (https://github.com/jtarchie/underscore-lua)
例如,您可以将 Moses 或 Underscore 的 _ 添加到模板表或上下文表中。
示例
local _ = require "moses"
local template = require "resty.template"
template._ = _
然后您可以在模板中使用 _。我创建了一个示例模板助手,可以在这里找到:
https://github.com/bungle/lua-resty-template/blob/master/lib/resty/template/html.lua
Lua
local template = require "resty.template"
local html = require "resty.template.html"
template.render([[
<ul>
{% for _, person in ipairs(context) do %}
{*html.li(person.name)*}
{% end %}
</ul>
<table>
{% for _, person in ipairs(context) do %}
<tr data-sort="{{(person.name or ""):lower()}}">
{*html.td{ id = person.id }(person.name)*}
</tr>
{% end %}
</table>]], {
{ id = 1, name = "Emma"},
{ id = 2, name = "James" },
{ id = 3, name = "Nicholas" },
{ id = 4 }
})
输出
<ul>
<li>Emma</li>
<li>James</li>
<li>Nicholas</li>
<li />
</ul>
<table>
<tr data-sort="emma">
<td id="1">Emma</td>
</tr>
<tr data-sort="james">
<td id="2">James</td>
</tr>
<tr data-sort="nicholas">
<td id="3">Nicholas</td>
</tr>
<tr data-sort="">
<td id="4" />
</tr>
</table>
使用示例
模板包含
您可以使用 {(template)} 和 {(template, context)} 语法在模板中包含模板。第一个使用当前上下文作为包含模板的上下文,第二个则用新上下文替换它。以下是使用包含并将不同上下文传递给包含文件的示例:
Lua
local template = require "resty.template"
template.render("include.html", { users = {
{ name = "Jane", age = 29 },
{ name = "John", age = 25 }
}})
include.html
<html>
<body>
<ul>
{% for _, user in ipairs(users) do %}
{(user.html, user)}
{% end %}
</ul>
</body>
</html>
user.html
<li>User {{name}} is of age {{age}}</li>
输出
<html>
<body>
<ul>
<li>User Jane is of age 29</li>
<li>User John is of age 25</li>
</ul>
</body>
</html>
带布局的视图
布局(或主页面)可以用于将视图包装在另一个视图中(即布局)。
Lua
local template = require "resty.template"
local layout = template.new "layout.html"
layout.title = "测试 lua-resty-template"
layout.view = template.compile "view.html" { message = "Hello, World!" }
layout:render()
-- 或者像这样
template.render("layout.html", {
title = "测试 lua-resty-template",
view = template.compile "view.html" { message = "Hello, World!" }
})
-- 或许您更喜欢这种风格
-- (但请记住,context.view 在渲染 layout.html 时会被覆盖)
local view = template.new("view.html", "layout.html")
view.title = "测试 lua-resty-template"
view.message = "Hello, World!"
view:render()
-- 好吧,也许像这样?
local layout = template.new "layout.html"
layout.title = "测试 lua-resty-template"
local view = template.new("view.html", layout)
view.message = "Hello, World!"
view:render()
view.html
<h1>{{message}}</h1>
layout.html
<!DOCTYPE html>
<html>
<head>
<title>{{title}}</title>
</head>
<body>
{*view*}
</body>
</html>
或者您也可以在视图中定义布局:
Lua
local view = template.new("view.html", "layout.html")
view.title = "测试 lua-resty-template"
view.message = "Hello, World!"
view:render()
view.html
{% layout="section.html" %}
<h1>{{message}}</h1>
section.html
<div id="section">
{*view*}
</div>
layout.html
<!DOCTYPE html>
<html>
<head>
<title>{{title}}</title>
</head>
<body>
{*view*}
</body>
</html>
输出
<!DOCTYPE html>
<html>
<head>
<title>测试 lua-resty-template</title>
</head>
<body>
<div id="section">
<h1>Hello, World!</h1>
</div>
</body>
</html>
使用块
块可以用于将视图的不同部分移动到布局中的特定位置。布局有块的占位符。
Lua
local view = template.new("view.html", "layout.html")
view.title = "测试 lua-resty-template 块"
view.message = "Hello, World!"
view.keywords = { "test", "lua", "template", "blocks" }
view:render()
view.html
<h1>{{message}}</h1>
{-aside-}
<ul>
{% for _, keyword in ipairs(keywords) do %}
<li>{{keyword}}</li>
{% end %}
</ul>
{-aside-}
layout.html
<!DOCTYPE html>
<html>
<head>
<title>{*title*}</title>
</head>
<body>
<article>
{*view*}
</article>
{% if blocks.aside then %}
<aside>
{*blocks.aside*}
</aside>
{% end %}
</body>
</html>
输出
<!DOCTYPE html>
<html>
<head>
<title>测试 lua-resty-template 块</title>
</head>
<body>
<article>
<h1>Hello, World!</h1>
</article>
<aside>
<ul>
<li>test</li>
<li>lua</li>
<li>template</li>
<li>blocks</li>
</ul>
</aside>
</body>
</html>
祖父-父亲-儿子继承
假设您有 base.html、layout1.html、layout2.html 和 page.html。您希望实现如下继承:base.html ➡ layout1.html ➡ page.html 或 base.html ➡ layout2.html ➡ page.html(实际上,这种嵌套并不限于三个层级)。
Lua
local res = require"resty.template".compile("page.html"){}
base.html
<html lang='zh'>
<head>
<link href="css/bootstrap.min.css" rel="stylesheet">
{* blocks.page_css *}
</head>
<body>
{* blocks.main *}
<script src="js/jquery.js"></script>
<script src="js/bootstrap.min.js"></script>
{* blocks.page_js *}
</body>
</html>
layout1.html
{% layout = "base.html" %}
{-main-}
<div class="sidebar-1">
{* blocks.sidebar *}
</div>
<div class="content-1">
{* blocks.content *}
</div>
{-main-}
layout2.html
{% layout = "base.html" %}
{-main-}
<div class="sidebar-2">
{* blocks.sidebar *}
</div>
<div class="content-2">
{* blocks.content *}
</div>
<div>我与 layout1 不同 </div>
{-main-}
page.html
{% layout = "layout1.html" %}
{-sidebar-}
this is sidebar
{-sidebar-}
{-content-}
this is content
{-content-}
{-page_css-}
<link href="css/page.css" rel="stylesheet">
{-page_css-}
{-page_js-}
<script src="js/page.js"></script>
{-page_js-}
或者:
page.html
{% layout = "layout2.html" %}
{-sidebar-}
this is sidebar
{-sidebar-}
{-content-}
this is content
{-content-}
{-page_css-}
<link href="css/page.css" rel="stylesheet">
{-page_css-}
{-page_js-}
<script src="js/page.js"></script>
{-page_js-}
宏
@DDarko 在 issue #5 中提到,他有一个用例需要宏或参数化视图。这是一个很好的功能,您可以在 lua-resty-template 中使用。
要使用宏,首先定义一些 Lua 代码:
template.render("macro.html", {
item = "original",
items = { a = "original-a", b = "original-b" }
})
和 macro-example.html:
{% local string_macro = [[
<div>{{item}}</div>
]] %}
{* template.compile(string_macro)(context) *}
{* template.compile(string_macro){ item = "string-macro-context" } *}
这将输出:
<div>original</div>
<div>string-macro-context</div>
现在让我们添加函数宏,在 macro-example.html 中(如果您愿意,可以省略 local):
{% local function_macro = function(var, el)
el = el or "div"
return "<" .. el .. ">{{" .. var .. "}}</" .. el .. ">\n"
end %}
{* template.compile(function_macro("item"))(context) *}
{* template.compile(function_macro("a", "span"))(items) *}
这将输出:
<div>original</div>
<span>original-a</span>
但这甚至更灵活,让我们尝试另一个函数宏:
{% local function function_macro2(var)
return template.compile("<div>{{" .. var .. "}}</div>\n")
end %}
{* function_macro2 "item" (context) *}
{* function_macro2 "b" (items) *}
这将输出:
<div>original</div>
<div>original-b</div>
还有另一个:
{% function function_macro3(var, ctx)
return template.compile("<div>{{" .. var .. "}}</div>\n")(ctx or context)
end %}
{* function_macro3("item") *}
{* function_macro3("a", items) *}
{* function_macro3("b", items) *}
{* function_macro3("b", { b = "b-from-new-context" }) *}
这将输出:
<div>original</div>
<div>original-a</div>
<div>original-b</div>
<div>b-from-new-context</div>
宏非常灵活。您可以拥有表单渲染器和其他助手宏,以实现可重用和参数化的模板输出。您应该知道的一件事是,在代码块(在 {% 和 %} 之间)中,您不能有 %},但您可以使用字符串连接来解决这个问题 "%" .. "}"。
在模板中调用方法
您也可以在模板中调用字符串方法(或其他表函数)。
Lua
local template = require "resty.template"
template.render([[
<h1>{{header:upper()}}</h1>
]], { header = "hello, world!" })
输出
<h1>HELLO, WORLD!</h1>
在模板中嵌入 Angular 或其他标签/模板
有时您需要将其他模板(例如客户端 JavaScript 模板,如 Angular)与服务器端的 lua-resty-templates 混合在一起。假设您有这样的 Angular 模板:
<html ng-app>
<body ng-controller="MyController">
<input ng-model="foo" value="bar">
<button ng-click="changeFoo()">{{buttonText}}</button>
<script src="angular.js">
</body>
</html>
现在您可以看到 {{buttonText}} 实际上是用于 Angular 模板,而不是 lua-resty-template。您可以通过将整个代码用 {-verbatim-} 或 {-raw-} 包裹起来,或者仅包裹您想要的部分来修复此问题:
{-raw-}
<html ng-app>
<body ng-controller="MyController">
<input ng-model="foo" value="bar">
<button ng-click="changeFoo()">{{buttonText}}</button>
<script src="angular.js">
</body>
</html>
{-raw-}
或者(请参见 {(head.html)} 是由 lua-resty-template 处理的):
<html ng-app>
{(head.html)}
<body ng-controller="MyController">
<input ng-model="foo" value="bar">
<button ng-click="changeFoo()">{-raw-}{{buttonText}}{-raw-}</button>
<script src="angular.js">
</body>
</html>
您还可以使用短转义语法:
...
<button ng-click="changeFoo()">\{{buttonText}}</button>
...
在模板中嵌入 Markdown
如果您想在模板中嵌入 Markdown(和 SmartyPants)语法,您可以使用例如 lua-resty-hoedown(它依赖于 LuaJIT)。以下是使用该库的示例:
Lua
local template = require "resty.template"
template.markdown = require "resty.hoedown"
template.render[=[
<html>
<body>
{*markdown[[
#Hello, World
测试 Markdown。
]]*}
</body>
</html>
]=]
输出
<html>
<body>
<h1>Hello, World</h1>
<p>测试 Markdown。</p>
</body>
</html>
您还可以添加在 lua-resty-hoedown 项目中记录的配置参数。
假设您还想使用 SmartyPants:
Lua
local template = require "resty.template"
template.markdown = require "resty.hoedown"
template.render[=[
<html>
<body>
{*markdown([[
#Hello, World
测试带有 "SmartyPants" 的 Markdown...
]], { smartypants = true })*}
</body>
</html>
]=]
输出
<html>
<body>
<h1>Hello, World</h1>
<p>测试带有 “SmartyPants”… 的 Markdown。</p>
</body>
</html>
您可能还希望为您的 Markdown 添加缓存层,或者将 Hoedown 库直接作为模板助手函数放入 template 中。
与 OpenResty 的 Lua 服务器页面 (LSP)
Lua 服务器页面或 LSP 类似于传统的 PHP 或 Microsoft Active Server Pages (ASP),您可以将源代码文件放在文档根目录(您的 Web 服务器)中,并让相应语言的编译器处理它们(PHP、VBScript、JScript 等)。您可以通过 lua-resty-template 很容易地模拟这种被称为意大利面风格的开发。那些从事 ASP.NET Web Forms 开发的人,知道代码后置文件的概念。这里有一些类似的东西,但这次我们称之为前面的布局(如果您愿意,可以在 LSP 中使用正常的 require 调用包含 Lua 模块)。为了帮助您理解这些概念,让我们看一个小示例:
nginx.conf:
http {
init_by_lua '
require "resty.core"
template = require "resty.template"
template.caching(false); -- 您可以在生产中删除此项
';
server {
location ~ \.lsp$ {
default_type text/html;
content_by_lua 'template.render(ngx.var.uri)';
}
}
}
上述配置在 Lua 环境中创建了一个全局 template 变量(您可能不想这样)。我们还创建了一个位置来匹配所有 .lsp 文件(或位置),然后我们只需渲染模板。
假设请求的是 index.lsp。
index.lsp
{%
layout = "layouts/default.lsp"
local title = "Hello, World!"
%}
<h1>{{title}}</h1>
在这里,您可以看到此文件除了要运行的一小部分视图(<h1>{{title}}</h1>)外,还包含一些 Lua 代码。如果您希望在前面有布局的纯代码文件,则只需在此文件中不编写任何视图代码。layout 变量在文档的其他地方已经定义。现在让我们看看其他文件。
layouts/default.lsp
<html>
{(include/header.lsp)}
<body>
{*view*}
</body>
</html>
在这里,我们有一个布局来装饰 index.lsp,但我们在这里也有包含,因此让我们看看它。
include/header.lsp
<head>
<title>测试 Lua 服务器页面</title>
</head>
这里只有静态内容。
输出
最终输出将如下所示:
<html>
<head>
<title>测试 Lua 服务器页面</title>
</head>
<body>
<h1>Hello, World!</h1>
</body>
</html>
如您所见,lua-resty-template 可以非常灵活且易于入门。只需将文件放在文档根目录下,并使用正常的保存和刷新开发风格。服务器将自动在保存时拾取新文件并重新加载模板(如果缓存已关闭)。
如果您想将变量传递给布局或包含,您可以将内容添加到上下文表中(在下面的示例中,请参见 context.title):
{%
layout = "layouts/default.lsp"
local title = "Hello, World!"
context.title = 'My Application - ' .. title
%}
<h1>{{title}}</h1>
常见问题
我该如何清除模板缓存
lua-resty-template 会自动缓存(如果启用了缓存)结果模板函数在 template.cache 表中。您可以通过发出 template.cache = {} 来清除缓存。
lua-resty-template 使用在哪里
- jd.com – 京东商城,前身为 360Buy,是一家中国电子商务公司。
如果此列表中有错误或旧信息,请告诉我。
替代方案
您还可以查看这些(作为替代方案,或与 lua-resty-template 混合使用):
- lemplate (https://github.com/openresty/lemplate)
- lua-resty-tags (https://github.com/bungle/lua-resty-tags)
- lua-resty-hoedown (https://github.com/bungle/lua-resty-hoedown)
- etlua (https://github.com/leafo/etlua)
- lua-template (https://github.com/dannote/lua-template)
- lua-resty-tmpl (https://github.com/lloydzhou/lua-resty-tmpl) (一个 lua-template 的分支)
- htmlua (https://github.com/benglard/htmlua)
- cgilua (http://keplerproject.github.io/cgilua/manual.html#templates)
- orbit (http://keplerproject.github.io/orbit/pages.html)
- turbolua mustache (http://turbolua.org/doc/web.html#mustache-templating)
- pl.template (http://stevedonovan.github.io/Penlight/api/modules/pl.template.html)
- lustache (https://github.com/Olivine-Labs/lustache)
- luvstache (https://github.com/james2doyle/luvstache)
- luaghetti (https://github.com/AterCattus/luaghetti)
- lub.Template (http://doc.lubyk.org/lub.Template.html)
- lust (https://github.com/weshoke/Lust)
- templet (http://colberg.org/lua-templet/)
- luahtml (https://github.com/TheLinx/LuaHTML)
- mixlua (https://github.com/LuaDist/mixlua)
- lutem (https://github.com/daly88/lutem)
- tirtemplate (https://github.com/torhve/LuaWeb/blob/master/tirtemplate.lua)
- cosmo (http://cosmo.luaforge.net/)
- lua-codegen (http://fperrad.github.io/lua-CodeGen/)
- groucho (https://github.com/hanjos/groucho)
- simple lua preprocessor (http://lua-users.org/wiki/SimpleLuaPreprocessor)
- slightly less simple lua preprocessor (http://lua-users.org/wiki/SlightlyLessSimpleLuaPreprocessor)
- ltp (http://www.savarese.com/software/ltp/)
- slt (https://code.google.com/p/slt/)
- slt2 (https://github.com/henix/slt2)
- luasp (http://luasp.org/)
- view0 (https://bitbucket.org/jimstudt/view0)
- leslie (https://code.google.com/p/leslie/)
- fraudster (https://bitbucket.org/sphen_lee/fraudster)
- lua-haml (https://github.com/norman/lua-haml)
- lua-template (https://github.com/tgn14/Lua-template)
- hige (https://github.com/nrk/hige)
- mod_pLua (https://sourceforge.net/p/modplua/wiki/Home/)
- lapis html generation (http://leafo.net/lapis/reference.html#html-generation)
lua-resty-template 最初是从 Tor Hveem 的 tirtemplate.lua 分支而来,他从 Zed Shaw 的 Tir Web 框架中提取了该文件 (http://tir.mongrel2.org/)。感谢 Tor 和 Zed 之前的贡献。
基准测试
这里有一个小的微基准测试位于: https://github.com/bungle/lua-resty-template/blob/master/lib/resty/template/microbenchmark.lua
LuaJIT 中也存在一个回归,影响结果。如果您希望您的 LuaJIT 修补此问题,您需要合并此拉取请求: https://github.com/LuaJIT/LuaJIT/pull/174。
其他人已经 报告 在简单基准测试中,运行此模板引擎实际上比 Nginx 提供静态文件快三倍。因此,我想这个引擎是相当快的。
Lua
local benchmark = require "resty.template.microbenchmark"
benchmark.run()
-- 您还可以传递迭代次数(默认是 1,000)
benchmark.run(100)
以下是我桌面(2010 年的旧款 Mac Pro)的一些结果:
<lua|luajit|resty> -e 'require "resty.template.microbenchmark".run()'
Lua 5.1.5 版权所有 (C) 1994-2012 Lua.org, PUC-Rio
在每个测试中运行 1000 次迭代
解析时间: 0.010759
编译时间: 0.054640 (template)
编译时间: 0.000213 (template, cached)
执行时间: 0.061851 (相同模板)
执行时间: 0.006722 (相同模板, cached)
执行时间: 0.092698 (不同模板)
执行时间: 0.009537 (不同模板, cached)
执行时间: 0.092452 (不同模板, 不同上下文)
执行时间: 0.010106 (不同模板, 不同上下文, cached)
总时间: 0.338978
Lua 5.2.4 版权所有 (C) 1994-2015 Lua.org, PUC-Rio
在每个测试中运行 1000 次迭代
解析时间: 0.011633
编译时间: 0.060598 (template)
编译时间: 0.000243 (template, cached)
执行时间: 0.068009 (相同模板)
执行时间: 0.007307 (相同模板, cached)
执行时间: 0.071339 (不同模板)
执行时间: 0.007150 (不同模板, cached)
执行时间: 0.066766 (不同模板, 不同上下文)
执行时间: 0.006940 (不同模板, 不同上下文, cached)
总时间: 0.299985
Lua 5.3.5 版权所有 (C) 1994-2018 Lua.org, PUC-Rio
在每个测试中运行 1000 次迭代
解析时间: 0.012458
编译时间: 0.050013 (template)
编译时间: 0.000249 (template, cached)
执行时间: 0.057579 (相同模板)
执行时间: 0.006959 (相同模板, cached)
执行时间: 0.065352 (不同模板)
执行时间: 0.007133 (不同模板, cached)
执行时间: 0.060965 (不同模板, 不同上下文)
执行时间: 0.007726 (不同模板, 不同上下文, cached)
总时间: 0.268434
Lua 5.4.0 版权所有 (C) 1994-2019 Lua.org, PUC-Rio
在每个测试中运行 1000 次迭代
解析时间: 0.009466
编译时间: 0.053116 (template)
编译时间: 0.000209 (template, cached)
执行时间: 0.059017 (相同模板)
执行时间: 0.006129 (相同模板, cached)
执行时间: 0.061882 (不同模板)
执行时间: 0.006613 (不同模板, cached)
执行时间: 0.059104 (不同模板, 不同上下文)
执行时间: 0.005761 (不同模板, 不同上下文, cached)
总时间: 0.261297
LuaJIT 2.0.5 -- 版权所有 (C) 2005-2017 Mike Pall. http://luajit.org/
在每个测试中运行 1000 次迭代
解析时间: 0.005198
编译时间: 0.029687 (template)
编译时间: 0.000082 (template, cached)
执行时间: 0.033824 (相同模板)
执行时间: 0.003130 (相同模板, cached)
执行时间: 0.075899 (不同模板)
执行时间: 0.007027 (不同模板, cached)
执行时间: 0.070269 (不同模板, 不同上下文)
执行时间: 0.007456 (不同模板, 不同上下文, cached)
总时间: 0.232572
LuaJIT 2.1.0-beta3 -- 版权所有 (C) 2005-2017 Mike Pall. http://luajit.org/
在每个测试中运行 1000 次迭代
解析时间: 0.003647
编译时间: 0.027145 (template)
编译时间: 0.000083 (template, cached)
执行时间: 0.034685 (相同模板)
执行时间: 0.002801 (相同模板, cached)
执行时间: 0.073466 (不同模板)
执行时间: 0.010836 (不同模板, cached)
执行时间: 0.068790 (不同模板, 不同上下文)
执行时间: 0.009818 (不同模板, 不同上下文, cached)
总时间: 0.231271
resty (resty 0.23, nginx 版本: openresty/1.15.8.2)
在每个测试中运行 1000 次迭代
解析时间: 0.003980
编译时间: 0.025983 (template)
编译时间: 0.000066 (template, cached)
执行时间: 0.032752 (相同模板)
执行时间: 0.002740 (相同模板, cached)
执行时间: 0.036111 (不同模板)
执行时间: 0.005559 (不同模板, cached)
执行时间: 0.032453 (不同模板, 不同上下文)
执行时间: 0.006057 (不同模板, 不同上下文, cached)
总时间: 0.145701
我还没有将结果与替代方案进行比较。
变更
此模块每个版本的更改记录在 Changes.md 文件中。
另见
- lua-resty-route — 路由库
- lua-resty-reqargs — 请求参数解析器
- lua-resty-session — 会话库
- lua-resty-validation — 验证和过滤库
路线图
我和社区希望添加的一些功能:
- 更好的调试能力和更好的错误消息
- 适当的沙箱
GitHub
您可以在 nginx-module-template 的 GitHub 仓库 中找到此模块的其他配置提示和文档。