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>
    Этот исходный код отформатирован с помощью FractalizeR's HabraSyntax Source Code Highlighter.


    Контекст:
    var context = {
        PageTitle
    : 'jJSt test',
        List
    : ['First', {name:'Second', note:'2th'}, 'Third'],
    };
    var result = njst.parse(html, context, {debug:1});
    Этот исходный код отформатирован с помощью FractalizeR's HabraSyntax Source Code Highlighter.


    Идея


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

    В предыдущих попытках вылить эту идею в жизнь — я применял следующую конструкцию шаблонных вставок:

    <script type="nodejs">...</script>
    Этот исходный код отформатирован с помощью FractalizeR's HabraSyntax Source Code Highlighter.


    Это было хорошо тем, что гарантировало подсветку JS-кода практически в любом редакторе. Плюс это напоминает о том, что код вставки является тем же JavaScript-ом. Но я быстро от этого ушёл, когда составил трезвый список недостатков такого приёма:
    • Много символов, получаются слишком длинные вставки, когда нужно вывести всего-то одно поле;
    • Такие вставки будут визуально путаться верстальщиком с клиентским JavaScript;
    • Необходимость подсветки сама себя исчерпала, ведь в идеале код вставок должен быть минимальным и подсветка там особо даже и не к чему;

    Метаний было не особо много, поскольку я знал что нужно. Во-первых, предусмотреть также короткий и компактный вариант, для вывода конкретного поля без лишнего кода. Во-вторых, использовать символы, которые в синтаксисе JS не встречаются, чтобы избежать конфликтов парсинга. Изначально я хотел использовать <% ... %>, популярный вариант в шаблонизаторах, сколько я знаю. Мой Notepad++ его подсвечивал, я даже взял на заметку, что может это хорошо, что сразу видно код шаблонизатора. Но для HTML-вставок я использовал изначально такую конструкцию:

    <% if (true) { &> html <& } %>
    Этот исходный код отформатирован с помощью FractalizeR's HabraSyntax Source Code Highlighter.


    То-бишь для вывода чистого 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! ';
        }
    #>
    Этот исходный код отформатирован с помощью FractalizeR's HabraSyntax Source Code Highlighter.


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

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

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


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


    Безопасность


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

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

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


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

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


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

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


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

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

    Жду ваших отзывов и пожеланий по доработке.
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 12
    • 0
      Дико извиняюсь за повторный постинг, были проблемы с кармой, получил инвайт на другого пользователя и писал от него, а теперь сделал по человечески. Выражаю свою благодарность людям, которые накинули кармы, чтобы я мог писать как и следует, со своего аккаунта. Спасибо!
      • +2
        Тогда повторюсь, и не ради полемики, а в качестве альтернативного подхода дам ссылку на Jade.
        • 0
          Вообще интересный подход, полностью исключить ручной html, что намекает на строгость к валидному и правильному коду. Ну то-есть, там же нет, например, закрывающих тегов, которые может забыть пьяный верстальщик. Плюс чистота отступов.
          • +1
            Да, в мире Ruby/Rails очень популярный подход.

            Сперва появился Haml. Jade стал его развитием для JavaScript, более простым и удобным. И потом его идеи перекочевали опять в мир Ruby в виде движка Slim.

            Программисты его любят, гораздо удобнее. Дизайнеры привыкают достаточно быстро.
          • 0
            А почему один раз с равно, а второй — без?
                title= pageTitle
                h1 Jade - node template engine
            

            • +1
              второй раз — это просто текст. минус не имеет значения.
              • 0
                Первый раз — тоже просто текст:
                title= pageTitle // <title>pageTitle</title>
                h1 Jade node t e // <h1>Jade node t e</h1>
                

                Результат одинаковый, а синтаксис — разный
                • +1
                  знак = в этих языках означает необходимость вычисления.

                  после = следует конструкция на языке программирования. pageTitle в данном случае — переменная контекста.
                  • 0
                    Во, теперь понял, спасибо)
          • 0
            На мой взгляд слишком уж громоздко все это получается. Я в своем js-шаблонизаторе ввел только 2 конструкции — цикл {*List}{/*} и условие {?()}{:}{/?}, для подавляющего большинства задач этого хватает:

            <html>
            <head>
              <title>{PageTitle}</title>
            </head>
            <body>
              <h1>{PageTitle}</h1>
              <ul>
              {*List}<li>{?(typeof $item !== 'object')}{$item}{:}{$item.name} - {$item.note}{/?}</li>{/*}
              </ul>
            </body>
            </html>
            


            выглядит проще, писать кода меньше, учить синтаксис не нужно, есть только итератор {*...} и условие {?...}. Если нужно что-то особое, можно передать функцию через параметр (контекст). Помимо этого выгоднее шаблон сначала скомпилировать в функцию, а затем вызывать ее для генерации в определенном контексте, при нескольких вызовах будет значительно производительнее.
            • 0
              Скачал, попробовал и влюбился! Давно искал что-то PHP-шное, легкое в восприятии и интеграции.
              Очень жду новых версий, большое Вам спасибо!
              • 0
                Свежая версия 0.2 уже на гитхабе и npm:
                npm install njst

              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.