Перейти к содержанию

template: Движок шаблонов (HTML) для Lua и nginx-module-lua

Установка

Если вы еще не настроили подписку на репозиторий 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

Чтобы использовать эту библиотеку Lua с NGINX, убедитесь, что nginx-module-lua установлен.

Этот документ описывает lua-resty-template v2.0, выпущенный 24 февраля 2020 года.


lua-resty-template — это компилирующий (1) (HTML) движок шаблонов для Lua и OpenResty.

(1) под компиляцией мы понимаем, что шаблоны переводятся в функции Lua, которые вы можете вызывать или string.dump как бинарные байт-коды на диск, которые могут быть позже использованы с lua-resty-template или стандартными функциями Lua load и loadfile (см. также Предкомпиляция шаблонов). Хотя, в общем, вам не нужно это делать, так как lua-resty-template обрабатывает это за кулисами.

Пример "Hello World" с lua-resty-template

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!" })

Содержание

Синтаксис шаблонов

Вы можете использовать следующие теги в шаблонах:

  • {{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 (в данном случае), см. использование блоков. Не используйте предопределенные имена блоков 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"}

И вы хотите отобразить значение foobar из ctx["foo:bar"] в вашем шаблоне. Вам нужно явно указать это, ссылаясь на context в вашем шаблоне:

{# {*["foo:bar"]*} не сработает, вам нужно использовать: #}
{*context["foo:bar"]*}

Или в целом:

template.render([[
{*context["foo:bar"]*}
]], {["foo:bar"] = "foobar"})
Слово о HTML экранировании

Только строки экранируются, функции вызываются без аргументов (рекурсивно), и результаты возвращаются как есть, другие типы преобразуются в tostring. nil и ngx.null преобразуются в пустые строки "".

Экранированные HTML символы:

  • & становится &amp;
  • < становится &lt;
  • > становится &gt;
  • " становится &quot;
  • ' становится &#39;
  • / становится &#47;

Пример

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 — хранит функцию помощника 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.

Конфигурация 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 возможно переопределить $template_root и $template_location с помощью кода Lua:

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, но у таблицы также есть метатаблица с определенным __tostring. См. пример ниже. Аргументы view и layout могут быть либо строками, либо путями к файлам, но макет также может быть таблицей, созданной ранее с помощью template.new.

С 2.0 новый экземпляр также может быть использован без аргументов, что создает новый экземпляр шаблона:

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

Вы также можете передать таблицу, которая затем будет изменена, чтобы стать шаблоном:

local config = {
  root = "/templates"
}

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

Это удобно, так как template, созданный с помощью new, не делит кэш с глобальным шаблоном, возвращаемым require "resty.template" (это было сообщено в проблеме #25).

Вы также можете передать логическое значение true или false в качестве параметра view, что означает, что будет возвращена либо безопасная, либо небезопасная версия шаблона:

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

Также доступна стандартная реализация safe:

local safe = require "resty.template.safe"
-- вы также можете создать экземпляр safe:
local safe_instance = safe.new()

Версия safe использует шаблон обработки ошибок Lua return nil, err, а 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([[
Как дела, {{user.name}}?

Вот новый рецепт для вас!

{% for i, ingredient in ipairs(ingredients) do %}
  {*i*}. {{ingredient}}
{% end %}
{-ad-}`lua-resty-template` движок шаблонов для OpenResty!{-ad-}
]])

local content = func{
  user = {
    name = "bungle"
  },
  ingredients = {
    "картошка",
    "колбасы"
  }
}

print(content)

Это выведет следующее:

  visit: 1
content: Как дела,

  visit: 2
   type: {
content: user.name

  visit: 3
content: ?

Вот новый рецепт для вас!

  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` движок шаблонов для OpenResty!

  visit: 10
content: `lua-resty-template` движок шаблонов для OpenResty!

Как дела, bungle?

Вот новый рецепт для вас!

  1. картошка
  2. колбасы

Функции-посетители должны иметь следующую сигнатуру:

string function(content, type, name)

Если функция не изменяет content, она должна вернуть content обратно, как это делает вышеуказанный посетитель.

Вот немного более сложный пример посетителя, который обрабатывает ошибки времени выполнения в выражениях:

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

template.render "Вычисление: {{i*10}}"

Это приведет к ошибке времени выполнения с:

ERROR: [string "context=... or {}..."]:7: попытка выполнить арифметическую операцию над глобальной 'i' (nil значение)
стек вызовов:
    resty/template.lua:652: в функции 'render'
    a.lua:52: в функции 'file_gen'
    init_worker_by_lua:45: в функции <init_worker_by_lua:43>
    [C]: в функции 'xpcall'
    init_worker_by_lua:52: в функции <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 "Вычисление: {{i*10}}\n"
template.render("Вычисление: {{i*10}}\n", { i = 1 })

Это выведет:

Вычисление:
Вычисление: 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)

Предкомпилирует шаблон как бинарный фрагмент. Этот бинарный фрагмент можно записать как файл (и вы можете использовать его напрямую с load и loadfile Lua). Для удобства вы можете опционально указать аргумент path, чтобы вывести бинарный фрагмент в файл. Вы также можете указать параметр strip со значением false, чтобы предкомпилированные шаблоны имели отладочную информацию (по умолчанию true). Последний параметр plain означает, что компиляция должна рассматривать view как строку (plain = true) или как путь к файлу (plain = false) или сначала пытаться как файл, а затем переходить к строке (plain = 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()

-- Либо вы можете просто записать (что делает то же самое, что и выше)
template.precompile(view, "precompiled-bin.html")

template.render("precompiled-bin.html", {
    title = "Имена",
    "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 вызывает эту функцию перед тем, как начать разбор шаблона (предполагая, что необязательный аргумент plain в template.parse оценивается как false или nil (по умолчанию). По умолчанию в lua-resty-template есть два загрузчика: один для Lua и другой для Nginx / OpenResty. Пользователи могут переопределить это поле своей собственной функцией. Например, вы можете написать функцию загрузчика шаблонов, которая загружает шаблоны из базы данных.

Стандартный template.load для Lua (присоединенный как template.load при использовании напрямую с 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

Стандартный template.load для Nginx / OpenResty (присоединенный как template.load при использовании в контексте 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

Как видите, lua-resty-template всегда пытается (по умолчанию) загрузить шаблон из файла (или с помощью ngx.location.capture), даже если вы предоставили шаблон в виде строки. lua-resty-template. Но если вы знаете, что ваши шаблоны всегда являются строками, а не путями к файлам, вы можете использовать аргумент plain в template.compile, template.render и template.parse ИЛИ заменить template.load на самый простой возможный загрузчик шаблонов (но будьте осторожны, если ваши шаблоны используют включения {(file.html)}, они также считаются строками, в этом случае file.html будет строкой шаблона, которая будет разобрана) — вы также можете настроить загрузчик, который находит шаблоны в какой-либо системе базы данных, например, Redis:

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

Если параметр plain равен false (значение nil не считается false), все проблемы с file 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("<!-- Вывод от моей функции -->")
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[[
начало
{%
for i=1, 10 do
  echo("\tстрока: ", i, "\n")
end
%}
конец
]]

Это выведет:

начало
    строка: 1
    строка: 2
    строка: 3
    строка: 4
    строка: 5
    строка: 6
    строка: 7
    строка: 8
    строка: 9
    строка: 10
конец

Это также можно записать так, но echo может быть полезен в некоторых случаях:

require "resty.template".render[[
начало
{% for i=1, 10 do %}
  строка: {* i *}
{% end %}
конец
]]

include(view, context)

Это в основном используется внутри с {(view.hmtl)}, {["view.hmtl"]} и с блоками {-block-name-}..{-block-name-}. Если context не задан, используется контекст, используемый для компиляции родительского представления. Эта функция скомпилирует view и вызовет результирующую функцию с context (или контекстом родительского представления, если не задан).

Другие способы расширения

Хотя lua-resty-template не имеет много инфраструктуры или способов его расширения, у вас все же есть несколько возможностей, которые вы можете попробовать.

  • Добавление методов к глобальным типам string и table (хотя это не рекомендуется)
  • Обернуть ваши значения чем-то перед добавлением их в контекст (например, прокси-таблицей)
  • Создание глобальных функций
  • Добавление локальных функций либо в таблицу template, либо в таблицу context
  • Использование метаметодов в ваших таблицах

Хотя модификация глобальных типов кажется удобной, это может иметь неприятные побочные эффекты. Поэтому я предлагаю вам сначала ознакомиться с этими библиотеками и статьями:

Вы можете, например, добавить _ от 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>Пользователь {{name}} в возрасте {{age}}</li>
Вывод
<html>
<body>
<ul>
    <li>Пользователь Jane в возрасте 29</li>
    <li>Пользователь John в возрасте 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 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>Тестирование 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.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-}
  это боковая панель
{-sidebar-}

{-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-}
  это боковая панель
{-sidebar-}

{-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 = "оригинал",
    items = { a = "оригинал-a", b = "оригинал-b" }
})

И macro-example.html:

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

Это выведет:

<div>оригинал</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>оригинал</div>
<span>оригинал-a</span>

Но это еще более гибко, давайте попробуем другой макрос-функцию:

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

Это выведет:

<div>оригинал</div>
<div>оригинал-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>оригинал</div>
<div>оригинал-a</div>
<div>оригинал-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[[
#Привет, мир

Тестирование Markdown.
]]*}
</body>
</html>
]=]
Вывод
<html>
<body>
<h1>Привет, мир</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([[
#Привет, мир

Тестирование Markdown с "SmartyPants"...
]], { smartypants = true })*}
</body>
</html>
]=]
Вывод
<html>
<body>
<h1>Привет, мир</h1>

<p>Тестирование Markdown с &ldquo;SmartyPants&rdquo;&hellip;</p>
</body>
</html>

Вы также можете добавить уровень кэширования для ваших Markdown или вспомогательных функций вместо того, чтобы напрямую помещать библиотеку Hoedown как функцию помощника в template.

Lua Server Pages (LSP) с OpenResty

Lua Server Pages или LSP похожи на традиционные PHP или Microsoft Active Server Pages (ASP), где вы можете просто размещать файлы исходного кода в корне вашего документа (вашего веб-сервера) и обрабатывать их компиляторами соответствующих языков (PHP, VBScript, JScript и т.д.). Вы можете довольно близко имитировать это, иногда называемое спагетти-стилем разработки, легко с помощью lua-resty-template. Те, кто занимался разработкой ASP.NET Web Forms, знают концепцию файлов Code Behind. Есть что-то похожее, но на этот раз мы называем это макетом спереди (вы можете включать модули Lua с обычными вызовами require, если хотите в LSP). Чтобы помочь вам понять концепции, давайте рассмотрим небольшой пример:

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)';
    }
  }
}

Вышеуказанная конфигурация создает глобальную переменную template в среде Lua (вы можете не хотеть этого). Мы также создали местоположение, чтобы соответствовать всем файлам .lsp (или местоположениям), а затем просто рендерим шаблон.

Предположим, что запрос идет на index.lsp.

index.lsp
{%
layout = "layouts/default.lsp"
local title = "Привет, мир!"
%}
<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 Server Pages</title>
</head>

Здесь только статические вещи.

Вывод

Итоговый вывод будет выглядеть так:

<html>
<head>
  <title>Тестирование Lua Server Pages</title>
</head>
<body>
  <h1>Привет, мир!</h1>
</body>
</html>

Как вы видите, lua-resty-template может быть довольно гибким и простым в начале. Просто разместите файлы в корне вашего документа и используйте обычный стиль разработки "сохранить и обновить". Сервер автоматически подберет новые файлы и перезагрузит шаблоны (если кэширование отключено) при сохранении.

Если вы хотите передать переменные в макеты или включения, вы можете добавить данные в таблицу контекста (в приведенном ниже примере см. context.title):

{%
layout = "layouts/default.lsp"
local title = "Привет, мир!"
context.title = 'Мое приложение - ' .. title
%}
<h1>{{title}}</h1>

Часто задаваемые вопросы

Как очистить кэш шаблонов

lua-resty-template автоматически кэширует (если кэширование включено) результирующие функции шаблонов в таблице template.cache. Вы можете очистить кэш, выполнив template.cache = {}.

Где используется lua-resty-template

  • jd.com – Jingdong Mall (китайский: 京东商城; пиньинь: Jīngdōng Shāngchéng), ранее 360Buy, является китайской электронной коммерческой компанией

Пожалуйста, дайте мне знать, если в этом списке есть ошибки или устаревшая информация.

Альтернативы

Вы также можете посмотреть на эти (как альтернативы или для смешивания их с lua-resty-template):

lua-resty-template изначально был форком tirtemplate.lua Тора Хвима, который он извлек из веб-фреймворка Zed Shaw (http://tir.mongrel2.org/). Спасибо Тору и Зеду за их предыдущие вклады.

Бенчмарки

Существует небольшой микробенчмарк, расположенный здесь: 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)

Вот некоторые результаты с моего настольного компьютера (старый Mac Pro 2010 года):

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

Запуск 1000 итераций в каждом тесте
    Время разбора: 0.010759
Время компиляции: 0.054640 (шаблон)
Время компиляции: 0.000213 (шаблон, кэшированный)
  Время выполнения: 0.061851 (тот же шаблон)
  Время выполнения: 0.006722 (тот же шаблон, кэшированный)
  Время выполнения: 0.092698 (разный шаблон)
  Время выполнения: 0.009537 (разный шаблон, кэшированный)
  Время выполнения: 0.092452 (разный шаблон, разный контекст)
  Время выполнения: 0.010106 (разный шаблон, разный контекст, кэшированный)
      Общее время: 0.338978
Запуск 1000 итераций в каждом тесте
    Время разбора: 0.011633
Время компиляции: 0.060598 (шаблон)
Время компиляции: 0.000243 (шаблон, кэшированный)
  Время выполнения: 0.068009 (тот же шаблон)
  Время выполнения: 0.007307 (тот же шаблон, кэшированный)
  Время выполнения: 0.071339 (разный шаблон)
  Время выполнения: 0.007150 (разный шаблон, кэшированный)
  Время выполнения: 0.066766 (разный шаблон, разный контекст)
  Время выполнения: 0.006940 (разный шаблон, разный контекст, кэшированный)
      Общее время: 0.299985
Запуск 1000 итераций в каждом тесте
    Время разбора: 0.012458
Время компиляции: 0.050013 (шаблон)
Время компиляции: 0.000249 (шаблон, кэшированный)
  Время выполнения: 0.057579 (тот же шаблон)
  Время выполнения: 0.006959 (тот же шаблон, кэшированный)
  Время выполнения: 0.065352 (разный шаблон)
  Время выполнения: 0.007133 (разный шаблон, кэшированный)
  Время выполнения: 0.060965 (разный шаблон, разный контекст)
  Время выполнения: 0.007726 (разный шаблон, разный контекст, кэшированный)
      Общее время: 0.268434
Запуск 1000 итераций в каждом тесте
    Время разбора: 0.009466
Время компиляции: 0.053116 (шаблон)
Время компиляции: 0.000209 (шаблон, кэшированный)
  Время выполнения: 0.059017 (тот же шаблон)
  Время выполнения: 0.006129 (тот же шаблон, кэшированный)
  Время выполнения: 0.061882 (разный шаблон)
  Время выполнения: 0.006613 (разный шаблон, кэшированный)
  Время выполнения: 0.059104 (разный шаблон, разный контекст)
  Время выполнения: 0.005761 (разный шаблон, разный контекст, кэшированный)
      Общее время: 0.261297
Запуск 1000 итераций в каждом тесте
    Время разбора: 0.005198
Время компиляции: 0.029687 (шаблон)
Время компиляции: 0.000082 (шаблон, кэшированный)
  Время выполнения: 0.033824 (тот же шаблон)
  Время выполнения: 0.003130 (тот же шаблон, кэшированный)
  Время выполнения: 0.075899 (разный шаблон)
  Время выполнения: 0.007027 (разный шаблон, кэшированный)
  Время выполнения: 0.070269 (разный шаблон, разный контекст)
  Время выполнения: 0.007456 (разный шаблон, разный контекст, кэшированный)
      Общее время: 0.232572

``` Запуск 1000 итераций в каждом тесте Время разбора: 0.003647 Время компиляции: