Pular para conteúdo

template: Motor de Templating (HTML) para Lua e nginx-module-lua

Instalação

Se você ainda não configurou a assinatura do repositório RPM, inscreva-se. Depois, você pode prosseguir com os seguintes passos.

CentOS/RHEL 7 ou 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

Para usar esta biblioteca Lua com NGINX, certifique-se de que o nginx-module-lua está instalado.

Este documento descreve lua-resty-template v2.0 lançado em 24 de fevereiro de 2020.


lua-resty-template é um motor de templating (1) (HTML) para Lua e OpenResty.

(1) com compilação, queremos dizer que os templates são traduzidos em funções Lua que você pode chamar ou string.dump como blobs de bytecode binário para o disco que podem ser utilizados posteriormente com lua-resty-template ou funções padrão load e loadfile do Lua (veja também Pré-compilação de Template). Embora, geralmente você não precise fazer isso, pois lua-resty-template lida com isso nos bastidores.

Olá Mundo com lua-resty-template

local template = require "resty.template"      -- OU
local template = require "resty.template.safe" -- retorna nil, err em caso de erros

-- Usando template.new
local view = template.new "view.html"
view.message = "Hello, World!"
view:render()
-- Usando template.render
template.render("view.html", { message = "Hello, World!" })
view.html
<!DOCTYPE html>
<html>
<body>
  <h1>{{message}}</h1>
</body>
</html>
Saída
<!DOCTYPE html>
<html>
<body>
  <h1>Hello, World!</h1>
</body>
</html>

O mesmo pode ser feito com uma string de template inline:

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

Conteúdos

Sintaxe de Template

Você pode usar as seguintes tags nos templates:

  • {{expressão}}, escreve o resultado da expressão - html escapado
  • {*expressão*}, escreve o resultado da expressão
  • {% código lua %}, executa código Lua
  • {(template)}, inclui o arquivo template, você também pode fornecer contexto para o arquivo incluído {(file.html, { message = "Hello, World" })} (NOTA: você não pode usar vírgula (,) em file.html, nesse caso use {["file,with,comma"]})
  • {[expressão]}, inclui o arquivo expressão (o resultado da expressão), você também pode fornecer contexto para o arquivo incluído {["file.html", { message = "Hello, World" }]}
  • {-block-}...{-block-}, envolve dentro de um {-block-} a um valor armazenado em uma tabela blocks com a chave block (neste caso), veja usando blocos. Não use nomes de blocos predefinidos verbatim e raw.
  • {-verbatim-}...{-verbatim-} e {-raw-}...{-raw-} são blocos predefinidos cujo conteúdo não é processado pelo lua-resty-template, mas o conteúdo é exibido como está.
  • {# comentários #} tudo entre {# e #} é considerado como comentado (ou seja, não exibido ou executado)

A partir dos templates, você pode acessar tudo na tabela context e tudo na tabela template. Nos templates, você também pode acessar context e template prefixando as chaves.

<h1>{{message}}</h1> == <h1>{{context.message}}</h1>
Sintaxe de Escape Curta

Se você não quiser que uma tag de template específica seja processada, você pode escapar a tag inicial com uma barra invertida \:

<h1>\{{message}}</h1>

Isso irá exibir (em vez de avaliar a mensagem):

<h1>{{message}}</h1>

Se você quiser adicionar um caractere de barra invertida logo antes da tag de template, você precisa escapar isso também:

<h1>\\{{message}}</h1>

Isso irá exibir:

<h1>\[message-variables-content-here]</h1>
Uma Palavra Sobre Chaves Complexas na Tabela de Contexto

Digamos que você tenha esse tipo de tabela de contexto:

local ctx = {["foo:bar"] = "foobar"}

E você quer renderizar o valor de ctx["foo:bar"] que é foobar no seu template. Você deve especificá-lo explicitamente referenciando o context no seu template:

{# {*["foo:bar"]*} não funcionará, você precisa usar: #}
{*context["foo:bar"]*}

Ou de uma vez:

template.render([[
{*context["foo:bar"]*}
]], {["foo:bar"] = "foobar"})
Uma Palavra Sobre Escape HTML

Apenas strings são escapadas, funções são chamadas sem argumentos (recursivamente) e os resultados são retornados como estão, outros tipos são convertidos para tostring. nils e ngx.nulls são convertidos em strings vazias "".

Caracteres HTML escapados:

  • & se torna &amp;
  • < se torna &lt;
  • > se torna &gt;
  • " se torna &quot;
  • ' se torna &#39;
  • / se torna &#47;

Exemplo

Lua
local template = require "resty.template"
template.render("view.html", {
  title   = "Testando 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>

Chaves de Contexto Reservadas e Observações

É aconselhável que você não use essas chaves em suas tabelas de contexto:

  • ___, contém o template compilado, se definido você precisa usar {{context.___}}
  • context, contém o contexto atual, se definido você precisa usar {{context.context}}
  • echo, contém a função auxiliar echo, se definida você precisa usar {{context.echo}}
  • include, contém a função auxiliar include, se definida você precisa usar {{context.include}}
  • layout, contém o layout pelo qual a view será decorada, se definido você precisa usar {{context.layout}}
  • blocks, contém os blocos, se definido você precisa usar {{context.blocks}} (veja: usando blocos)
  • template, contém a tabela de template, se definida você precisa usar {{context.template}}

Além disso, com template.new você não deve sobrescrever:

  • render, a função que renderiza uma view, obviamente ;-)

Você também não deve {(view.html)} recursivamente:

Lua
template.render "view.html"
view.html
{(view.html)}

Você pode carregar templates de "subdiretórios" também com {(sintaxe)}:

view.html
{(users/list.html)}

Observe também que você pode fornecer o template tanto como um caminho de arquivo quanto como uma string. Se o arquivo existir, ele será usado; caso contrário, a string será utilizada. Veja também template.load.

Configuração do Nginx / OpenResty

Quando lua-resty-template é usado no contexto do Nginx / OpenResty, há algumas diretivas de configuração que você precisa estar ciente:

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

Se nenhuma dessas estiver definida na configuração do Nginx, o valor de ngx.var.document_root (também conhecido como diretiva root) é usado. Se template_location estiver definido, ele será usado primeiro, e se a localização retornar qualquer coisa diferente de 200 como código de status, nós voltamos para template_root (se definido) ou document_root.

Com lua-resty-template 2.0, é possível sobrescrever $template_root e $template_location com código Lua:

local template = require "resty.template".new({
  root     = "/templates",
  location = "/templates" 
})
Usando document_root

Este tenta carregar o conteúdo do arquivo com código Lua do diretório html (relativo ao prefixo do Nginx).

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

Este tenta carregar o conteúdo do arquivo com código Lua do diretório /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!" })
      ';      
    }
  }
}
Usando template_location

Este tenta carregar o conteúdo com ngx.location.capture da localização /templates (neste caso, isso é servido com o módulo 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/;
    }    
  }
}

Veja também template.load.

API Lua

template.root

Você pode configurar a raiz do template definindo esta variável que será procurada por arquivos de template:

local template = require "resty.template".new({
  root = "/templates"
})
template.render_file("test.html")

Esta propriedade sobrescreve a definida na configuração do Nginx (set $template_root /my-templates;)

template.location

Isso é o que você pode usar com OpenResty, pois usará ngx.location.capture para buscar arquivos de templates de forma não bloqueante.

local template = require "resty.template".new({
  location = "/templates"
})
template.render_file("test.html")

Esta propriedade sobrescreve a definida na configuração do Nginx (set $template_location /my-templates;)

table template.new(view, layout)

Cria uma nova instância de template que é usada como um contexto (padrão) quando renderizado. Uma tabela que é criada tem apenas um método render, mas a tabela também tem uma metatabela com __tostring definida. Veja o exemplo abaixo. Tanto os argumentos view quanto layout podem ser strings ou caminhos de arquivo, mas o layout também pode ser uma tabela criada anteriormente com template.new.

Com a versão 2.0, o novo pode também ser usado sem argumentos, o que cria uma nova instância de template:

local template = require "resty.template".new()

Você também pode passar uma tabela que será modificada para ser um template:

local config = {
  root = "/templates"
}

local template = require "resty.template".new(config)

Isso é útil, pois o template criado por new não compartilha o cache com o template global retornado por require "resty.template" (isso foi relatado com a issue #25).

Você também pode passar um booleano true ou false como um parâmetro view, o que significa que uma versão safe ou un-safe do template é retornada:

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

Há também uma implementação safe padrão disponível:

local safe = require "resty.template.safe"
-- você pode criar uma instância de safe também:
local safe_instance = safe.new()

A versão safe usa o padrão de tratamento de erros do Lua return nil, err e a unsafe apenas lança os erros, que você pode capturar com pcall, xpcall ou coroutine.wrap.

Aqui estão exemplos de uso de new com argumentos:

local view = template.new"template.html"              -- ou
local view = template.new("view.html", "layout.html") -- ou
local view = template.new[[<h1>{{message}}</h1>]]     -- ou
local view = template.new([[<h1>{{message}}</h1>]], [[
<html>
<body>
  {*view*}
</body>
</html>
]])
Exemplo
local template = require "resty.template"
local view = template.new"view.html"
view.message  = "Hello, World!"
view:render()
-- Você também pode substituir o contexto na renderização
view:render{ title = "Testando lua-resty-template" }
-- Se você quiser incluir o contexto da view no contexto de substituição
view:render(setmetatable({ title = "Testando lua-resty-template" }, { __index = view }))
-- Para obter o template renderizado como uma string, você pode usar tostring
local result = tostring(view)

boolean template.caching(boolean ou nil)

Esta função habilita ou desabilita o cache de templates, ou se nenhum parâmetro for passado, retorna o estado atual do cache de templates. Por padrão, o cache de templates está habilitado, mas você pode querer desabilitá-lo em situações de desenvolvimento ou de baixa memória.

local template = require "resty.template"   
-- Obter o estado atual do cache de templates
local enabled = template.caching()
-- Desabilitar o cache de templates
template.caching(false)
-- Habilitar o cache de templates
template.caching(true)

Por favor, note que se o template já estava em cache ao compilar um template, a versão em cache será retornada. Você pode querer limpar o cache com template.cache = {} para garantir que seu template realmente seja recompilado.

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

Analisa, compila e armazena em cache (se o cache estiver habilitado) um template e retorna o template compilado como uma função que recebe o contexto como parâmetro e retorna o template renderizado como uma string. Opcionalmente, você pode passar cache_key que é usado como uma chave de cache. Se a chave de cache não for fornecida, view será usada como chave de cache. Se a chave de cache for no-cache, o cache do template não será verificado e a função resultante não será armazenada em cache. Você também pode opcionalmente passar plain com um valor de true se o view for uma string de texto simples (isso irá pular template.load e a detecção de chunk binário na fase template.parse). Se plain for false, o template é considerado um arquivo, e todos os problemas com leitura de arquivos são considerados como erros. Se plain for definido como nil (o padrão), o template não considera erros de leitura de arquivos como fatais, e retorna de volta o view (geralmente o caminho do template).

local func = template.compile("template.html")          -- ou
local func = template.compile([[<h1>{{message}}</h1>]])
Exemplo
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)

Observe também o segundo valor de retorno que é um booleano. Você pode descartá-lo ou usá-lo para determinar se a função retornada foi armazenada em cache.

function, boolean template.compile_string(view, cache_key)

Isso apenas chama template.compile(view, cache_key, true)

function, boolean template.compile_file(view, cache_key)

Isso apenas chama template.compile(view, cache_key, false)

template.visit(func)

Permite que você registre funções visitadoras do parser de templates. Os visitantes são chamados na ordem em que são registrados. E uma vez registrados, não podem ser removidos do parser. Talvez seja mais fácil mostrar como funciona:

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([[
Como você está, {{user.name}}?

Aqui está uma nova receita de culinária para você!

{% for i, ingredient in ipairs(ingredients) do %}
  {*i*}. {{ingredient}}
{% end %}
{-ad-}`lua-resty-template` o motor de templating para OpenResty!{-ad-}
]])

local content = func{
  user = {
    name = "bungle"
  },
  ingredients = {
    "batatas",
    "salsichas"
  }
}

print(content)

Isso irá gerar a seguinte saída:

  visit: 1
content: Como você está,

  visit: 2
   type: {
content: user.name

  visit: 3
content: ?

Aqui está uma nova receita de culinária para você!

  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` o motor de templating para OpenResty!

  visit: 10
content: `lua-resty-template` o motor de templating para OpenResty!

Como você está, bungle?

Aqui está uma nova receita de culinária para você!

  1. batatas
  2. salsichas

As funções visitadoras devem ter esta assinatura:

string function(content, type, name)

Se a função não modificar o content, ela deve retornar o content de volta, como o visitante acima faz.

Aqui está um exemplo de visitante um pouco mais avançado que lida com erros em tempo de execução nas expressões:

local template = require "resty.template".new()

template.render "Cálculo: {{i*10}}"

Isso irá gerar um erro em tempo de execução com:

ERROR: [string "context=... or {}..."]:7: tentativa de realizar aritmética na global 'i' (um valor nil)
stack traceback:
    resty/template.lua:652: na função 'render'
    a.lua:52: na função 'file_gen'
    init_worker_by_lua:45: na função <init_worker_by_lua:43>
    [C]: na função 'xpcall'
    init_worker_by_lua:52: na função 'init_worker_by_lua:50'

Agora vamos adicionar um visitante que lida com esse erro:

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 "Cálculo: {{i*10}}\n"
template.render("Cálculo: {{i*10}}\n", { i = 1 })

Isso irá gerar a saída:

Cálculo: 
Cálculo: 10

string template.process(view, context, cache_key, plain)

Analisa, compila, armazena em cache (se o cache estiver habilitado) e retorna a saída como string. Você também pode passar opcionalmente cache_key que é usado como uma chave de cache. Se plain avaliar como true, o view é considerado um template de string simples (template.load e a detecção de chunk binário são pulados na template.parse). Se plain for false, o template é considerado um arquivo, e todos os problemas com leitura de arquivos são considerados como erros. Se plain for definido como nil (o padrão), o template não considera erros de leitura de arquivos como fatais, e retorna de volta o view.

local output = template.process("template.html", { message = "Hello, World!" })          -- ou
local output = template.process([[<h1>{{message}}</h1>]], { message = "Hello, World!" })

string template.process_string(view, context, cache_key)

Isso apenas chama template.process(view, context, cache_key, true)

string template.process_file(view, context, cache_key)

Isso apenas chama template.process(view, context, cache_key, false)

template.render(view, context, cache_key, plain)

Analisa, compila, armazena em cache (se o cache estiver habilitado) e exibe o template, seja com ngx.print se disponível, ou print. Você pode opcionalmente passar cache_key que é usado como uma chave de cache. Se plain avaliar como true, o view é considerado um template de string simples (template.load e a detecção de chunk binário são pulados na template.parse). Se plain for false, o template é considerado um arquivo, e todos os problemas com leitura de arquivos são considerados como erros. Se plain for definido como nil (o padrão), o template não considera erros de leitura de arquivos como fatais, e retorna de volta o view.

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

string template.render_string(view, context, cache_key)

Isso apenas chama template.render(view, context, cache_key, true)

string template.render_file(view, context, cache_key)

Isso apenas chama template.render(view, context, cache_key, false)

string template.parse(view, plain)

Analisa o arquivo ou string do template e gera uma string de template analisada. Isso pode ser útil ao depurar templates. Você deve notar que se você estiver tentando analisar um chunk binário (por exemplo, um retornado com template.compile), template.parse retornará esse chunk binário como está. Se plain avaliar como true, o view é considerado um template de string simples (template.load e a detecção de chunk binário são pulados na template.parse). Se plain for false, o template é considerado um arquivo, e todos os problemas com leitura de arquivos são considerados como erros. Se plain for definido como nil (o padrão), o template não considera erros de leitura de arquivos como fatais, e retorna de volta o view.

local t1 = template.parse("template.html")
local t2 = template.parse([[<h1>{{message}}</h1>]])

string template.parse_string(view, plain)

Isso apenas chama template.parse(view, plain, true)

string template.parse_file(view, plain)

Isso apenas chama template.parse(view, plain, false)

string template.precompile(view, path, strip, plain)

Pré-compila o template como um chunk binário. Este chunk binário pode ser escrito como um arquivo (e você pode usá-lo diretamente com load e loadfile do Lua). Para conveniência, você pode opcionalmente especificar o argumento path para output do chunk binário para um arquivo. Você também pode fornecer o parâmetro strip com o valor false para fazer com que os templates pré-compilados tenham informações de depuração também (o padrão é true). O último parâmetro plain significa que a compilação deve tratar o view como string (plain = true) ou como caminho de arquivo (plain = false) ou tentar primeiro como um arquivo e voltar para string (plain = nil). No caso de plain=false (um arquivo) e houver erro com file io, a função também irá gerar um erro com uma falha de asserçã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()

-- Alternativamente, você poderia apenas escrever (o que faz a mesma coisa que acima)
template.precompile(view, "precompiled-bin.html")

template.render("precompiled-bin.html", {
    title = "Nomes",
    "Emma", "James", "Nicholas", "Mary"
})

string template.precompile_string(view, path, strip)

Isso apenas chama template.precompile(view, path, strip, true).

string template.precompile_file(view, path, strip)

Isso apenas chama template.precompile(view, path, strip, false).

string template.load(view, plain)

Este campo é usado para carregar templates. template.parse chama esta função antes de começar a analisar o template (assumindo que o argumento opcional plain na template.parse avalia como false ou nil (o padrão). Por padrão, há dois carregadores no lua-resty-template: um para Lua e o outro para Nginx / OpenResty. Os usuários podem sobrescrever este campo com sua própria função. Por exemplo, você pode querer escrever uma função carregadora de template que carrega templates de um banco de dados.

O carregador padrão template.load para Lua (anexado como template.load quando usado diretamente com Lua):

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

O carregador padrão template.load para Nginx / OpenResty (anexado como template.load quando usado no contexto do Nginx / OpenResty):

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

Como você pode ver, lua-resty-template sempre tenta (por padrão) carregar um template de um arquivo (ou com ngx.location.capture) mesmo que você tenha fornecido o template como uma string. lua-resty-template. Mas se você sabe que seus templates são sempre strings, e não caminhos de arquivo, você pode usar o argumento plain na template.compile, template.render, e template.parse OU substituir template.load com o carregador de template mais simples que existe (mas esteja ciente de que se seus templates usarem inclusões {(file.html)}, essas também são consideradas como strings, nesse caso file.html será a string do template que é analisada) - você também poderia configurar um carregador que encontra templates em algum sistema de banco de dados, por exemplo, Redis:

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

Se o parâmetro plain for false (nil não é tratado como false), todos os problemas com file io são considerados erros de asserção.

string template.load_string(view)

Isso apenas chama template.load(view, true)

string template.load_file(view)

Isso apenas chama template.load(view, false)

template.print

Este campo contém uma função que é usada em template.render() ou template.new("example.html"):render() para exibir os resultados. Por padrão, isso contém ngx.print (se disponível) ou print. Você pode querer (e está autorizado a) sobrescrever este campo, se quiser usar sua própria função de saída. Isso também é útil se você estiver usando algum outro framework, por exemplo, Turbo.lua (http://turbolua.org/).

local template = require "resty.template"

template.print = function(s)
  print(s)
  print("<!-- Saída pela Minha Função -->")
end

Pré-compilação de Template

lua-resty-template suporta pré-compilação de templates. Isso pode ser útil quando você quer pular a análise de templates (e a interpretação Lua) em produção ou se você não quer que seus templates sejam distribuídos como arquivos de texto simples em servidores de produção. Além disso, ao pré-compilar, você pode garantir que seus templates não contenham algo que não pode ser compilado (eles são sintaticamente válidos em Lua). Embora os templates sejam armazenados em cache (mesmo sem pré-compilação), há alguns ganhos de desempenho (e memória). Você poderia integrar a pré-compilação de templates em seus scripts de construção (ou implantação) (talvez como tarefas Gulp, Grunt ou Ant).

Pré-compilando template e output como um arquivo binário
local template = require "resty.template"
local compiled = template.precompile("example.html", "example-bin.html")
Carregar arquivo de template pré-compilado e executá-lo com parâmetros de contexto
local template = require "resty.template"
template.render("example-bin.html", { "Jack", "Mary" })

Ajuda de Template

Ajuda Integrada

echo(...)

Ecoa a saída. Isso é útil com {% .. %}:

require "resty.template".render[[
começo
{%
for i=1, 10 do
  echo("\tlinha: ", i, "\n")
end
%}
fim
]]

Isso irá gerar:

começo
    linha: 1
    linha: 2
    linha: 3
    linha: 4
    linha: 5
    linha: 6
    linha: 7
    linha: 8
    linha: 9
    linha: 10
fim

Isso também pode ser escrito assim, mas echo pode ser útil em alguns casos:

require "resty.template".render[[
começo
{% for i=1, 10 do %}
  linha: {* i *}
{% end %}
fim
]]

include(view, context)

Isso é usado principalmente internamente com {(view.html)}, {["view.html"]} e com blocos {-block-name-}..{-block-name-}. Se context não for fornecido, o contexto usado para compilar a view pai é utilizado. Esta função irá compilar o view e chamar a função resultante com context (ou o context da view pai se não fornecido).

Outras Maneiras de Estender

Embora lua-resty-template não tenha muita infraestrutura ou maneiras de estendê-lo, você ainda tem algumas possibilidades que pode tentar.

  • Adicionar métodos aos tipos globais string e table (não encorajado, porém)
  • Envolver seus valores com algo antes de adicioná-los ao contexto (por exemplo, tabela proxy)
  • Criar funções globais
  • Adicionar funções locais à tabela template ou à tabela context
  • Usar metamétodos em suas tabelas

Enquanto modificar tipos globais parece conveniente, isso pode ter efeitos colaterais indesejados. É por isso que eu sugiro que você olhe para essas bibliotecas e artigos primeiro:

Você poderia, por exemplo, adicionar o _ do Moses ou do Underscore à tabela de template ou à tabela de contexto.

Exemplo
local _ = require "moses"
local template = require "resty.template"
template._ = _

Então você pode usar _ dentro de seus templates. Eu criei um exemplo de ajuda de template que pode ser encontrado aqui: 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 }
})
Saída
<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>

Exemplos de Uso

Inclusão de Template

Você pode incluir templates dentro de templates com a sintaxe {(template)} e {(template, context)}. O primeiro usa o contexto atual como um contexto para o template incluído, e o segundo substitui-o por um novo contexto. Aqui está um exemplo de uso de inclusões e passando um contexto diferente para o arquivo incluído:

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>Usuário {{name}} tem {{age}} anos</li>
Saída
<html>
<body>
<ul>
    <li>Usuário Jane tem 29 anos</li>
    <li>Usuário John tem 25 anos</li>
</ul>
</body>
</html>

Views com Layouts

Layouts (ou Páginas Mestres) podem ser usados para envolver uma view dentro de outra view (também conhecida como layout).

Lua
local template = require "resty.template"
local layout   = template.new "layout.html"
layout.title   = "Testando lua-resty-template"
layout.view    = template.compile "view.html" { message = "Hello, World!" }
layout:render()
-- Ou assim
template.render("layout.html", {
  title = "Testando lua-resty-template",
  view  = template.compile "view.html" { message = "Hello, World!" }
})
-- Ou talvez você goste mais deste estilo
-- (mas lembre-se que o contexto.view é sobrescrito ao renderizar o layout.html)
local view     = template.new("view.html", "layout.html")
view.title     = "Testando lua-resty-template"
view.message   = "Hello, World!"
view:render()
-- Bem, talvez assim então?
local layout   = template.new "layout.html"
layout.title   = "Testando 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>
Alternativamente, você pode definir o layout em uma view também:
Lua
local view     = template.new("view.html", "layout.html")
view.title     = "Testando 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>
Saída
<!DOCTYPE html>
<html>
<head>
    <title>Testando lua-resty-template</title>
</head>
<body>
<div id="section">
    <h1>Hello, World!</h1>
</div>
</body>
</html>

Usando Blocos

Blocos podem ser usados para mover diferentes partes das views para lugares específicos nos layouts. Os layouts têm espaços reservados para blocos.

Lua
local view     = template.new("view.html", "layout.html")
view.title     = "Testando blocos de lua-resty-template"
view.message   = "Hello, World!"
view.keywords  = { "teste", "lua", "template", "blocos" }
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>
Saída
<!DOCTYPE html>
<html>
<head>
<title>Testando blocos de lua-resty-template</title>
</head>
<body>
<article>
    <h1>Hello, World!</h1>
</article>
<aside>
    <ul>
        <li>teste</li>
        <li>lua</li>
        <li>template</li>
        <li>blocos</li>
    </ul>
</aside>
</body>
</html>

Herança Avô-Pai-Filho

Digamos que você tenha base.html, layout1.html, layout2.html e page.html. Você quer uma herança como esta: base.html ➡ layout1.html ➡ page.html ou base.html ➡ layout2.html ➡ page.html (na verdade, esse aninhamento não é limitado a três níveis).

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>Eu sou diferente do layout1 </div>
{-main-}
page.html
{% layout = "layout1.html" %}
{-sidebar-}
  este é o sidebar
{-sidebar-}

{-content-}
  este é o conteúdo
{-content-}

{-page_css-}
  <link href="css/page.css" rel="stylesheet">
{-page_css-}

{-page_js-}
  <script src="js/page.js"></script>
{-page_js-}

Ou:

page.html
{% layout = "layout2.html" %}
{-sidebar-}
  este é o sidebar
{-sidebar-}

{-content-}
  este é o conteúdo
{-content-}

{-page_css-}
  <link href="css/page.css" rel="stylesheet">
{-page_css-}

{-page_js-}
  <script src="js/page.js"></script>
{-page_js-}

Macros

@DDarko mencionou em uma issue #5 que ele tem um caso de uso onde precisa ter macros ou views parametrizadas. Isso é um recurso legal que você pode usar com lua-resty-template.

Para usar macros, vamos primeiro definir algum código Lua:

template.render("macro.html", {
    item = "original",
    items = { a = "original-a", b = "original-b" } 
})

E o macro-example.html:

{% local string_macro = [[
<div>{{item}}</div>
]] %}
{* template.compile(string_macro)(context) *}
{* template.compile(string_macro){ item = "string-macro-context" } *}

Isso irá gerar:

<div>original</div>
<div>string-macro-context</div>

Agora vamos adicionar uma macro de função, em macro-example.html (você pode omitir local se quiser):

{% 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) *}

Isso irá gerar:

<div>original</div>
<span>original-a</span>

Mas isso é ainda mais flexível, vamos tentar outra macro de função:

{% local function function_macro2(var)
    return template.compile("<div>{{" .. var .. "}}</div>\n")
end %}
{* function_macro2 "item" (context) *}
{* function_macro2 "b" (items) *}

Isso irá gerar:

<div>original</div>
<div>original-b</div>

E aqui está mais uma:

{% 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" }) *}

Isso irá gerar:

<div>original</div>
<div>original-a</div>
<div>original-b</div>
<div>b-from-new-context</div>

Macros são realmente flexíveis. Você pode ter renderizadores de formulário e outras macros auxiliares para ter uma saída de template reutilizável e parametrizada. Uma coisa que você deve saber é que dentro de blocos de código (entre {% e %}) você não pode ter %}, mas você pode contornar isso usando concatenação de strings "%" .. "}".

Chamando Métodos em Templates

Você pode chamar métodos de string (ou outras funções de tabela) em templates também.

Lua
local template = require "resty.template"
template.render([[
<h1>{{header:upper()}}</h1>
]], { header = "hello, world!" })
Saída
<h1>HELLO, WORLD!</h1>

Incorporando Angular ou outras tags / templating dentro dos Templates

Às vezes você precisa misturar e combinar outros templates (digamos, templates de JavaScript do lado do cliente como Angular) com templates do lado do servidor lua-resty-templates. Digamos que você tenha este tipo de template 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>

Agora você pode ver que há {{buttonText}} que é realmente para o templating do Angular, e não para o lua-resty-template. Você pode corrigir isso envolvendo todo o código com {-verbatim-} ou {-raw-} ou apenas as partes que você deseja:

{-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-}

ou (veja que o {(head.html)} é processado pelo 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>

Você também pode usar a sintaxe de escape curta:

...
<button ng-click="changeFoo()">\{{buttonText}}</button>
...

Incorporando Markdown dentro dos Templates

Se você quiser incorporar a sintaxe Markdown (e SmartyPants) dentro de seus templates, você pode fazer isso usando, por exemplo, lua-resty-hoedown (depende do LuaJIT). Aqui está um exemplo de uso disso:

Lua
local template = require "resty.template"
template.markdown = require "resty.hoedown"

template.render[=[
<html>
<body>
{*markdown[[
#Olá, Mundo

Testando Markdown.
]]*}
</body>
</html>
]=]
Saída
<html>
<body>
<h1>Olá, Mundo</h1>

<p>Testando Markdown.</p>
</body>
</html>

Você também pode adicionar parâmetros de configuração que estão documentados no projeto lua-resty-hoedown. Digamos que você também queira usar SmartyPants:

Lua
local template = require "resty.template"
template.markdown = require "resty.hoedown"

template.render[=[
<html>
<body>
{*markdown([[
#Olá, Mundo

Testando Markdown com "SmartyPants"...
]], { smartypants = true })*}
</body>
</html>
]=]
Saída
<html>
<body>
<h1>Olá, Mundo</h1>

<p>Testando Markdown com &ldquo;SmartyPants&rdquo;&hellip;</p>
</body>
</html>

Você também pode querer adicionar uma camada de cache para seus Markdowns, ou funções auxiliares em vez de colocar a biblioteca Hoedown diretamente como uma função auxiliar de template no template.

Páginas de Servidor Lua (LSP) com OpenResty

Páginas de Servidor Lua ou LSPs são semelhantes às tradicionais páginas de servidor PHP ou Microsoft Active Server Pages (ASP), onde você pode simplesmente colocar arquivos de código-fonte na raiz do seu documento (do seu servidor web) e tê-los processados pelos compiladores das respectivas linguagens (PHP, VBScript, JScript, etc.). Você pode emular bastante de perto isso, às vezes chamado de estilo de desenvolvimento spaghetti, facilmente com lua-resty-template. Aqueles que têm feito desenvolvimento com ASP.NET Web Forms conhecem o conceito de arquivos Code Behind. Há algo semelhante, mas desta vez chamamos de Layout na Frente aqui (você pode incluir módulos Lua com chamadas normais de require se desejar em LSPs). Para ajudar você a entender os conceitos, vamos ter um pequeno exemplo:

nginx.conf:
http {
  init_by_lua '
    require "resty.core"
    template = require "resty.template"
    template.caching(false); -- você pode remover isso em produção
  ';
  server {
    location ~ \.lsp$ {
      default_type text/html;
      content_by_lua 'template.render(ngx.var.uri)';
    }
  }
}

A configuração acima cria uma variável global template no ambiente Lua (você pode não querer isso). Também criamos uma localização para corresponder a todos os arquivos (ou locais) .lsp, e então apenas renderizamos o template.

Vamos imaginar que a solicitação é para index.lsp.

index.lsp
{%
layout = "layouts/default.lsp"
local title = "Olá, Mundo!"
%}
<h1>{{title}}</h1>

Aqui você pode ver que este arquivo inclui um pouco de uma view (<h1>{{title}}</h1>) além de algum código Lua que queremos executar. Se você quiser ter um arquivo de código puro com Layout na Frente, então simplesmente não escreva nenhum código de view neste arquivo. A variável layout já está definida nas views conforme documentado em outro lugar nesta documentação. Agora vamos ver os outros arquivos também.

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

Aqui temos um layout para decorar o index.lsp, mas também temos uma inclusão aqui, então vamos olhar para isso.

include/header.lsp
<head>
  <title>Testando Páginas de Servidor Lua</title>
</head>

Apenas coisas estáticas aqui.

Saída

A saída final ficará assim:

<html>
<head>
  <title>Testando Páginas de Servidor Lua</title>
</head>
<body>
  <h1>Olá, Mundo!</h1>
</body>
</html>

Como você pode ver, lua-resty-template pode ser bastante flexível e fácil de começar. Basta colocar arquivos sob sua raiz de documento e usar o estilo normal de salvar e atualizar durante o desenvolvimento. O servidor irá automaticamente pegar os novos arquivos e recarregar os templates (se o cache estiver desligado) ao salvar.

Se você quiser passar variáveis para layouts ou inclusões, pode adicionar coisas à tabela de contexto (no exemplo abaixo veja context.title):

{%
layout = "layouts/default.lsp"
local title = "Olá, Mundo!"
context.title = 'Minha Aplicação - ' .. title
%}
<h1>{{title}}</h1>

FAQ

Como Eu Limpo o Cache do Template

lua-resty-template automaticamente armazena em cache (se o cache estiver habilitado) as funções de template resultantes na tabela template.cache. Você pode limpar o cache emitindo template.cache = {}.

Onde o lua-resty-template é Usado

  • jd.com – Jingdong Mall (Chinês: 京东商城; pinyin: Jīngdōng Shāngchéng), anteriormente 360Buy, é uma empresa chinesa de comércio eletrônico.

Por favor, me avise se houver erros ou informações desatualizadas nesta lista.

Alternativas

Você também pode olhar para estas (como alternativas, ou para misturá-las com lua-resty-template):

lua-resty-template foi originalmente forkado do tirtemplate.lua de Tor Hveem que ele havia extraído do framework web Tir de Zed Shaw (http://tir.mongrel2.org/). Obrigado Tor e Zed por suas contribuições anteriores.

Benchmarks

Há um pequeno microbenchmark localizado aqui: https://github.com/bungle/lua-resty-template/blob/master/lib/resty/template/microbenchmark.lua

Há também uma regressão no LuaJIT que afeta os resultados. Se você quiser que seu LuaJIT seja corrigido contra isso, você precisa mesclar esta solicitação de pull: https://github.com/LuaJIT/LuaJIT/pull/174.

Outros relataram que em benchmarks simples, executar este motor de template realmente supera o Nginx servindo arquivos estáticos por um fator de três. Então, eu acho que este motor é bastante rápido.

Lua
local benchmark = require "resty.template.microbenchmark"
benchmark.run()
-- Você também pode passar a contagem de iterações (por padrão é 1.000)
benchmark.run(100)

Aqui estão alguns resultados do meu desktop (antigo Mac Pro de 2010):

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

Executando 1000 iterações em cada teste
    Tempo de Análise: 0.010759
Tempo de Compilação: 0.054640 (template)
Tempo de Compilação: 0.000213 (template, em cache)
  Tempo de Execução: 0.061851 (mesmo template)
  Tempo de Execução: 0.006722 (mesmo template, em cache)
  Tempo de Execução: 0.092698 (template diferente)
  Tempo de Execução: 0.009537 (template diferente, em cache)
  Tempo de Execução: 0.092452 (template diferente, contexto diferente)
  Tempo de Execução: 0.010106 (template diferente, contexto diferente, em cache)
      Tempo Total: 0.338978
Executando 1000 iterações em cada teste
    Tempo de Análise: 0.011633
Tempo de Compilação: 0.060598 (template)
Tempo de Compilação: 0.000243 (template, em cache)
  Tempo de Execução: 0.068009 (mesmo template)
  Tempo de Execução: 0.007307 (mesmo template, em cache)
  Tempo de Execução: 0.071339 (template diferente)
  Tempo de Execução: 0.007150 (template diferente, em cache)
  Tempo de Execução: 0.066766 (template diferente, contexto diferente)
  Tempo de Execução: 0.006940 (template diferente, contexto diferente, em cache)
      Tempo Total: 0.299985
Executando 1000 iterações em cada teste
    Tempo de Análise: 0.012458
Tempo de Compilação: 0.050013 (template)
Tempo de Compilação: 0.000249 (template, em cache)
  Tempo de Execução: 0.057579 (mesmo template)
  Tempo de Execução: 0.006959 (mesmo template, em cache)
  Tempo de Execução: 0.065352 (template diferente)
  Tempo de Execução: 0.007133 (template diferente, em cache)
  Tempo de Execução: 0.060965 (template diferente, contexto diferente)
  Tempo de Execução: 0.007726 (template diferente, contexto diferente, em cache)
      Tempo Total: 0.268434
Executando 1000 iterações em cada teste
    Tempo de Análise: 0.009466
Tempo de Compilação: 0.053116 (template)
Tempo de Compilação: 0.000209 (template, em cache)
  Tempo de Execução: 0.059017 (mesmo template)
  Tempo de Execução: 0.006129 (mesmo template, em cache)
  Tempo de Execução: 0.061882 (template diferente)
  Tempo de Execução: 0.006613 (template diferente, em cache)
  Tempo de Execução: 0.059104 (template diferente, contexto diferente)
  Tempo de Execução: 0.005761 (template diferente, contexto diferente, em cache)
      Tempo Total: 0.261297
Executando 1000 iterações em cada teste
    Tempo de Análise: 0.005198
Tempo de Compilação: 0.029687 (template)
Tempo de Compilação: 0.000082 (template, em cache)
  Tempo de Execução: 0.033824 (mesmo template)
  Tempo de Execução: 0.003130 (mesmo template, em cache)
  Tempo de Execução: 0.075899 (template diferente)
  Tempo de Execução: 0.007027 (template diferente, em cache)
  Tempo de Execução: 0.070269 (template diferente, contexto diferente)
  Tempo de Execução: 0.007456 (template diferente, contexto diferente, em cache)
      Tempo Total: 0.232572
Executando 1000 iterações em cada teste
    Tempo de Análise: 0.003647
Tempo de Compilação: 0.027145 (template)
Tempo de Compilação: 0.000083 (template, em cache)
  Tempo de Execução: 0.034685 (mesmo template)
  Tempo de Execução: 0.002801 (mesmo template, em cache)
  Tempo de Execução: 0.073466 (template diferente)
  Tempo de Execução: 0.010836 (template diferente, em cache)
  Tempo de Execução: 0.068790 (template diferente, contexto diferente)
  Tempo de Execução: 0.009818 (template diferente, contexto diferente, em cache)
      Tempo Total: 0.231271
resty (resty 0.23, versão do nginx: openresty/1.15.8.2)
Executando 1000 iterações em cada teste
    Tempo de Análise: 0.003980
Tempo de Compilação: 0.025983 (template)
Tempo de Compilação: 0.000066 (template, em cache)
  Tempo de Execução: 0.032752 (mesmo template)
  Tempo de Execução: 0.002740 (mesmo template, em cache)
  Tempo de Execução: 0.036111 (template diferente)
  Tempo de Execução: 0.005559 (template diferente, em cache)
  Tempo de Execução: 0.032453 (template diferente, contexto diferente)
  Tempo de Execução: 0.006057 (template diferente, contexto diferente, em cache)
      Tempo Total: 0.145701

Ainda não comparei os resultados com as alternativas.

Mudanças

As mudanças de cada lançamento deste módulo estão registradas no arquivo Changes.md.

Veja Também

Roteiro

Algumas coisas que eu e a comunidade desejamos que sejam adicionadas:

  • Melhores capacidades de depuração e melhores mensagens de erro
  • Sandboxing adequado

GitHub

Você pode encontrar dicas de configuração adicionais e documentação para este módulo no repositório GitHub do nginx-module-template.