lua-resty-template

Lua 和 OpenResty 的模板引擎 (HTML)

$ opm get bungle/lua-resty-template

lua-resty-template

lua-resty-template 是一个用于 Lua 和 OpenResty 的编译 (1) (HTML) 模板引擎。

(1) 编译是指将模板转换为 Lua 函数,您可以调用这些函数,或使用 string.dump 将其作为二进制字节码 blob 保存到磁盘,稍后可以使用 lua-resty-template 或基本的 loadloadfile 标准 Lua 函数来利用这些 blob (另请参见 “模板预编译”)。不过,通常您无需执行此操作,因为 lua-resty-template 会在后台处理。

使用 lua-resty-template 的 Hello World

    local template = require "resty.template"      -- OR
    local template = require "resty.template.safe" -- return nil, err on errors
    
    -- Using template.new
    local view = template.new "view.html"
    view.message = "Hello, World!"
    view:render()
    -- Using 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>

使用内联模板字符串也可以实现相同的功能

    -- Using template string
    template.render([[
    <!DOCTYPE html>
    <html>
    <body>
      <h1>{{message}}</h1>
    </body>
    </html>]], { message = "Hello, World!" })

内容

模板语法

您可以在模板中使用以下标签

  • {{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-} 内部的内容包装到一个名为 blocks 的表中,键为 block (在本例中),请参阅 使用块。不要使用预定义的块名称 verbatimraw

  • {-verbatim-}...{-verbatim-}{-raw-}...{-raw-} 是预定义的块,其内部内容不会由 lua-resty-template 处理,而是按原样输出。

  • {# comments #} {##} 之间的所有内容都被视为注释(即不会输出或执行)

从模板中,您可以访问 context 表中的所有内容,以及 template 表中的所有内容。在模板中,您还可以通过添加前缀来访问 contexttemplate

    <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"]*} won't work, you need to use: #}
    {*context["foo:bar"]*}

或者完全

    template.render([[
    {*context["foo:bar"]*}
    ]], {["foo:bar"] = "foobar"})

关于 HTML 转义的说明

仅转义字符串,函数在没有参数的情况下(递归地)被调用,结果按原样返回,其他类型被 tostring 化。nilngx.null 被转换为空字符串 ""

转义的 HTML 字符

  • & 变成 &amp;

  • < 变成 &lt;

  • > 变成 &gt;

  • " 变成 &quot;

  • ' 变成 &#39;

  • / 变成 &#47;

示例

Lua

    local template = require "resty.template"
    template.render("view.html", {
      title   = "Testing 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,保存 echo 助手函数,如果设置了此键,则需要使用 {{context.echo}}

  • include,保存 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`

安装

只需将 `template.lua``template` 目录放在 package.path 中的某个位置,在 resty 目录下。如果您使用的是 OpenResty,则默认位置为 /usr/local/openresty/lualib/resty

使用 OpenResty 包管理器 (opm)

    $ opm get bungle/lua-resty-template

使用 LuaRocks

    $ luarocks install lua-resty-template

lua-resty-template 的 LuaRocks 存储库位于 https://luarocks.org/modules/bungle/lua-resty-template。

Nginx/OpenResty 配置

lua-resty-template 在 Nginx/OpenResty 的上下文中使用时,您需要了解一些配置指令

  • template_root (set $template_root /var/www/site/templates)

  • template_location (set $template_location /templates)

如果没有在 Nginx 配置中设置这些指令,则使用 ngx.var.document_root (即 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

此方法尝试使用 Lua 代码从 html 目录(相对于 Nginx 前缀)加载文件内容。

    http {
      server {
        location / {
          root html;
          content_by_lua '
            local template = require "resty.template"
            template.render("view.html", { message = "Hello, World!" })
          ';      
        }
      }
    }

使用 template_root

此方法尝试使用 Lua 代码从 /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

此方法尝试使用 ngx.location.capture/templates 位置加载内容(在本例中,这是使用 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 时用作(默认)上下文。创建的表只有一个方法 render,但该表还具有定义了 __tostring 元表的元表。请参见下面的示例。viewlayout 参数可以是字符串或文件路径,但 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 中有报道)。

您还可以传递一个布尔值 truefalse 作为 view 参数,这意味着将返回模板的安全或不安全版本

    local unsafe = require "resty.template"
    local safe   = unsafe.new(true)

还有一个默认的 safe 实现可用

    local safe = require "resty.template.safe"
    -- you can create instance of safe too:
    local safe_instance = safe.new()

safe 版本使用 return nil, err Lua 错误处理模式,而 unsafe 只是抛出错误,您可以使用 pcallxpcallcoroutine.wrap 捕获这些错误。

以下是使用带参数的 new 的示例

    local view = template.new"template.html"              -- or
    local view = template.new("view.html", "layout.html") -- or
    local view = template.new[[<h1>{{message}}</h1>]]     -- or
    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()
    -- You may also replace context on render
    view:render{ title = "Testing lua-resty-template" }
    -- If you want to include view context in  replacement context
    view:render(setmetatable({ title = "Testing lua-resty-template" }, { __index = view }))
    -- To get rendered template as a string, you can use tostring
    local result = tostring(view)

boolean template.caching(boolean or nil)

此函数启用或禁用模板缓存,或者如果未传递参数,则返回模板缓存的当前状态。默认情况下,模板缓存已启用,但您可能希望在开发或内存不足的情况下禁用它。

    local template = require "resty.template"   
    -- Get current state of template caching
    local enabled = template.caching()
    -- Disable template caching
    template.caching(false)
    -- Enable template caching
    template.caching(true)

请注意,如果在编译模板时模板已缓存,则将返回缓存的版本。您可能希望使用 template.cache = {} 清除缓存,以确保您的模板确实被重新编译。

function, boolean template.compile(view, cache_key, plain)

解析、编译和缓存(如果启用了缓存)模板,并将编译后的模板作为函数返回,该函数以上下文作为参数,并返回渲染后的模板作为字符串。您可以选择传递 cache_key 作为缓存键。如果未提供缓存键,则将 view 用作缓存键。如果缓存键为 no-cache,则不会检查模板缓存,并且不会缓存结果函数。您还可以选择传递 plain,其值为 true,如果 view 是纯文本字符串(这将跳过 template.loadtemplate.parse 阶段中的二进制块检测)。如果 plainfalse,则模板被视为文件,所有文件读取问题都被视为错误。如果 plain 设置为 nil (默认值),则模板不会将文件读取错误视为致命错误,并返回 view (通常是模板的路径)。

    local func = template.compile("template.html")          -- or
    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.parse上跳过template.load和二进制块检测)。如果plainfalse,则模板被视为文件,并且所有文件读取问题都被视为错误。如果plain设置为nil(默认值),则模板不会将文件读取错误视为致命错误,并返回view

    local output = template.process("template.html", { message = "Hello, World!" })          -- or
    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.parse上跳过template.load和二进制块检测)。如果plainfalse,则模板被视为文件,并且所有文件读取问题都被视为错误。如果plain设置为nil(默认值),则模板不会将文件读取错误视为致命错误,并返回view

    template.render("template.html", { message = "Hello, World!" })          -- or
    template.render([[<h1>{{message}}</h1>]], { message = "Hello, World!" })

template.render_string(view, context, cache_key)

这只是调用template.render(view, context, cache_key, true)

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.parse上跳过template.load和二进制块检测)。如果plainfalse,则模板被视为文件,并且所有文件读取问题都被视为错误。如果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的loadloadfile)。为了方便起见,您可以选择指定path参数以将二进制块输出到文件。您还可以提供值为falsestrip参数以使预编译的模板也具有调试信息(默认为true)。最后一个参数plain表示编译应该将view视为stringplain = true)还是file pathplain = false)或首先尝试作为文件,然后回退到stringplain = nil)。如果plain=false(文件)并且file io出错,则函数也将出现断言失败错误。

    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()
    
    -- Alternatively you could just write (which does the same thing as above)
    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参数评估为falsenil(默认值))。默认情况下,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.compiletemplate.rendertemplate.parse中使用plain参数,或者用最简单的模板加载器替换template.load(但请注意,如果您的模板使用{(file.html)}包含,则这些也被视为字符串,在这种情况下,file.html将是解析的模板字符串) - 您还可以设置一个加载器,以便在某些数据库系统中查找模板,例如Redis

    local template = require "resty.template"
    template.load = function(view, plain) return view end

如果plain参数为falsenil不被视为false),则所有文件io问题都被视为断言错误。

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("<!-- Output by My Function -->")
    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
    ]]

这将输出

        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没有太多基础设施或扩展方式,但您仍然有一些可以尝试的可能性。

  • 向全局stringtable类型添加方法(虽然不鼓励这样做)

  • 在将值添加到上下文中之前,使用某些内容包装它们(例如代理表)

  • 创建全局函数

  • 将本地函数添加到template表或context表中

  • 在您的表中使用元方法

虽然修改全局类型看起来很方便,但它可能会产生不良的副作用。因此,我建议您首先查看这些库和文章

  • 方法链包装器(https://lua-users.lua.ac.cn/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   = "Testing lua-resty-template"
    layout.view    = template.compile "view.html" { message = "Hello, World!" }
    layout:render()
    -- Or like this
    template.render("layout.html", {
      title = "Testing lua-resty-template",
      view  = template.compile "view.html" { message = "Hello, World!" }
    })
    -- Or maybe you like this style more
    -- (but please remember that context.view is overwritten on rendering the layout.html)
    local view     = template.new("view.html", "layout.html")
    view.title     = "Testing lua-resty-template"
    view.message   = "Hello, World!"
    view:render()
    -- Well, maybe like this then?
    local layout   = template.new "layout.html"
    layout.title   = "Testing 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     = "Testing 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>Testing 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     = "Testing lua-resty-template blocks"
    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>Testing lua-resty-template blocks</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.htmllayout1.htmllayout2.htmlpage.html。您希望继承如下:base.html ➡ layout1.html ➡ page.htmlbase.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>I am different from 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问题 #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-模板混合匹配。假设您有这种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
    
    Testing Markdown.
    ]]*}
    </body>
    </html>
    ]=]

输出

    <html>
    <body>
    <h1>Hello, World</h1>
    
    <p>Testing 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
    
    Testing Markdown with "SmartyPants"...
    ]], { smartypants = true })*}
    </body>
    </html>
    ]=]

输出

    <html>
    <body>
    <h1>Hello, World</h1>
    
    <p>Testing Markdown with &ldquo;SmartyPants&rdquo;&hellip;</p>
    </body>
    </html>

您可能还想为Markdown添加缓存层,或添加辅助函数,而不是将Hoedown库直接作为模板助手函数放在template中。

使用OpenResty的Lua服务器页面(LSP)

Lua Server Pages 或 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); -- you may remove this on production
      ';
      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>

在这里您可以看到,除了我们想要运行的一些 Lua 代码之外,此文件还包含了一小部分视图(`

layouts/default.lsp

    <html>
    {(include/header.lsp)}
    <body>
    {*view*}
    </body>
    </html>

这里我们有一个布局来装饰 `index.lsp`,但我们这里也包含了引入,所以让我们看一下。

include/header.lsp

    <head>
      <title>Testing Lua Server Pages</title>
    </head>

这里只有静态内容。

输出

最终输出将如下所示

    <html>
    <head>
      <title>Testing Lua Server Pages</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 – 京东商城(英文:Jingdong Mall),前身为 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)

  • 简单的 Lua 预处理器 (https://lua-users.lua.ac.cn/wiki/SimpleLuaPreprocessor)

  • 稍微不那么简单的 Lua 预处理器 (https://lua-users.lua.ac.cn/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 生成 (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()
    -- You may also pass iteration count (by default it is 1,000)
    benchmark.run(100)

以下是我桌面电脑(旧款 2010 Mac Pro)的一些结果

    <lua|luajit|resty> -e 'require "resty.template.microbenchmark".run()'

`

    Running 1000 iterations in each test
        Parsing Time: 0.010759
    Compilation Time: 0.054640 (template)
    Compilation Time: 0.000213 (template, cached)
      Execution Time: 0.061851 (same template)
      Execution Time: 0.006722 (same template, cached)
      Execution Time: 0.092698 (different template)
      Execution Time: 0.009537 (different template, cached)
      Execution Time: 0.092452 (different template, different context)
      Execution Time: 0.010106 (different template, different context, cached)
          Total Time: 0.338978
    Running 1000 iterations in each test
        Parsing Time: 0.011633
    Compilation Time: 0.060598 (template)
    Compilation Time: 0.000243 (template, cached)
      Execution Time: 0.068009 (same template)
      Execution Time: 0.007307 (same template, cached)
      Execution Time: 0.071339 (different template)
      Execution Time: 0.007150 (different template, cached)
      Execution Time: 0.066766 (different template, different context)
      Execution Time: 0.006940 (different template, different context, cached)
          Total Time: 0.299985
    Running 1000 iterations in each test
        Parsing Time: 0.012458
    Compilation Time: 0.050013 (template)
    Compilation Time: 0.000249 (template, cached)
      Execution Time: 0.057579 (same template)
      Execution Time: 0.006959 (same template, cached)
      Execution Time: 0.065352 (different template)
      Execution Time: 0.007133 (different template, cached)
      Execution Time: 0.060965 (different template, different context)
      Execution Time: 0.007726 (different template, different context, cached)
          Total Time: 0.268434
    Running 1000 iterations in each test
        Parsing Time: 0.009466
    Compilation Time: 0.053116 (template)
    Compilation Time: 0.000209 (template, cached)
      Execution Time: 0.059017 (same template)
      Execution Time: 0.006129 (same template, cached)
      Execution Time: 0.061882 (different template)
      Execution Time: 0.006613 (different template, cached)
      Execution Time: 0.059104 (different template, different context)
      Execution Time: 0.005761 (different template, different context, cached)
          Total Time: 0.261297
    Running 1000 iterations in each test
        Parsing Time: 0.005198
    Compilation Time: 0.029687 (template)
    Compilation Time: 0.000082 (template, cached)
      Execution Time: 0.033824 (same template)
      Execution Time: 0.003130 (same template, cached)
      Execution Time: 0.075899 (different template)
      Execution Time: 0.007027 (different template, cached)
      Execution Time: 0.070269 (different template, different context)
      Execution Time: 0.007456 (different template, different context, cached)
          Total Time: 0.232572
    Running 1000 iterations in each test
        Parsing Time: 0.003647
    Compilation Time: 0.027145 (template)
    Compilation Time: 0.000083 (template, cached)
      Execution Time: 0.034685 (same template)
      Execution Time: 0.002801 (same template, cached)
      Execution Time: 0.073466 (different template)
      Execution Time: 0.010836 (different template, cached)
      Execution Time: 0.068790 (different template, different context)
      Execution Time: 0.009818 (different template, different context, cached)
          Total Time: 0.231271

resty (resty 0.23,nginx 版本:openresty/1.15.8.2)

    Running 1000 iterations in each test
        Parsing Time: 0.003980
    Compilation Time: 0.025983 (template)
    Compilation Time: 0.000066 (template, cached)
      Execution Time: 0.032752 (same template)
      Execution Time: 0.002740 (same template, cached)
      Execution Time: 0.036111 (different template)
      Execution Time: 0.005559 (different template, cached)
      Execution Time: 0.032453 (different template, different context)
      Execution Time: 0.006057 (different template, different context, cached)
          Total Time: 0.145701

我还没有将结果与替代方案进行比较。

更改

此模块每个版本的更改都记录在 Changes.md 文件中。

另请参阅

路线图

我以及社区希望添加的一些内容

  • 更好的调试功能和更友好的错误消息

  • 合适的沙盒

许可证

`lua-resty-template` 使用三条款 BSD 许可证(因为它最初是从使用该许可证的模块分支而来)。

    Copyright (c) 2014 - 2020, Aapo Talvensaari
    All rights reserved.
    
    Redistribution and use in source and binary forms, with or without modification,
    are permitted provided that the following conditions are met:
    
    * Redistributions of source code must retain the above copyright notice, this
      list of conditions and the following disclaimer.
    
    * Redistributions in binary form must reproduce the above copyright notice, this
      list of conditions and the following disclaimer in the documentation and/or
      other materials provided with the distribution.
    
    * Neither the name of the {organization} nor the names of its
      contributors may be used to endorse or promote products derived from
      this software without specific prior written permission.
    
    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
    ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
    WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
    DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
    ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
    (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
    LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
    ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
    (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
    SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

作者

Aapo Talvensaari (@bungle)

许可证

3bsd

版本