Данная статья содержит в себе описание основных возможностей шаблонизатора Handlebars и является свободным переводом его официальной документации. Итак, поехали…



С помощью Handlebars вы сможете построить эффективные семантические шаблоны. Шаблоны Handlebars во многом совместимы с шаблонами Mustache — в большинстве случаев они взаимозаменяемы. Полные сведения можно найти по этой ссылке.

Выражения


Для начала работы с Handlebars необходимо подключить файл handlebars.js:

<script src="js/handlebars.js"></script>

Шаблоны Handlebars выглядят как обычный HTML, со встроенными handlebars-выражениями.

Самое простое handlebars-выражение представляет собой простой идентификатор:

{{some_contents}}

Идентификатором может быть любой символ Unicode за исключением следующих:

Пробел ! " # % & ' ( ) * + , . / ; < = > @ [ \ ] ^ ` { | } ~

Вы можете вставить шаблон Handlebars в ваш HTML-код, включив его в тэг <script />:

<script id="entry-template" type="text/x-handlebars-template">
  <div class="entry">
    <h1>{{title}}</h1>
  </div>
</script>

В JavaScript шаблон компилируется с помощью Handlebars.compile:

var source   = $("#entry-template").html();
var template = Handlebars.compile(source);

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

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

var context = { title: "Собаке Качалова" };
var html    = template(context);

Мы получим HTML-код:

<div class="entry">
  <h1>Собаке Качалова</h1>
</div>

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

<div class="entry">
  <h1>{{title}}</h1>
  <h2>{{author.name}}</h2>
</div>

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

Этот шаблон будет работать со следующим контекстом:

var context = {
  title: "Собаке Качалова",
  author: {
    id: 47,
    name: "Сергей Есенин"
  }
};

Пути Handlebars также могут включать ../ сегменты, позволяющие указать путь относительно родительского контекста. С ними мы столкнемся, когда будем рассматривать блоковые хелперы.

Handlebars экранирует выражения, помещенные в двойные скобки {{ }}. Чтобы избежать этого, необходимо поместить выражение в тройные скобки {{{ }}}. Чтобы в будущем при написании генерирующего HTML-код хэлпера избежать экранирования возвращаемого им результата, мы будем возвращать new Handlebars.SafeString(result)

Хелперы


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

{{{link "Подробнее..." poem.url}}}

Handlebars-хелперы могут быть доступны в шаблоне из любого контекста.

<div>
  <h1>{{poem.title}}</h1>
  {{link "Подробнее..." poem.url}}
</div>

Вы можете зарегистрировать хелпер с помощью метода Handlebars.registerHelper. При использовании следующего контекста и хелпера:

var context = {
  poem: {
    title: "Собаке Качалова",
    url: "Sobake_Kachalova.html"
  }
};
Handlebars.registerHelper('link', function(text, url) {
  url = Handlebars.escapeExpression(url);      //экранирование выражения
  text = Handlebars.escapeExpression(text);
  return new Handlebars.SafeString("<a href='" + url + "'>" + text + "</a>");
});

Получим следующий результат:

<div>
  <h1>Собаке Качалова</h1>
  <a href='Sobake_Kachalova.html'>Подробнее...</a>
</div>

Handlebars-хелперам также можно передать последовательность пар ключ-значение после всех непарных параметров. Ключи должны быть простыми идентификаторами, а значения — handlebars-выражениями (т.е. значения могут быть идентификаторами, путями, или строками).

{{link "Подробнее..." href=poem.url class="poem"}}

Регистрация хелпера:

Handlebars.registerHelper('link', function(text, options) {
  console.log(options.hash['href']);   //значение, лежащее в poem.url
  console.log(options.hash['class']); //"poem"
  return new Handlebars.SafeString("<a href=\"" + options.hash['href'] + "\">" + Handlebars.escapeExpression(text) + "</a>");
});

Потенциально может возникнуть конфликт имен между хелперами и полями данных (скажем, у нас используются в одном контексте handlebars-выражение {{name}} и хелпер {{name}}). Handlebars предлагает нам следующую возможность разрешения этого конфликта:

<p>{{./name}} or {{this/name}} or {{this.name}}</p>

Любое из вышеперечисленных handlebars-выражений вызовет поле данных {{name}} в текущем контексте, а не хелпер с тем же самым именем.

Блоковые хелперы


Блоковые хелперы позволяют определять пользовательские итераторы, а также другую функциональность, с помощью которой можно вызвать переданный блок в новом контексте. Блоковый хелпер записывается следующим образом:
{{#helper_name}}some_block{{/helper_name}}

При регистрации хелпера параметры в Handlebars.registerHelper передаются в том порядке, в котором они были переданы хелперу пользователем. Вслед за всеми указанными пользователем параметрами следует параметр options. Он имеет следующие свойства:

1) options.fn — содержит функцию, которая ведет себя как обычный скомпилированный шаблон Handlebars. В качестве параметра функция принимает контекст и возвращает строку.

Пример 1. Давайте определим блоковый хелпер, который изменяет разметку обернутого в него текста:

<div class="body">
  {{#bold}}{{body}}{{/bold}}
</div>

Регистрация:

Handlebars.registerHelper('bold', function(options) {
  return '<div class="mybold">' + options.fn(this) + '</div>';
});

Так как содержание блокового хелпера экранируется при вызове options.fn(context), Handlebars не экранирует результат, возвращаемый блоковым хелпером. Если это сделать, то внутреннее содержание будет экранировано дважды!

Здесь в качестве параметра функции мы передали текущий контекст (this). Давайте укажем другой контекст, передав его хелперу как параметр:

Пример 2.

<div class="entry">
{{author}}
{{#with poem}}
  <div class="title">{{title}}</div>
  <div class="author">{{../author}}</div>
  <div class="year">{{year}}</div>
{{/with}}
</div>

Выполнить этот шаблон мы могли бы со следующим JSON, имеющим вложенные в объект poem ключи title и year:

{
  author: "Сергей Есенин",
  poem: {
    title: "Собаке Качалова",
    year: 1925
  }
}

Регистрация:

Handlebars.registerHelper('with', function(context, options) {
  console.log(context);   //{title: "Собаке Качалова", year: 1925}
  return options.fn(context);
});

Мы получим HTML-код:

<div class="entry">
  Сергей Есенин
  <div class="title">Собаке Качалова</div>
  <div class="author">Сергей Есенин</div>
  <div class="year">1925</div>
</div>

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

Пример 3. Давайте напишем итератор, создающий обертку <ul /> и вкладывающий в нее каждый передаваемый ему блок как элемент <li />.

{{#list nav}}
  <a href="{{url}}">{{title}}</a>
{{/list}}

Выполнить этот шаблон мы могли бы, используя следующий контекст:

{
  nav: [
    { url: "http://www.museum-esenin.ru/biography", title: "Биография Сергея Есенина" },
    { url: "http://www.fedordostoevsky.ru/biography", title: "Биография Ф. М. Достоевского" },
  ]
}

Регистрация хелпера:

Handlebars.registerHelper('list', function(context, options) {
  var ret = "<ul>";

  for(var i=0, j=context.length; i<j; i++) {
    ret = ret + "<li>" + options.fn(context[i]) + "</li>";
  }

  return ret + "</ul>";
});

Мы получим HTML-код:

<ul>
  <li><a href="http://www.museum-esenin.ru/biography">Биография Сергея Есенина</a></li>
  <li><a href="http://www.fedordostoevsky.ru/biography">Биография Ф. М. Достоевского</a></li>
</ul>

2) options.inverse — функция используется при работе с управляющими структурами Handlebars. Управляющие структуры обычно не изменяют текущий контекст — вместо этого они решают, вызвать ли блок, основываясь на значении переданного параметра.

{{#if isActive}}
  <img src="minus.gif" alt="Active">
{{else}}
  <img src="plus.gif" alt="Inactive">
{{/if}}

Handlebars возвращает блок, следующий за {{else}}, через функцию options.inverse:

Handlebars.registerHelper('if', function(conditional, options) {
  if(conditional) {
    return options.fn(this);
  } else {
    return options.inverse(this);
  }
});

3) options.hash — содержит последовательность пар ключ-значение, передаваемых хелперу после всех непарных параметров.

Пример. Давайте вернемся к хелперу list и сделаем возможным добавление любого количества атрибутов создаваемой нами обертки <ul />.

{{#list nav id="nav-bar" class="top"}}
  <a href="{{url}}">{{title}}</a>
{{/list}}

Handlebars передает парные параметры в options.hash, где они хранятся как ключи-значения объекта.

Регистрация хелпера:

Handlebars.registerHelper('list', function(context, options) {
  var attrs = [];
  for (key in options.hash) {
    attrs.push(key + '="' + options.hash[key] + '"');
  }
  return "<ul " + attrs.join(" ") + ">" + context.map(function(item) {
    return "<li>" + options.fn(item) + "</li>";
  }).join("\n") + "</ul>";
});

Мы получим HTML-код:

<ul class="top" id="nav-bar">
  <li><a href="http://www.museum-esenin.ru/biography">Биография Сергея Есенина</a></li>
  <li><a href="http://www.fedordostoevsky.ru/biography">Биография Ф. М. Достоевского</a></li>
</ul>

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

Встроенные хелперы


1) if — используйте этот хелпер для вывода блока по условию.

<div class="entry">
  {{#if author}}
    <h1>{{firstName}} {{lastName}}</h1>
  {{else}}
    <h1>Автор неизвестен</h1>
  {{/if}}
</div>

2) unless — используйте этот хелпер как обратный хелперу if. Блок будет выведен, если выражение вернет ложное значение.

<div class="entry">
  {{#unless license}}
  <h3 class="warning">ВНИМАНИЕ: Эта запись не имеет лицензии!</h3>
  {{/unless}}
</div>

3) each — используйте этот хелпер для перебора списков. Внутри блока Вы можете использовать this для ссылки на элемент списка.

<ul class="writers_list">
  {{#each writers}}
    <li>{{@index}}: {{this}}</li>
  {{else}}
    <li>Список пуст</li>
  {{/each}}
</ul>

Этот шаблон может быть использован в следующем контексте:

{
  writers: [
    "А. С. Пушкин",
    "Ф. М. Достоевский",
    "Сергей Есенин"
  ]
}

В результате получим:

<ul class="writers_list">
  <li>0: А. С. Пушкин</li>
  <li>1: Ф. М. Достоевский</li>
  <li>2: Сергей Есенин</li>
</ul>

Блок, следующий за секцией {{else}}, выводится только когда список пуст.

4) with — используйте этот хелпер для сдвига контекста секции handlebars-шаблона.

<div class="entry">
  <h1>{{title}}</h1>
  {{#with author}}
    <h2>{{firstName}} {{lastName}}</h2>
  {{else}}
    <h2>Автор неизвестен</h2>
  {{/with}}
</div>

Этот шаблон может быть использован в следующем контексте:

{
  title: "Собаке Качалова",
  author: {
    firstName: "Сергей",
    lastName: "Есенин"
  }
}

5) log — позволяет логировать состояние контекста во время выполнения шаблона.

{{log "Фамилия: " lastName "; Имя: " firstName}}

Делегируется в Handlebars.logger.log, который может быть переопределен для выполнения пользовательского логирования.
Более подробная информация о встроенных хелперах с примерами находится по этой ссылке.

Повторное использование шаблонов (Partials)


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

Чтобы использовать Partial, его необходимо сначала зарегистрировать через Handlebars.registerPartial:

Handlebars.registerPartial('myPartial', '{{name}}')

Partial может быть предварительно скомпилирован — в таком случае предварительно скомпилированный шаблон передается в качестве второго параметра.

Вызов Partial:

{{> myPartial }}

Partial выполняется в текущем контексте. Имеется также возможность выполнить Partial в пользовательском контексте, передавая контекст при вызове Partial:

{{> myPartial myOtherContext }}

Более подробные сведения о Partials можно получить, перейдя по этой ссылке.

Комментарии (28)


  1. xamd
    21.12.2015 13:44
    +1

    О, мы использовали handlebars в 2011. Но, честно говоря, я думаю что целевая аудитория этой статьи крайне ограничена людьми, которым приходится поддерживать legacy- код на backbone или чем-то подобном. Все современные решения (а-ля angular, react) предоставляют свои инструменты для шаблонизации HTML.

    Тем не менее, материал качественный и красиво оформленный. А это, нынче, редкость.


    1. DenimTornado
      21.12.2015 15:20
      +4

      Ну почему же? Для небольших приложений и виджетов очень удобно использовать handlebars, зачем тянуть монстра Angular, когда можно обойтись vanilla + шаблонизация?


      1. lega
        21.12.2015 22:36
        +2

        Но весит больше чем Angular Light.


    1. Mithgol
      21.12.2015 18:48
      +4

      Angular и React — это целокупные фреймворки, а Handlebars — всего лишь библиотека шаблонизации. У ней меньше идеологии, требующей принятия. Тем она полезнее.

      На основную же тему блогозаписи я хочу прибавить свeдение о том, что существует хороший модуль express-handlebars для употребления Handlebars на стороне сервера в тех случаях, когда сервер этот — Express.js.


    1. DSL88
      21.12.2015 22:55

      Ember.js


    1. evil_random
      22.12.2015 10:37

      Legacy-код. На Backbone. Ну-ну.


  1. RubaXa
    21.12.2015 13:46

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


    1. DenimTornado
      21.12.2015 15:28

      Так шаблон вы сами собираете, а значит сами делаете невалидный html или я не так понял ваш коммент?


      1. RubaXa
        21.12.2015 15:53

        Ну если так рассуждать, то, да, но и инструмент позволил мне это сделать. От шаблонизатора html я ожидаю, что он сообщит мне об ошибках в верстке, например когда «я» неправильно закрыл {{# if}} (уровнем выше или ниже), но он будет молчать, а отлавливать это трудно.


        1. dubr
          22.12.2015 17:00
          +1

          Кстати, когда-то давно мы в конторе много писали на XSLT, и мне неоднократно приходилось обучать юных падаванов его основам. Так вот, ВСЕХ очень напрягал именно тот факт, что шаблон генерирует на выход дерево узлов (и, соответственно, ругается на незакрытые теги), а не последовательность букв. То есть, ожидали ровно обратного.

          Правда, в те времена балом правил Smarty :)


  1. lazant
    21.12.2015 16:00

    Я использую jsRender не помню на какие, но с ходу натолкнулся на ограничение в Handlebars которые меня не устроили.


    1. lazant
      21.12.2015 16:14
      +1

      1. lazant
        21.12.2015 16:16

        www.jsviews.com/#iftag вот так это решается в jsRender


      1. DSL88
        21.12.2015 22:58

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


        1. lazant
          22.12.2015 09:44
          +1

          Особенно красиво выглядит вынесение таких вещей как проверка длины массива. Идея мне понятна, но доводить её до абсурда не нужно.


          1. DSL88
            22.12.2015 10:31

            Мне кажется, в любом случае, есть решение без реализации такой конструкции


            1. lazant
              22.12.2015 10:45
              +1

              Есть модель

              {name:'someName',sports:['capoeira','football']}
              

              Отображать нужно только если sports.length>1

              С хелпером понятно, какие ещё варианты с handlebars? Мне не очень понятно зачем так урезали функционал, в угоду чего?


              1. artemmalko
                22.12.2015 11:03

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


                1. lazant
                  22.12.2015 11:07
                  +1

                  Я понимаю идею, но эта урезанность функций view увеличивает bolerplate code. Причем если подумать, то чья эта сфера ответсвенности как не view?


                  1. artemmalko
                    22.12.2015 11:12

                    Не понимаю, за что минус…
                    Это сфера viewModel, model и т.д. как бы это еще не назвалось.


                    1. lazant
                      22.12.2015 11:15
                      +2

                      Минус в том чтобы написать вместо чего-то типа {{if sports.length>1}} какую-то неведомую {{ifLengthMoreThanOne}} её где-то определить и тем самым увеличить количества кода и уменьшить его читаемость. Я уж не говорю о том что увеличивается вероятность ошибок и т.д. во время кодирования.


                      1. artemmalko
                        22.12.2015 11:17

                        Вы не передавайте данные, если их не нужно рендерить. Разве это не удобнее, если вы только в 1 месте управляете данными, которые должны отрендериться?


                        1. lazant
                          22.12.2015 11:21

                          Это хорошая идея. Но я не об этом, я о том что могли бы оставить возможность писать условные выражения в «шаблоне», а уже пользователь выбрал бы как ему удобнее. По сути это первое с чем я столкнулся и что сразу же решило буду ли я пользоваться handlebars или нет.


                          1. DSL88
                            22.12.2015 14:58

                            isHaveSportsItems компьютед свойство и все.
                            И ваша верстка будет более читабельна, чем с условиями.

                            Если вам уж совсем нужно чтобы определять по длине массива, то

                            (arr.length === 0) === false
                            


                            а

                            (arr.length>0) === true
                            


                            И как следствие

                            {{#if sports.length}}
                              your code here
                            {{/if}}
                            


                            1. lazant
                              22.12.2015 15:02

                              Это называется костыль :). Модель не должна знать как её отображают.

                              Насчет читаемости ifLengthMoreThanOne по сравнению с sports.length>1 можно поспорить, но при сложных условиях, согласен, первый тип записи был бы более читаемым.


                            1. Mithgol
                              22.12.2015 22:09

                              Всецело поддерживаю мнение о том, что в шаблонах для Handlebars.js следует записывать условие в формате «{{#if sports.length}}» (в значении «если длина массива sports — не нулевая») или «{{#unless sports.length}}» (в значении «если длина массива sports — нулевая»).

                              Прибавлю, что иногда (в зависимости от модели) может пригодиться вокруг всего соответствующего куска шаблона дополнительная обёртка — оператор «{{#if sports}}» (на тот случай, если массив sports вовсе не был задан в модели).

                              Иногда же (в более простом частном случае) используется цикл «{{#each sports}}» по элементам массива, и только наличие общего заголовка их (или общего хвоста) оправдывает условный оператор.


                              1. DSL88
                                22.12.2015 22:39
                                +1

                                each поддерживает else, который срабатывает когда элементов 0 — об этом мало кто знает/помнит


                              1. justboris
                                30.12.2015 10:46

                                Там выше был вопрос что делать с {{#if sports.length > 1}}. Это так просто не решить.
                                В модели добавлять дополнительный флаг не осень хорошо, потому что, во-первых, это будет дублированием информации, а во-вторых может и на сервер случайно отослаться