Статьи

Handlebars.js: взгляд за кулисами

Handlebars набирает популярность благодаря внедрению их в такие фреймворки, как Meteor и Ember.js, но что на самом деле происходит за кулисами этого захватывающего движка шаблонов?

В этой статье мы подробно рассмотрим процесс, который проходит Handlebars для компиляции ваших шаблонов.

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

При использовании шаблона Handlebars вы, вероятно, знаете, что сначала вы скомпилируете источник шаблона в функцию с помощью Handlebars.compile() а затем используете эту функцию для генерации окончательного HTML-кода, передавая значения для свойств и заполнителей.

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

  • Токенизируйте источник на компоненты.
  • Обработайте каждый токен в набор операций.
  • Преобразуйте стек процессов в функцию.
  • Запустите функцию с контекстом и помощниками для вывода некоторого HTML.

В этой статье мы будем создавать инструмент для анализа шаблонов Handlebars на каждом из этих этапов, поэтому для лучшего отображения результатов на экране я буду использовать подсветку синтаксиса prism.js, созданную единственной Lea Verou . Загрузите минимизированный источник, не забывая проверять JavaScript в разделе языков.

Следующий шаг — создать пустой HTML-файл и заполнить его следующим текстом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<!DOCTYPE HTML>
<html xmlns=»http://www.w3.org/1999/html»>
    <head>
        <title>Handlebars.js</title>
        <link rel=»stylesheet» href=»prism.css»></p>
 
        <script src=»prism.js» data-manual></script>
        <script src=»handlebars.js»></script>
    </head>
    <body>
        <div id=»analysis»>
            <div id=»tokens»><h1>Tokens:</h1></div>
            <div id=»operations»><h1>Operations:</h1></div>
            <div id=»output»><h1>Output:</h1></div>
            <div id=»function»>
                <h1>Function:</h1>
                <pre><code class=»language-javascript» id=»source»></code></pre>
            </div>
        </div>
        <script id=»dt» type=»template/handlebars»>
        </script>
 
        <script>
            //Code will go here
        </script>
    </body>
</html>

Это просто некоторый стандартный код, который включает в себя руль и призму, а затем настраивает некоторые элементы div для различных шагов. Внизу вы можете увидеть два блока скриптов: первый для шаблона, а второй для нашего кода JS.

Я также написал немного CSS, чтобы все было немного лучше, и вы можете добавить его:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
body{
    margin: 0;
    padding: 0;
    font-family: «opensans», Arial, sans-serif;
    background: #F5F2F0;
    font-size: 13px;
}
#analysis {
    top: 0;
    left: 0;
    position: absolute;
    width: 100%;
    height: 100%;
    margin: 0;
    padding: 0;
}
#analysis div {
    width: 33.33%;
    height: 50%;
    float: left;
    padding: 10px 20px;
    box-sizing: border-box;
    overflow: auto;
}
#function {
    width: 100% !important;
}

Далее нам нужен шаблон, поэтому давайте начнем с самого простого шаблона, просто статического текста:

01
02
03
04
05
06
07
08
09
10
11
<script id=»dt» type=»template/handlebars»>
    Hello World!
</script>
 
<script>
    var src = document.getElementById(«dt»).innerHTML.trim();
 
    //Display Output
    var t = Handlebars.compile(src);
    document.getElementById(«output»).innerHTML += t();
</script>

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

Основной вывод

Первый шаг, выполняемый рулем в вашем шаблоне, состоит в том, чтобы токенизировать источник. Это означает, что нам нужно разбить источник на отдельные компоненты, чтобы мы могли обрабатывать каждый фрагмент соответствующим образом. Так, например, если бы был какой-то текст с заполнителем в середине, то Handlebars отделяли бы текст перед заполнителем, помещая его в один токен, тогда сам заполнитель помещался бы в другой токен, и, наконец, весь текст после заполнителя будет помещен в третий токен. Это потому, что эти части должны сохранять порядок шаблона, но они также должны обрабатываться по-разному.

Этот процесс выполняется с помощью функции Handlebars.parse() , и вы получаете объект, который содержит все сегменты или «операторы».

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

Итак, мы начнем с запуска исходного шаблона в Handlebars.parse чтобы получить список токенов. Затем мы перебираем все отдельные компоненты и строим набор удобочитаемых строк на основе типа сегмента. Обычный текст будет иметь тип «содержимого», который мы можем затем просто вывести на экран строку, заключенную в кавычки, чтобы показать, чему она равна. Заполнители будут иметь тип «усы», который мы затем можем отобразить вместе с их «id» (имя заполнителя). И, наконец, что не менее важно, у помощников блоков будет тип «блока», который мы затем можем также просто отобразить внутренним «идентификатором» блока (именем блока).

Обновив это сейчас в браузере, вы должны увидеть только один «строковый» токен с текстом нашего шаблона.

Лексемы

Как только руль получает коллекцию токенов, он перебирает каждый из них и «генерирует» список предопределенных операций, которые необходимо выполнить для компилируемого шаблона. Этот процесс выполняется с использованием объекта Handlebars.Compiler() , передавая объект токена с шага 1:

Здесь мы собираем токены в последовательность операций, о которой я говорил, и затем циклически повторяем каждый из них и создаем аналогичный список, как на первом шаге, за исключением того, что здесь нам просто нужно напечатать код операции. Код операции — это «операция» или «имя» функции, которые необходимо запустить для каждого элемента в последовательности.

Вернувшись в браузер, вы теперь должны увидеть только одну операцию с именем ‘appendContent’, которая добавит значение к текущему ‘буферу’ или ‘строке текста’. Существует множество различных кодов операций, и я не думаю, что я квалифицирован, чтобы объяснить некоторые из них, но быстрый поиск в исходном коде для данного кода операции покажет вам функцию, которая будет выполняться для него.

Коды операций

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

Первая строка создает компилятор, передающий в последовательности операций, и эта строка возвратит последнюю функцию, использованную для генерации шаблона. Затем мы преобразуем функцию в строку и сообщаем Prism, чтобы синтаксис выделил ее

С этим окончательным кодом ваша страница должна выглядеть примерно так:

Функция

Эта функция невероятно проста, так как была только одна операция, она просто возвращает данную строку; Давайте теперь посмотрим на редактирование шаблона и увидим, как эти индивидуально прямые шаги сгруппированы, чтобы сформировать очень мощную абстракцию.


Давайте начнем с чего-то простого, и давайте просто заменим слово «Мир» на заполнитель; Ваш новый шаблон должен выглядеть следующим образом:

1
2
3
<script id=»dt» type=»template/handlebars»>
       Hello {{name}}!
   </script>

И не забудьте передать переменную, чтобы результат выглядел нормально:

Запустив это, вы обнаружите, что добавление только одного простого заполнителя значительно усложняет процесс.

Единый заполнитель

Сложный раздел if / else заключается в том, что он не знает, является ли заполнитель на самом деле заполнителем или вспомогательным методом.

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

Далее в разделе операций есть немало дополнений. Если вы помните ранее, чтобы просто вывести некоторый текст, Handlebars использует операцию ‘appendContent’, которую вы теперь можете видеть вверху и внизу списка (как для «Hello», так и для «!»). Остальное посередине — все операции, необходимые для обработки заполнителя и добавления экранированного содержимого.

Наконец, в нижнем окне вместо того, чтобы просто возвращать строку, на этот раз он создает буферную переменную и обрабатывает один токен за раз. Сложный раздел if / else заключается в том, что он не знает, является ли заполнитель на самом деле заполнителем или вспомогательным методом. Таким образом, он пытается увидеть, существует ли вспомогательный метод с заданным именем, и в этом случае он вызовет вспомогательный метод и установит значение «stack1». В случае, если это заполнитель, он присваивает значение из переданного контекста (здесь он называется «глубиной 0»), а если функция была передана, он помещает результат функции в переменную «стек1». Как только это все сделано, оно ускользает от него, как мы видели в операциях, и добавляет его в буфер.

Для нашего следующего изменения, давайте просто попробуем тот же шаблон, но на этот раз без экранирования результатов (для этого добавьте еще одну фигурную скобку "{{{name}}}" )

Обновляя страницу, теперь вы увидите, что она удалила операцию для экранирования переменной, и вместо этого она просто добавляет ее, что превращается в функцию, которая теперь просто проверяет, что значение не является ложным значением (кроме 0), а затем добавляет, не убегая.

Одиночный заполнитель без побега

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


Нет смысла делать это более сложным, чем нужно, давайте просто создадим простую функцию, которая будет возвращать дубликат переданного числа, поэтому замените шаблон и добавьте новый блок сценария для помощника (перед другим кодом). ):

1
2
3
4
5
6
7
8
9
<script id=»dt» type=»template/handlebars»>
    3 * 2 = {{{doubled 3}}}
</script>
 
<script>
    Handlebars.registerHelper(«doubled», function(number){
        return number * 2;
    });
</script>

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

Вспомогательная функция

Здесь вы можете видеть, что он знает, что это помощник, поэтому вместо того, чтобы говорить «invokeAmbiguous», теперь он говорит «invokeHelper» и, следовательно, также в функции больше нет блока if / else. Тем не менее, он по-прежнему удостоверяется, что помощник существует, и пытается вернуться к контексту для функции с тем же именем, если это не так.

Еще одна вещь, о которой стоит упомянуть, это то, что вы можете видеть, как параметры для помощников передаются напрямую и фактически жестко закодированы, если это возможно, когда генерируется функция get (число 3 в удвоенной функции).

Последний пример, который я хочу охватить, — о помощниках блоков.


Помощники по блокам позволяют вам оборачивать другие токены внутри функции, которая может устанавливать свой собственный контекст и параметры. Давайте посмотрим на пример с использованием стандартного помощника блока if:

1
2
3
4
5
6
7
8
<script id=»dt» type=»template/handlebars»>
    Hello
    {{#if name}}
        {{{name}}}
    {{else}}
        World!
    {{/if}}
</script>

Здесь мы проверяем, установлено ли «имя» в текущем контексте, и в этом случае мы будем отображать его, в противном случае мы выводим «Мир!». Запустив это в нашем анализаторе, вы увидите только два токена, хотя их больше; это связано с тем, что каждый блок запускается как собственный «шаблон», поэтому все токены внутри него (например, {{{name}}} ) не будут частью внешнего вызова, и вам потребуется извлечь его из самого узла блока ,

Кроме того, если вы посмотрите на функцию:

Блок Хелпер

Вы можете видеть, что он фактически компилирует функции помощника блока в функцию шаблона. Их два, потому что одна — главная функция, а другая — обратная функция (когда параметр не существует или имеет значение false). Основная функция: «program1» — это именно то, что было у нас раньше, когда у нас был только текст и один заполнитель, потому что, как я упоминал, каждая из вспомогательных функций блока создается и обрабатывается точно так же, как обычный шаблон. Затем они запускаются через помощник «if», чтобы получить правильную функцию, которую он затем добавит во внешний буфер.

Как и прежде, стоит упомянуть, что первым параметром для помощника блока является сам ключ, тогда как параметр ‘this’ устанавливается для всего передаваемого в контексте, что может пригодиться при построении ваших собственных помощников блоков.


В этой статье мы, возможно, не взяли практический взгляд на то, как добиться чего-то в Handlebars, но я надеюсь, что вы лучше поняли, что именно происходит за кулисами, что должно позволить вам создавать лучшие шаблоны и помощников с этим новым найденным знание.

Надеюсь, вам понравилось читать, как всегда, если у вас есть какие-либо вопросы, не стесняйтесь обращаться ко мне в Twitter ( @GabrielManricks ) или в Nettuts + IRC (#nettuts on freenode).