Handlebars.js - взгляд за кулисы
() translation by (you can also view the original English article)
Handlebars завоевывает популярность благодаря его внедрению в таких системах, как Meteor и Ember.js, но что действительно происходит за кулисами этого захватывающего шаблонного движка?
В этой статье мы рассмотрим базовый процесс, который обрабатывает Handlebars для компиляции ваших шаблонов.
В этой статье предполагается, что вы прочитали мое предыдущее введение в Handlebars, и поэтому предполагается, что вы знаете основы создания шаблонов Handlebar.
При использовании шаблона Handlebars вы, вероятно, знаете, что начинаете с компиляции источника шаблона в функцию с помощью Handlebars.compile()
, а затем используете эту функцию для генерации окончательного HTML, передавая значения для свойств и заполнителей.
Но эта, казалось бы, простая функция компиляции на самом деле за кулисами делает несколько шагов, и именно об этом мы поговорим в этой статье; давайте взглянем на быструю разбивку процесса:
- Обозначить токенами источник в компонентах.
- Обработать каждый токен в наборе операций.
- Преобразовать стек процесса в функцию.
- Запустить функцию с контекстом и помощниками для вывода некоторого HTML.
Настройка
В этой статье мы будем создавать инструмент для анализа шаблонов Handlebars на каждом из этих этапов, поэтому для отображения результатов на экране, я буду использовать подсветку синтаксиса prism.js, созданную Lea Verou. Загрузите минимифицированный источник.
Следующий шаг - создать пустой HTML-файл и заполнить его следующим:
1 |
<!DOCTYPE HTML>
|
2 |
<html xmlns="http://www.w3.org/1999/html"> |
3 |
<head>
|
4 |
<title>Handlebars.js</title> |
5 |
<link rel="stylesheet" href="prism.css"></p> |
6 |
|
7 |
<script src="prism.js" data-manual></script> |
8 |
<script src="handlebars.js"></script> |
9 |
</head>
|
10 |
<body>
|
11 |
<div id="analysis"> |
12 |
<div id="tokens"><h1>Tokens:</h1></div> |
13 |
<div id="operations"><h1>Operations:</h1></div> |
14 |
<div id="output"><h1>Output:</h1></div> |
15 |
<div id="function"> |
16 |
<h1>Function:</h1> |
17 |
<pre><code class="language-javascript" id="source"></code></pre> |
18 |
</div>
|
19 |
</div>
|
20 |
<script id="dt" type="template/handlebars"> |
21 |
</script>
|
22 |
|
23 |
<script>
|
24 |
//Code will go here
|
25 |
</script>
|
26 |
</body>
|
27 |
</html>
|
Это всего лишь некоторый шаблонный код, который включает в себя handlebars и prism, а затем устанавливает некоторые div для разных шагов. Внизу вы можете увидеть два блока сценариев: первый для шаблона, второй - для нашего JS-кода.
Я также добавил немного CSS, чтобы все выглядело немного лучше:
1 |
|
2 |
body{ |
3 |
margin: 0; |
4 |
padding: 0; |
5 |
font-family: "opensans", Arial, sans-serif; |
6 |
background: #F5F2F0; |
7 |
font-size: 13px; |
8 |
}
|
9 |
#analysis { |
10 |
top: 0; |
11 |
left: 0; |
12 |
position: absolute; |
13 |
width: 100%; |
14 |
height: 100%; |
15 |
margin: 0; |
16 |
padding: 0; |
17 |
}
|
18 |
#analysis div { |
19 |
width: 33.33%; |
20 |
height: 50%; |
21 |
float: left; |
22 |
padding: 10px 20px; |
23 |
box-sizing: border-box; |
24 |
overflow: auto; |
25 |
}
|
26 |
#function { |
27 |
width: 100% !important; |
28 |
}
|
Далее нам нужен шаблон, так что давайте начнем с самого простого шаблона, просто статического текста:
1 |
<script id="dt" type="template/handlebars"> |
2 |
Hello World! |
3 |
</script>
|
4 |
|
5 |
<script>
|
6 |
var src = document.getElementById("dt").innerHTML.trim(); |
7 |
|
8 |
//Display Output
|
9 |
var t = Handlebars.compile(src); |
10 |
document.getElementById("output").innerHTML += t(); |
11 |
</script>
|
Открытие этой страницы в вашем браузере должно привести к тому, что шаблон будет отображаться в окне вывода, как ожидалось, ничего другого, теперь нам нужно написать код для анализа процесса на каждом из трех других этапов.



Токены
Первыми шагами, которые выполняет handlebars, является токенизация источника, что означает, что нам нужно разбить источник на отдельные компоненты, чтобы мы могли обрабатывать каждую часть соответственно. Так, например, если бы был какой-то текст с заполнителем посередине, то Handlebars разделил бы текст до placeholder и разместил его в один токен, тогда сам placeholder будет помещен в другой токен и, наконец, весь текст после заполнителя будет помещен в третий токен. Это связано с тем, что эти части должны сохранять порядок шаблона, но их также нужно обрабатывать по-разному.
Этот процесс выполняется с помощью функции Handlebars.parse()
, и то, что вы возвращаете, является объектом, который содержит все сегменты или «утверждения».
Чтобы лучше проиллюстрировать то, о чем я говорю, давайте создадим список абзацев для каждого токена:
1 |
|
2 |
//Display Tokens
|
3 |
var tokenizer = Handlebars.parse(src); |
4 |
var tokenStr = ""; |
5 |
for (var i in tokenizer.statements) { |
6 |
var token = tokenizer.statements[i]; |
7 |
tokenStr += "<p>" + (parseInt(i)+1) + ") "; |
8 |
switch (token.type) { |
9 |
case "content": |
10 |
tokenStr += "[string] - \"" + token.string + "\""; |
11 |
break; |
12 |
case "mustache": |
13 |
tokenStr += "[placeholder] - " + token.id.string; |
14 |
break; |
15 |
case "block": |
16 |
tokenStr += "[block] - " + token.mustache.id.string; |
17 |
}
|
18 |
}
|
19 |
document.getElementById("tokens").innerHTML += tokenStr; |
Поэтому мы начнем с запуска источника шаблонов в Handlebars.parse
, чтобы получить список токенов. Затем мы перебираем все отдельные компоненты и создаем набор человекочитаемых строк на основе типа сегмента. Обычный текст будет иметь тип «content», который мы можем просто вывести строкой, заключенной в кавычки, чтобы показать, чему она равна. У заполнителей будет тип «mustache», который мы можем отобразить вместе с их «id» (имя заполнителя). И последнее, но не менее важное: блок-помощники будут иметь тип «block», который затем мы также можем просто отобразить внутренние блоки «id» (имя блока).
Обновляя это сейчас в браузере, вы должны увидеть только один токен 'string', с текстом нашего шаблона.



Операции
Когда у handlebars есть набор токенов, он циклически проходит через каждый и «генерирует» список предопределенных операций, которые необходимо выполнить для скомпилированного шаблона. Этот процесс выполняется с использованием объекта Handlebars.Compiler()
, передающего объект токена с шага 1:
1 |
|
2 |
//Display Operations
|
3 |
var opSequence = new Handlebars.Compiler().compile(tokenizer, {}); |
4 |
var opStr = ""; |
5 |
for (var i in opSequence.opcodes) { |
6 |
var op = opSequence.opcodes[i]; |
7 |
opStr += "<p>" + (parseInt(i)+1) + ") - " + op.opcode; |
8 |
}
|
9 |
document.getElementById("operations").innerHTML += opStr; |
Здесь мы компилируем токены в последовательность операций, о которой я говорил, а затем мы циклически перемещаемся по каждому из них и создаем аналогичный список, как на первом шаге, но здесь нам просто нужно распечатать код операции. Код операции - это «операция» или «имя» функции, которая должна выполняться для каждого элемента в последовательности.
В браузере теперь вы должны увидеть только одну операцию под названием «appendContent», которая добавит значение в текущий «буфер» или «строку текста». Есть много разных кодов операций, и я не думаю, что я могу объяснить некоторые из них, но быстрый поиск в исходном коде для данного кода операции покажет вам функцию, которая будет для него запущена.



Функция
Последний этап - взять список кодов операций и преобразовать их в функцию, он делает это, читая список операций и умело конкатенируя код для каждого из них. Вот код, необходимый для выполнения функции для этого шага:
1 |
|
2 |
//Display Function
|
3 |
var outputFunction = new Handlebars.JavaScriptCompiler().compile(opSequence, {}, undefined, true); |
4 |
document.getElementById("source").innerHTML = outputFunction.toString(); |
5 |
Prism.highlightAll(); |
Первая строка создает компилятор, проходящий в последовательности op, и эта строка возвращает конечную функцию, используемую для генерации шаблона. Затем мы преобразуем функцию в строку и расскажем prism о синтаксисе.
С помощью этого окончательного кода ваша страница должна выглядеть примерно так:



Эта функция невероятно проста, так как была только одна операция, она просто возвращает заданную строку; давайте теперь взглянем на редактирование шаблона и посмотрим, как эти индивидуальные шаги объединяются вместе, чтобы сформировать очень мощную абстракцию.
Изучение шаблонов
Начнем с чего-то простого, и давайте просто заменим слово «Мир» на местозаполнитель; ваш новый шаблон должен выглядеть следующим образом:
1 |
<script id="dt" type="template/handlebars"> |
2 |
Hello {{name}}! |
3 |
</script>
|
И не забудьте передать переменную так, чтобы результат выглядел нормально:
1 |
//Display Output
|
2 |
var t = Handlebars.compile(src); |
3 |
document.getElementById("output").innerHTML += t({name: "Gabriel"}); |
Запустив это, вы обнаружите, что, добавив только один простой заполнитель, это совсем немного усложняет процесс.



Сложный раздел if/else состоит в том, что он не знает, является ли местозаполнитель фактически заполнителем или вспомогательным методом
Если вы все еще не уверены в том, что означают токены, у вас сейчас должна быть хорошая мысль; как вы можете видеть на картинке, он отделил местозаполнитель от строк и создал три отдельных компонента.
Далее, в разделе операций есть немало дополнений. Если вы помните ранее, чтобы просто вывести какой-то текст, Handlebars использует операцию «appendContent», что вы теперь можете увидеть в верхней и нижней части списка (как для «Hello», так и для «!»). Остальные в середине - все операции, необходимые для обработки заполнителя и добавления экранированного содержимого.
Наконец, в нижнем окне вместо того, чтобы просто возвращать строку, на этот раз она создает буферную переменную и обрабатывает один токен за раз. Сложный раздел if/else состоит в том, что он не знает, является ли местозаполнитель фактически заполнителем или вспомогательным методом. Поэтому он пытается увидеть, существует ли вспомогательный метод с заданным именем, и в этом случае он вызовет вспомогательный метод и установит значение «stack1». В случае, если это местозаполнитель, он присваивает значение из переданного контекста (здесь называется «depth0»), и если функция была передана в него, он поместит результат функции в переменную «stack1». Как только все это будет сделано, затем происходит экранирование, как мы видели в операциях, и добавляем его в буфер.
Для нашего следующего изменения давайте просто попробуем тот же шаблон, но на этот раз не экранируя результатов (для этого добавьте еще одну фигурную скобку "{{{name}}}
")
Обновляя страницу, теперь вы увидите, что она удалила операцию, чтобы избежать этой переменной, и вместо этого она просто добавляет ее, она превращается в функцию, которая теперь просто проверяет, чтобы значение не было ложным (кроме 0), а затем добавляет его, не экранируя.



Поэтому я думаю, что заполнители довольно прямолинейны, теперь давайте взглянем на использование вспомогательных функций.
Вспомогательные функции
Нет никакого смысла в том, чтобы сделать это более сложным, чем это должно быть, давайте просто создадим простую функцию, которая вернет дубликат числа, переданного в нее, поэтому замените шаблон и добавьте новый блок сценария для помощника (перед остальным кодом):
1 |
<script id="dt" type="template/handlebars"> |
2 |
3 * 2 = {{{doubled 3}}} |
3 |
</script>
|
4 |
|
5 |
<script>
|
6 |
Handlebars.registerHelper("doubled", function(number){ |
7 |
return number * 2; |
8 |
});
|
9 |
</script>
|
Я решил не избегать этого, так как это делает последнюю функцию немного более простой для чтения, но вы можете попробовать обе, если хотите. В любом случае, выполнение этого должно привести к следующему:



Здесь вы можете видеть, что движок знает, что это помощник, поэтому вместо того, чтобы говорить «invokeAmbiguous», теперь он говорит «invokeHelper», а потому и в функции больше нет блока if/else. Тем не менее он все же гарантирует, что помощник существует и пытается вернуться к контексту для функции с тем же именем в случае, если это не так.
Еще одна вещь, о которой стоит упомянуть, - это рассмотреть то, как параметры для помощников передаются напрямую и на самом деле жестко закодированы, если это возможно, когда генерируется функция get (число 3 в удвоенной функции).
Последний пример, который я хочу затронуть, - это блок-помощники.
Блок помощники
Блочные помощники позволяют обернуть другие токены внутри функции, которая может установить свой собственный контекст и параметры. Давайте посмотрим на пример с использованием вспомогательного блока «if» по умолчанию:
1 |
<script id="dt" type="template/handlebars"> |
2 |
Hello
|
3 |
{{#if name}} |
4 |
{{{name}}} |
5 |
{{else}} |
6 |
World! |
7 |
{{/if}} |
8 |
</script>
|
Здесь мы проверяем, установлено ли «имя» в текущем контексте, и в этом случае мы будем отображать его, иначе мы выводим «Мир!». Запустив это в нашем анализаторе, вы увидите только два токена, хотя их больше; это потому, что каждый блок запускается как свой собственный «шаблон», поэтому все токены внутри него (например, {{{name}}}
) не будут частью внешнего вызова, и вам нужно будет извлечь его из самого узла блока.
Кроме того, если вы посмотрите на функцию:



Вы можете видеть, что он фактически компилирует функции хелпера блока в функцию шаблона. Их две, потому что одна из них является основной, а другая - обратной функцией (если параметр не существует или является ложным). Основная функция: «program1» - это то, что было раньше, когда у нас был только текст и один заполнитель, потому что, как я уже упоминал, каждая из вспомогательных функций блока создается и обрабатывается точно как обычный шаблон. Затем они запускаются через помощник «if» для получения правильной функции, которую он затем добавляет к внешнему буферу.
Как и прежде, стоит упомянуть, что первым параметром для вспомогательного блока блока является сам ключ, тогда как параметр «this» задается всем переданным параметрам в контексте, что может пригодиться при создании собственных блочных помощников.
Вывод
В этой статье мы, возможно, не нашли практического взгляда на то, как добиться чего-то в Handlebars, но я надеюсь, что вы лучше понимаете, что именно происходит за кулисами, что должно позволить вам создавать лучшие шаблоны и помощники с этим новым знанием.
Надеюсь, вам понравилось читать, как всегда, если у вас есть какие-либо вопросы, не стесняйтесь обращаться ко мне в Twitter (@GabrielManricks) или в Nettuts + IRC (#nettuts on freenode).