пятница, 3 июня 2011 г.

Node.JS: Native JavaScript Templates (nJSt). Шаблонизатор, построенный на нативном JavaScript

Приветствую тебя, читатель. Гоняясь за идеей сделать шаблонизатор, основанный на нативном JavaScript — я кое к чему пришёл. В Node.JS для реализации этой задачи нашлось всё, что я мог пожелать, и выполнить задачу получилось настолько же нативными средствами. Например, главным инструментом послужил модуль VM для выполнения изолированного от внешней среды JavaScript-кода. Шаблонные вставки — это чистый JS, но туда не попадают всяческие опасные инструменты вроде скальпеля, require, global и др.

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

HTML-шабон:
<html>
<head>
    <title>#{PageTitle}</title>
</head>

<body>
    <h1>#{PageTitle}</h1>

    <ul>
    <# for (var i=0; i<List.length; i++) { #>
        <li>
            <#
                if (typeof List[i] !== 'object') {
                    show(List[i]);
                } else {
                    show(List[i].name +' - '+ List[i].note);
                }
            #>
        </li>
    <# } #>
    </ul>
</body>
</html>

Контекст:
var context = {
    PageTitle: 'jJSt test',
    List: ['First', {name:'Second', note:'2th'}, 'Third'],
};
var result = njst.parse(html, context, {debug:1});

Идея

Я думаю мне стоит уделить пару слов о том, как я до такого докатился к этому пришёл. Вдохновившись идеей 1С-Битрикса (вы не ослышались) о нативной шаблонизации, с помощью языка, который большинство знает (PHP) — я решил провернуть подобное и с Node.JS. Но тут по-моему вариант даже ещё более выигрышный, ибо это чистый JavaScript. Верстальщик пишет шаблоны, HTML-CSS-JavaScript... Вот-вот, а тут он пишет шаблонные вставки опять же на JavaScript-е, который ему лучше всего должен быть знаком из языков программирования. И ему не надо вникать в тонкости синтаксиса какого-нибудь нового шаблонизатора. Плюс во многих шаблонизаторах всплывают подводные камни, которые не позволяют реализовать вполне тривиальные задачи. Конечно в идеале нужно править логику, в рамках одного проекта — это нормально, но когда требуется универсальность, по любому поводу в логику не полезешь. Разумеется я не предлагаю средство для возможности блеснуть навыками программирования верстальщикам, и расчёт идёт на минимальные необходимые вставки, а не на перенос логики в шаблоны. Этому метод кнута основательно не научит, тут должна самодисциплина место иметь. Хотя если говорить и о методе кнута, то благодаря модулю VM, поставляемому с Node.JS, шансы на реализацию логики в шаблонах существенно уменьшаются.

В предыдущих попытках вылить эту идею в жизнь — я применял следующую конструкцию шаблонных вставок:
<script type="nodejs">...</script>
Это было хорошо тем, что гарантировало подсветку JS-кода практически в любом редакторе. Плюс это напоминает о том, что код вставки является тем же JavaScript-ом. Но я быстро от этого ушёл, когда составил трезвый список недостатков такого приёма:
  • Много символов, получаются слишком длинные вставки, когда нужно вывести всего-то одно поле;
  • Такие вставки будут визуально путаться верстальщиком с клиентским JavaScript;
  • Необходимость подсветки сама себя исчерпала, ведь в идеале код вставок должен быть минимальным и подсветка там особо даже и не к чему;
Метаний было не особо много, поскольку я знал что нужно. Во-первых, предусмотреть также короткий и компактный вариант, для вывода конкретного поля без лишнего кода. Во-вторых, использовать символы, которые в синтаксисе JS не встречаются, чтобы избежать конфликтов парсинга. Изначально я хотел использовать <% ... %>, популярный вариант в шаблонизаторах, сколько я знаю. Мой Notepad++ его подсвечивал, я даже взял на заметку, что может это хорошо, что сразу видно код шаблонизатора. Но для HTML-вставок я использовал изначально такую конструкцию:
<% if (true) { &> html <& } %>
То-бишь для вывода чистого HTML использовались не %> и <%, а &> и <&. И эта подсветка быстро стала проблемой. Я выбрал вариант с решёткой <# if (true) { &> html <& } #>. А спустя время я изменил принцип парсинга и исклчил заморочки с <&>. И в итоге всё стало похоже на аналогичные PHP вставки (кстасти шаблонизтор приближен к его принципу вставок кода). А вот менять решётку на символ процента (<% if (true) { %> html <% } %>) я как-то и не стал. В прочем если считаете, что зря — скажите мне об этом.

Вот вставки, которые доступны в применению:
  • #{variable} — для вывода variable, переданной в контексте;
  • <# jsCode #> — для выполнения JS-кода, циклов, условий и прочего. Так же возможен и вывод с помощью функции show().

Спецификация

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

Во-первых: в #{...} — желательно писать только название ключа из контекста, чтобы вывести конкретное значение, либо имя раннее заданной переменной, никаких лишних операций, пусть и некоторые из них возможны. Ну, например, собрать массив в строку: #{arr.join(', ')} — я думаю будет не великим грехом. Переводы строк и ; здесь намеренно игнорируются. Для всяческого кода (циклов, условий и прочего) использовать <# ... #>.

Если вам нужен вывод внутри вставки <# ... #>, то на помощь вам придёт функция show() или переменная toShow, которую можно наполнить (но не заменить, иначе предыдущий вывод, в т.ч. от show() будет затёрт). Вот пример вывода с использованием этих инструментов:
<#
    while (toShow.length < 100) {
        show('I like it! ');
        toShow += 'I love it! ';
    }
#>

Во-вторых: простое правило в теории, а в реалии про него часто забывают: никакого лишнего кодинга! Только необходимое для вывода, думаю циклов и условий хватит за глаза. Функции хоть и можно создавать, но всегда помните, что это — смертный грех, и если будете так делать — у вас завянуть пальцы, и вы больше не сможете писать функции в шаблонах.

В-третьих: если вы незнакомы со спецификацией JavaScript (пусть и неофициальной), то обязательно начните с неё, читать тут. Если с вашим кодом в потенциале могут работать другие люди, или вы сами спустя какое-то в время, надеясь потом понять самого себя, собираетесь с ним работать, то не избегайте этого пункта. Это очень популярная проблема, я и сам болел, но после того как побывал в роли жертвы, понял как важен этот момент. Иной раз встретишь такое, что мозг никак не может интерпретировать, да и сам автор без психотропных средств с трудом справится.

Область видимости

Один из вопросов, который скорее остальных может промелькнуть у вас в голове: «а если задать переменную var v = true; будет ли она видна в следующей шаблонной вставке вставке?» — ответ — ДА! И если вам трудно понять «как это работает», то хорошо, если вы знаете PHP, представьте, что вставки шаблонизатора nJSt — это вставки PHP, поведение наиболее приближенно именно к такому типу. Так же как и вывод:
#{...} = <?=...?>
<# ... #> = <? ... ?>

Безопасность
 
Как и говорилось выше, внутрь шаблона попадает только контекст. Никаких глобальных объектов (require, global). Перезаписать какую-нибудь внешнюю глобальную переменную тоже не получится. В общем всё в рамках контекста. Конечно, если есть нужда работать с наружностями — передаём нужную функцию в контексте, которая по запросу будет это делать.

Не скрою, прорехи всё-таки есть, но в следующих версиях я займусь их прикрытием. Например, если написать <# while(true)++ #> — мы получим 100% нагрузки на процессор, и пока не вызвать kill по процессу node, можно использовать компьютер как обогреватель, а дальнейшие действия в ноде становятся невозможны. В следующей версии я собираюсь повесить таймер и убивать выполнение, если оно затягивается во избежании таких вот зависаний сервера.

Возможности и применение

Возможности скорей ограничиваются лишь вашей фантазией, поскольку в контекст мы передаём что хотим. На GitHub в примерах есть демо вызова функции для скачивания текущей страницы. Функция скачивания передаётся в контекст и вызывается в шаблоне. Вы можете передать любую функцию в контексте, которая является инструментом вашего проекта, и которая, может быть, оперирует внешней средой. В общем, если вы знаете JS, то не должно возникнуть никаких проблем. А если не знаете, то вот раз и два.

В следующих версиях

  • Защита от зацикливаний (см. безопасность);
  • Возможность возврата изменённого в шаблоне контекста от парсера.

Пользуйтесь на здоровье

Если хотите больше примеров и поподробнее, то см. папку examples на GitHub, там же и качайте сам модуль и изучайте исходные коды.

nJSt на GitHub: https://github.com/unclechu/njst

Жду ваших отзывов и пожеланий по доработке.

2 комментария:

  1. а как это дело правильно прикрутить к express?

    ОтветитьУдалить
  2. Качаем с GitHub-а папку njst в корень проекта и вот пример:

    var app = express.createServer();
    var njst = require('./njst');

    app.get('/', function(req, res){
    res.send(njst.parse('bla - #{test} - bla', {test:'olala'}, {debug:1}));
    });

    app.listen(3000);

    ОтветитьУдалить