nodeJS Быстрый веб-сервер на javascript движке V8

22Мар/101

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

Итак, в написании блога настал момент когда просто выводить HTML стало недостаточно, и я стал искать шаблонизатор для сайта. Выбор пал на Mu – порт Mustache из Ruby, насколько я понял. И хоть с Ruby работать мне не приходилось, синтаксис и набор фич понравился. Чёткое отделение логики от представления (в шаблоне вообще нет кода), вложенные шаблоны, компиляция в функции. В общем, решил изучить.

В качестве шаблона было решено взять один из WordPress’овских. После недолгих поисков выбор пал на Several от NET-TEC Internetmarketing. Портирование, к моему удивлению, заняло не так много времени.

Правда без граблей тоже не обошлось. При первом запуске шаблонизатора я увидел следующую ошибку:

TypeError: Object # has no method 'nextTick'
    at Object.expandPartials (/mnt/hgfs/node/blog/mu/mu/preprocessor.js:23:11)
    at [object Object]. (/mnt/hgfs/node/blog/mu/mu/parser.js:16:23)
    at [object Object].emitSuccess (node.js:259:13)
    at [object Object]. (node.js:652:21)
    at [object Object].emitSuccess (node.js:259:13)
    at node.js:509:29
    at node.js:988:9
    at node.js:992:1

Я уже видел обсуждение в Google-группе насчёт nextTick и сразу понял что у меня просто старая версия (Кстати, версию можно проверить запустив node с параметром -v). После обновления до 0.1.26 всё заработало как надо.

Кстати, пара слов насчёт nextTick. Это превентивная мера для событий, которые могут произойти слишком быстро. Метод Process.nextTick() откладывает выполнение переданной ему функции до следующего цикла event loop’a. Здесь можно остановится поподробнее и рассмотреть что же происходит внутри Node.

Event Loop и все-все-все

События в Node, как известно, происходят асинхронно. Но как на самом деле это просиходит?

Всеми событиями в Node управляет event loop. Каждый цикл Node проверяет какие события произошли (они скапливаются в специальном пайпе) и запускает соответствующие обработчики. Таким образом, когда мы делаем:

1
2
3
4
var promise = posix.readdir("/usr");
promise.addCallback(function (files) {
    puts(files);
});

… у нас на самом деле происходит следующее. На текущем цикле Event loop’а мы выполняем var promise = posix.readdir("/usr") и, так как promise возвращается из функции мгновенно, на том же цикле мы навешиваем на неё Callback. Когда событие «директория прочитана» попадёт в пайп, обработчик уже будет его ждать. В принципе, мы даже можем немного повременить с навешиванием обработчика: чтение с диска может занимать несколько сотен циклов event loop’а.

Проблемы начинаются когда наши события выполняются сразу, а callback добавляется через один или более циклов. Когда успешно завершённое событие попадёт в пайп, могла возникнуть ситуация когда обработчик ещё не был навешен – race condition в чистом виде.

По крайней мере так было до версии 0.1.26. После того как правки Феликса для Promises были смержены в основную вертку, все promises хранят свои значения до тех пор пока не будет навешен обработчик. Если мы сделаем так:

1
2
3
4
5
6
 var promise = posix.readdir("/usr");
  setTimeout(function () {
    promise.addCallback(function (files) {
      puts(files);
    });
  }, 10);

…очевидно, что структура каталога прочитается быстрее чем за 10 секунд. При этом promise запомнит полученный результат и будет ждать обработчика. Как только он будет навешен, событие пойдёт в пайп и выполнится на следующем цикле event loop’а.

Использование шаблонизатора

Ладно, вернёмся к нашим шаблонизаторам. Ставим Mu, создаём папку для наших шаблонов. Синтаксис шаблона выглядит примерно так:

<h1>{{header}}</h1>
  <ul>
  {{#items}}
      <li><strong>{{name}}</strong> [<a href="{{url}}">{{name}}</a>]</li>
  {{/items}}
  </ul>

К сожалению, я не нашёл подходящей подсветки в WordPress :) В любом случае, этот шаблон принимает объект следующего вида:

1
2
3
4
5
6
7
8
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. Вызов шаблонизатора будет выглядеть примерно так:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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}).addCallback(function (output) {
 
	var buffer = '';
 
	output.addListener('data', function (c) {
		buffer += c;
	}).addListener('eof', function () {
		sys.puts(buffer);
	});
})
.addErrback(function (e) {
	sys.puts('Oops:' + JSON.stringify(e));
});

Как видите, Mu рендерит шаблон постепенно. Для небольших страниц это скорее всего не будет заметно, и рендеринг будет происходить сразу. Но большие страницы можно рендерить потоком и, например, сразу отдавать клиенту. Это позволит здорово сэкономить память. Приведённый код предполагает, что шаблон сохранён в файл ./theme/todo.mu. При рендеринге и при включении в другие шаблоны имя шаблона указывается без расширения.

Настройка шаблонизатора

Mu можно довольно гибко настраивать. Помимо расположения шаблонов можно указать используемое расширение:

1
2
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}}

Соответственным образом изменится и передаваемый объект:

1
2
3
4
5
6
7
8
9
10
11
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}}). Функции могут обращаться к другим переменным шаблона:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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: элемент шаблона заменяется пустой строкой

Источник: Механический мир

Комментарии (1) Пинги (0)
  1. var mu = require(‘./mu/mu’);
    Необходимо писать Mu, чувствительность к регистру.


Оставить комментарий


Нет обратных ссылок на эту запись.