Skip to content

Шаблонизаторы в Node: Mu

05/02/2010

Итак, в написании блога настал момент когда просто выводить 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: элемент шаблона заменяется пустой строкой

Ссылки по теме:

16 комментариев
  1. sketch43 permalink

    В примере рендеринга шаблона поправьте ошибочку в строке var mu = require(‘./mu/mu’);

    Переменная должна быть с большой буквы, иначе дальнейший код вызывает ошибку)))

  2. Michel Beloshitsky permalink

    Кстати, пара слов насчёт nextTick. Это превентивная мера для событий, которые могут произойти слишком быстро.

    Я бы только сформулировал немного по-другому. 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, когда дополнительные обработчики событий могли быть навешены уже после того как событие произошло и основной обработчик был запущен. Насколько я знаю, этот баг уже пофиксили, но статью я пока не поправил.

  3. Michel Beloshitsky permalink

    Но вообще на момент публикации этой статьи nextTick применялся именно для этого – немного отсрочить выполнение события, чтобы обработчик не попал в пул задач слишком рано.

    Ага, теперь понял. Но тогда по-моему в статье пример не совсем удачный: 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, чтобы знать что именно чем заменять.

  4. kpeo permalink

    в новых версиях выдает ошибку при запуске, изменился синтаксис:

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

  5. vanyasmart permalink

    Про сборку страницы из нескольких кусков (partials) можно где-то почитать?

Trackbacks & Pingbacks

  1. Node.js 0.1.32 « nodeJS
  2. Шаблонизатор: Mu « nodeJS
  3. Пара слов о асинхронности шаблонизаторов для node.js и nannou « Интернет-страничка лаборанта

Добавить комментарий

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход / Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход / Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход / Изменить )

Google+ photo

Для комментария используется ваша учётная запись Google+. Выход / Изменить )

Connecting to %s

%d такие блоггеры, как: