Шаблонизаторы в Node: Mu
Итак, в написании блога настал момент когда просто выводить HTML стало недостаточно, и я стал искать шаблонизатор для сайта. Я выбрал Mu — порт Mustache из Ruby, насколько я понял. И хоть с Ruby работать мне не приходилось, синтаксис и набор фич понравился. Чёткое отделение логики от представления (в шаблоне вообще нет кода), вложенные шаблоны, компиляция в функции. В общем, решил изучить.
В качестве шаблона было решено взять один из WordPress’овских. После недолгих поисков выбор пал на Several от NET-TEC Internetmarketing. Портирование, к моему удивлению, заняло не так много времени.
Использование шаблонизатора
Установка шаблонизатора проходит без проблем. К сожалению npm-модуля пока нет, так что ставить придётся из репозитория. После установки создаём папку для наших шаблонов. Синтаксис шаблона выглядит примерно так:
<h1>{{header}}</h1>
<ul>
{{#items}}
<li><strong>{{name}}</strong> [<a href="{{url}}">{{name}}</a>]</li>
{{/items}}
</ul>
К сожалению, я не нашёл подходящей подсветки в WordPress
В любом случае, этот шаблон принимает объект следующего вида:
var data = {
'header': 'Todo list',
'items': [
{'name':'Buy a calf', 'url':'http://ebay.com/'},
{'name':'Raise cow', 'url':'http://www.calfnotes.com/'},
{'name':'Sell milk', 'url':'http://ebay.com/'}
]
};
Переменные шаблона, начинающиеся с #—это итераторы (и другие полезные вещи, как Вы потом увидите). В нашем случае для каждого элемента массива items будет создан свой пункт списка. Обратите внимание, что внутри итератора мы обращаемся непосредственно к свойствам объектов массива — примерно как с функцией with. В результате рендеринга шаблона получится следующий HTML:
<h1>Todo list</h1>
<ul>
<li><strong>Buy a calf</strong> [<a href="http://ebay.com/">Buy a calf</a>]</li>
<li><strong>Raise cow</strong> [<a href="http://www.calfnotes.com/">Raise cow</a>]</li>
<li><strong>Sell milk</strong> [<a href="http://ebay.com/">Sell milk</a>]</li>
</ul>
Ещё одна полезная возможность: можно включать одни шаблоны внутрь других. Получившийся шаблон будет отрендерён как один файл. Вложенные шаблоны делаются конструкцией {{>header}}, где header — имя шаблона.
Рендеринг шаблона
Для того чтобы отрисовать шаблон, надо сохранить его в директорию шаблонов с расширением .mu. Вызов шаблонизатора будет выглядеть примерно так:
var Mu = require('./mu/mu');
var sys = require('sys');
Mu.templateRoot = './theme'; // здесь лежат шаблоны
var data = {
'header': 'Todo list',
'items': [
{'name':'Buy a calf', 'url':'http://ebay.com/'},
{'name':'Raise cow', 'url':'http://www.calfnotes.com/'},
{'name':'Sell milk', 'url':'http://ebay.com/'}
]
};
Mu.render('todo', data, {chunkSize: 10}, function (err, output) {
if (err) {
throw err;
}
var buffer = '';
output.on('data', function (c) {
buffer += c;
}).on('end', function () {
sys.puts(buffer);
});
});
Как видите, Mu рендерит шаблон постепенно. Для небольших страниц это скорее всего не будет заметно, и рендеринг будет происходить сразу. Но большие страницы можно рендерить потоком и, например, сразу отдавать клиенту. Это позволит здорово сэкономить память. Приведённый код предполагает, что шаблон сохранён в файл ./theme/todo.mu. При рендеринге и при включении в другие шаблоны имя шаблона указывается без расширения.
Настройка шаблонизатора
Mu можно довольно гибко настраивать. Помимо расположения шаблонов можно указать используемое расширение:
var mu = require('./mu/mu');
Mu.templateExtension = 'jstpl';
Уже в шаблоне можно сменить синтаксис разделителей — заменить двойные фигурные скобки чем то другим. Делается это так:
{{=<% %>=}}
<h1><%header%></h1>
<ul>
<%#items%>
<li><strong><%name%></strong> [<a href="<%url%>"><%name%></a>]</li>
<%/items%>
</ul>
Так как логику в шаблон переносить нельзя, особые случаи вроде пустого списка придётся обрабатывать с помощью дополнительной переменной:
<h1>{{header}}</h1>
{{#list}}
<ul>
{{#items}}
<li><strong>{{name}}</strong> [<a href="{{url}}">{{name}}</a>]</li>
{{/items}}
</ul>
{{/list}}
{{#emptylist}}
<p>The list is empty</p>
{{/emptylist}}
Соответственным образом изменится и передаваемый объект:
var data = {
'header': 'Todo list',
'list': [
'items': [
{'name':'Buy a calf', 'url':'http://ebay.com/'},
{'name':'Raise cow', 'url':'http://www.calfnotes.com/'},
{'name':'Sell milk', 'url':'http://ebay.com/'}
]
],
'emptylist': false
};
Использование изменяемых элементов шаблона
Впрочем, способ управлять представлением всё таки есть. Элементам объекта можно передавать функции, которые будут выполнены шаблонизатором. Их значение будет использовано для подстановки, если соответствующий элемент шаблона — динамический (задан как {{#element}}). Функции могут обращаться к другим переменным шаблона:
var items = [
{'name':'Buy a calf', 'url':'http://ebay.com/'},
{'name':'Raise cow', 'url':'http://www.calfnotes.com/'},
{'name':'Sell milk', 'url':'http://ebay.com/'}
];
var data = {
'header': 'Todo list',
'items': items,
'list': function() {
return this.item.length !== 0;
},
'emptylist': function() {
return this.item.length === 0;
}
};
Если в шаблонизатор будет передан непустой массив items, он будет выведен в виде списка. Если в массиве не будет элементов, будет выведено соответствующее сообщение. В этом случае элемент шаблона {{#list}} работает не как итератор, а как условие. Действие каждого такого оператора определяется типом данных, переданных в шаблонизатор с соответствующим именем. Если передана функция, используется тип возвращаемого ей значения. В нашем случае переданные в list и emptylist вернули boolean-значения, и соответствующие элементы шаблона сработали как условные операторы. Вот как действуют разные типы данных на динамический элемент шаблона:
- Функция: функция выполняется, полученное значение используется для определения действия
- Массив: элемент шаблона действует как итератор, перебирая значения массива
- Двоичная переменная: элемент шаблона действует как условный оператор
- Объект: элемент шаблона заменяется строковым представлением объекта
- Undefined: элемент шаблона заменяется пустой строкой

В примере рендеринга шаблона поправьте ошибочку в строке var mu = require(‘./mu/mu’);
Переменная должна быть с большой буквы, иначе дальнейший код вызывает ошибку)))
Спасибо, поправил.
Я бы только сформулировал немного по-другому.
nextTick– способ вручную вернуть на время управление в event-loop. Если кто занимался программированием под Delphi, возможно вспомнят методApplication.ProcessMessages.А нужен он вот зачем. Предположим у нас есть метод
process90GBdataкоторый выполняется очень долго. Причем это чистый javascript-метод, не делающий никакого IO, то есть способности nodejs оказываются здесь бесполезны: будучи вызванным он наглухо вешает весь сервер до тех пор пока его выполнение не прервется.Это нам не нравится и предположим, что мы нашли способ разбить этот метод на последовательность методов
process25RBdata. Но загвоздка в том, что если написать просто:function process90GBdata () { var i for (i = 0; i < 3600000; i++) { process25KBdata() } }то это ничего по сути не изменит: для event-loop’а это будет опять то же непрерывный метод. А вот если написать
function process90GBdata () { var count = 3600000, next = function () { if (count--) { process25KBdata() process.nextTick(function () { next() }) } } next() }управление будет возвращаться в event-loop после каждого вызова
process25RBdata.Хорошее сравнение с Delphi
Да, по сути process.nextTick даёт event loop’у возможность разгрести накопившуюся очередь задач (обработчик nextTick попадает в конец этой очереди).
Но вообще на момент публикации этой статьи nextTick применялся именно для этого – немного отсрочить выполнение события, чтобы обработчик не попал в пул задач слишком рано. Это делалось из за особенностей работы event loop, когда дополнительные обработчики событий могли быть навешены уже после того как событие произошло и основной обработчик был запущен. Насколько я знаю, этот баг уже пофиксили, но статью я пока не поправил.
Ага, теперь понял. Но тогда по-моему в статье пример не совсем удачный:
setTimeout(someFunc, 10)выполнится все равно позже, чем то что выполнится поnextTickи коллбек не сработает. Правда не уверен, не проверял.Вообще, возвращаясь к шаблонам, по отношению их использования в nodejs у меня есть одна большая претензия: их синхронность.
Все идеи этих шаблонизаторов были взяти из других фрейворков (тот же Mu – как и написано в статье – порт Mustache из Ruby), а другие фреймворки разрабатывались с закладкой на поточную модель паралеллизации. Помещенные в среду nodejs все они начинают требовать чтобы для них подготовили один большой json c данными и они бы его отрендерили. Синхронно и сразу весь. С таким подходом они все преврашаются в аналог функции
process90GBdata.PS nodejs.ru твое детище? Я могу (и если да, то как) туда заслать статейку-другую?
Nodejs.ru детище Олега и моё (как автора статей). Статейки заслать конечно можно, будем рады разместить
Можно отправлять на bolter.fire [at] gmail.com.
Насчёт таймаута: да, someFunc выполнится позже, но promise всё равно запустил бы обработчик – потому что мы вешаем Callback на уже заведомо успешное событие – и выполнил бы его с нужными аргументами.
Да, с синхронностью шаблонов обычно проблемы. Т.е. многие шаблоны асинхронны в том смысле что обрабатывают файл шаблона в потоковом режиме (не дожидаясь его полной загрузки), но естественно им для этого нужен полный JSON, чтобы знать что именно чем заменять.
в новых версиях выдает ошибку при запуске, изменился синтаксис:
Mu.render(‘todo’, data, {chunkSize: 10}, function (err,output) {
if(err) { sys.puts(‘Oops:’ + JSON.stringify(err)); throw err; }
var buffer = ”;
output.addListener(‘data’, function (c) { buffer += c; })
.addListener(‘end’, function () { sys.puts(buffer); });
});
Спасибо за замечание
Про сборку страницы из нескольких кусков (partials) можно где-то почитать?
Насколько я знаю только в сорцах ) В примерах к Mu было применение partials, но там было только простое использование.
Вот пример, в котором шаблон собирается из кусков:
https://github.com/ivanit/mu-partials-example
Только там всё немного костыльно.