Клиентские библиотеки

Использование JsRender с JavaScript и HTML

Джон Папа

John PapaНа многих платформах разработки используются шаблоны для сокращения объема кода и упрощения сопровождения, так что HTML5 и JavaScript здесь не являются исключением. JsRender — это библиотека JavaScript, позволяющая один раз определить стереотипную структуру, а затем многократно использовать ее для динамической генерации HTML. JsRender — новая библиотека шаблонов для разработки под HTML5 с теговым синтаксисом и высокой производительностью, она не имеет зависимости ни от jQuery, ни от Document Object Model (DOM), поддерживает создание собственных функций и использует рендеринг исключительно на основе строк. В этой статье рассматриваются сценарии, в которых применение JsRender идеально, и демонстрируется, как использовать ее возможности. Все примеры кода можно скачать с сайта MSDN Magazine, а JsRender — по ссылке bit.ly/ywSoNu.

Согласно блогу Бориса Мура (Boris Moore) и группы jQuery UI, в апреле 2011 г. группа решила приостановить работу над шаблонами jQuery и отдать предпочтение созданию библиотеки рендеринга на основе строк, которая не содержит никакой логики. Это привело Мура (одного из наиболее активных разработчиков шаблонов jQuery) к созданию JsRender и JsViews. Эти две библиотеки являются, по сути, заменой шаблонам jQuery. Подробнее об истории создании шаблонов и JsRender читайте в статье Мура (bit.ly/AdKeDk) и статье группы jQuery (bit.ly/fhnk8A). В течение следующих нескольких месяцев мы будем исследовать JsRender и JsViews.

Почему шаблоны?

Использование шаблонов с JavaScript сокращает и упрощает код. Без шаблонов добавление серии элементов списка и других HTML-элементов для набора данных потребовало бы манипуляций с DOM браузера. Здесь-то шаблоны с применением плагина вроде JsRender могут оказаться весьма полезными для выполнения всей тяжелой рутинной работы. Допустим, к примеру, что вы получаете набор названий фильмов и хотите показать их список. Вы могли бы написать код на JavaScript и манипулировать DOM, но следующий код иллюстрирует, что и такая простая задача может стать трудной даже с применением jQuery:

// Без шаблона
var i = 1;
$(my.vm.movies).each(function () {
  var movie = this;
  $("#movieContainer1").append(
    "<div>" + i++ + ": " + movie.name + " ("
    + movie.releaseYear + ")</div>");
});

Этот код полностью написан на JavaScript и jQuery, но отличить HTML от данных из JavaScript может быть весьма трудно. Используя шаблон, мы можем легко выделить структуру и исключить большую часть кода на JavaScript. Следующий шаблон (файл 01-with-and-without-templates-with-jquery.html в пакете исходного кода для этой статьи) демонстрирует эту концепцию:

<script id="myMovieTemplate" type="text/x-jsrender ">
  <div>{{:#index+1}}: {{:name}} ({{:releaseYear}})</div>
</script>

Заметьте, что шаблон обернут в тег script, ему присвоен соответствующий тип и он получает свой id, по которому его можно будет впоследствии идентифицировать. Рендеринг шаблона требует трех элементов: шаблона, данных и контейнера. Шаблон определяет, как будет осуществляться рендеринг данных, а контейнер — где будет выполняться этот рендеринг. Следующий код показывает, как выполнить рендеринг списка фильмов, используя шаблон myMovieTemplate, в элементе с идентификатором movieContainer:

$("#movieContainer").html($("#myMovieTemplate").render(my.vm.movies));

В этом примере jQuery используется для упрощения синтаксиса. Важно отметить, что JsRender не зависит от jQuery. Код, в котором для рендеринга данных применяется JsRender с использованием шаблона, мог бы быть написан и так, как показано ниже (файл 02-jsrender-no-jquery.html в пакете скачиваемого кода):

my.vm = {
  movies: my.getMovies()
};
jsviews.templates({
  movieTemplate: document.getElementById("myMovieTemplate").innerHTML
});
document.getElementById("movieContainerNojQuery").innerHTML
  = jsviews.render.movieTemplate(my.vm.movies);

Рендеринг шаблонов

Выполнять рендеринг шаблонов с помощью JavaScript можно несколькими способами. Сначала вам понадобится определить свой шаблон либо как строку, либо в теге <script>. Вариант с тегом <script> удобен, когда нужно определять свои шаблоны в HTML, присваивать им id и многократно использовать их. Вы также можете создавать шаблоны из строк, что позволяет формировать шаблоны «на лету» в коде или даже извлекать их из хранилища данных.

Для рендеринга HTML-контента из данных с использованием шаблона применяется метод render. Рендеринг набора данных можно выполнить с помощью шаблона, объявленного в теге <script> по синтаксису $(“#myTmpl”).render(data). Например, вы могли бы осуществить рендеринг списка фильмов с применением шаблона, используя следующий код:

// #1: рендеринг данных my.vm data
var htmlString = $("#scriptTmpl").render(my.vm);
// Вставляем htmlString в DOM
$("#div1").html(htmlString);

Вы также можете скомпилировать шаблон из строки вызовом функции $.templates(tmplString) и присвоить его какой-либо переменной. Затем рендеринг скомпилированного шаблона выполняется так:

// #2: Компиляция шаблона из строки;
// возвращается скомпилированный шаблон
var tmpl2 = $.templates(tmplString);
htmlString = tmpl2.render(my.vm);
$("#div2").html(htmlString);

Кроме того, можно скомпилировать шаблон из строки по синтаксису $.templates(name, template):

// #3: Шаблон компилируется, получает имя и регистрируется
$.templates("myTmpl3", tmplString);
var htmlString = $.render.myTmpl3(my.vm);
$("#div3").html(htmlString);

В этом примере функция $.templates компилирует шаблон из строки tmplString и регистрирует его как именованный шаблон. После этого к шаблону можно обращаться по имени и выполнять его рендеринг с применением синтаксиса $.render.name().

Функция $.templates аналогична таким jQuery-методам, как .css или .attrib, в том плане, что она предоставляет альтернативный синтаксис для регистрации и компиляции нескольких шаблонов за один вызов. Вместо передачи двух параметров (name и templateString) можно передать всего один параметр, который содержит объект-сопоставление (mapping object) пар «ключ-значение» для каждого шаблона, который должен быть зарегистрирован:

// #4: Компиляция нескольких шаблонов,
// их регистрация и рендеринг
var tmplString2 = "<div>*** {{:movies.length}} Total Movies ***</div>";
$.templates({
  tmpl4a: tmplString,
  tmpl4b: tmplString2
});
htmlString = $.render.tmpl4a(my.vm);
htmlString += $.render.tmpl4b(my.vm);
$("#div4").html(htmlString);

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

У вас много вариантов создания, регистрации и рендеринга шаблонов. Определение шаблонов в тегах script — распространенный подход в большинстве сценариев. Однако создание шаблонов из строк обеспечивает высокую гибкость. Предыдущий пример с расширенным синтаксисом предлагает еще большую гибкость в сопоставлении другого функционала с именованными шаблонами (например, объявление вспомогательных функций, специфичных для шаблона). Все эти примеры вы найдете в файле 03-rendering-templates.html в пакете скачиваемого кода.

Основы JsRender

Шаблоны JsRender состоят из HTML-разметки и тегов JsRender, например тега {{for …}} или {{: …}}. В табл. 1 показан синтаксис для самых основных тегов JsRender: {{: }} и {{> }}. Все теги шаблонов JsRender заключаются в двойные фигурные скобки. За именем тега (в данном случае символом «:» или «>») может следовать один или более параметров или выражений. (В случае тега {{: }} будет выполняться рендеринг результата выражения.) Как только шаблон определен и в него помещены данные, подлежащие рендерингу, вы можете выполнять рендеринг этого шаблона.

Табл. 1. Базовый синтаксис JsRender

Описание Пример Вывод
Значение свойства firstName элемента данных без кодировки {{:firstName}} Madelyn
Простой объектный путь к вложенному свойству без кодировки {{:movie.releaseYear}} 1987
Простое сравнение {{:movie.releaseYear < 2000}} true
Значение без кодировки {{:movie.name}} Star Wars IV: Return of the Jedi
HTML-кодированное значение {{>movie.name}} Star Wars: Episode VI: <span style='color:purple;font-style: italic;'>Return of the Jedi</span>
HTML-кодированное значение {{html:movie.name}} Star Wars: Episode VI: <span style='color:purple;font-style: italic;'>Return of the Jedi</span>

Следующий код содержит HTML-элемент с именем movie¬Container (рендеринг шаблона будет выполняться в нем):

<div id="movieContainer" class="resultsPanel movieListItemMedium"></div>
<script id="movieTemplate" type="text/x-jsrender">
  <div class="caption">{{:name}}</div>
  <div class="caption">{{>name}}</div>
  <img src="{{:boxArt.smallUrl}}"/>
  <div class="text">Year Released: {{:releaseYear}}</div>
  <div class="text">Rating: {{:rating}}</div>
</script>

Предыдущий код также содержит шаблон movie¬Template, который определяет div для отображения названия фильма без HTML-кодировки по синтаксису {{:name}}. Название фильма в примерах данных может содержать HTML-элементы, поэтому для рендеринга элементов, содержащих HTML, важно не использовать кодировку. Однако, если вам нужен рендеринг кодированного HTML, можно использовать синтаксис с символом > или HTML (как показано в табл. 1).

Значение свойства name содержит HTML-элементы, поэтому только в демонстрационных целях предыдущий код отображает значение этого свойства без кодировки ({{:name}}), а затем показывает HTML-кодированное значение ({{>name}}). Вы можете запустить полный пример 04-render-values.html в пакете скачиваемого кода.

Данные по фильмам, передаваемые шаблону, имеют свойство boxArt, которое в свою очередь имеет свойство smallUrl изображения. Тег img src устанавливается использованием иерархии объектного графа по синтаксису boxArt.smallUrl. Пути также можно проходить с помощью квадратных скобок, поэтому boxArt.smallUrl и boxArt['smallUrl'] дадут одинаковый результат.

Важно отметить, что синтаксис JsRender также пригоден для рендеринга других значений, например имен классов или идентификаторов HTML-элементов.

В процессе рендеринга шаблона JsRender распознает, является ли параметр данных массивом. Если это массив, возвращаемое значение представляет собой объединение строк, которое получилось бы передачей каждого элемента массива в метод render. Так что шаблон подвергается рендерингу только раз для каждого элемента данных, и результатом будет сцепленная строка.

Взгляните на код, который демонстрирует, как выполняется рендеринг одного объекта movie из массива в movieTemplate (полный исходный код доступен в примере 04-render-values.html):

my.vm = { movies: getMovies() };
$("#movieContainer").html($("#movieTemplate").render(my.vm.movies[1]));

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

Углубление в иерархические данные

Шаблоны часто используются для рендеринга серии элементов, которые зачастую содержат вложенные и иерархические данные (объектные графы). В табл. 2 показано, как JsRender может в цикле перебирать серии данных, используя тег {{for}}. Параметром для тега {{for}} может быт массив или серия массивов.

Табл. 2. Базовые средства итерации в цикле

Описание Пример Вывод
Перебор каждого элемента массива в цикле с использованием for

{{for cast}}

    <div>{{:stageName}}</div>

{{/for}}

Landon Papa

Ella Papa

Доступ к контексту данных через #data {{for phone}}<div>{{:#data}}</div>{{/for}}

555-555-1212

555-555-1212

В данных по фильмам определено, что каждый movie имеет массив звездочек рейтинга RatingStars. Это свойство содержит CSS-класс, отображающий звездочки рейтинга для фильма. Следующий код (из примера 05-for-data.html) демонстрирует, как перебрать в цикле каждый элемент в массиве RatingStars и выполнить рендеринг имени CSS-класса, используя синтаксис {{:#data}}:

<ul>
  {{for ratingStars}}
  <li class="rating {{:#data}}"/>
  {{/for}}
</ul>

Маркер #data — ключевое слово JsRender, которое представляет перебираемый объект. В данном случае массив RatingStars содержит строковый массив, поэтому #data будет представлять строку. Вывод для этого примера показан на рис. 1, где рядом с картинкой отображаются звездочки рейтинга фильма.

Rendering an Object with and Without Encoding
Рис. 1. Рендеринг объекта с кодировкой и без нее

Передача массивов в шаблоны

Когда массив передается шаблону, выполняется рендеринг этого шаблона по одному разу для каждого элемента в массиве. Рендеринг шаблона осуществляется по отношению к одному элементу данных, но, если в шаблон включен тег {{for}} с параметром-массивом, в разделе шаблона между открывающим и закрывающим тегами {{for}} и {{/for}} будет происходить дальнейшая итерация. Следующий код передаст объект my.vm, содержащий массив объектов movie, в функцию render, чтобы отобразить элемент списка для каждого фильма (полный исходный код см. в примере 06-if-else.html):

$("#movieList").html($("#movieTemplateMedium").render(my.vm));

В результате рендеринга шаблона будет получен элемент <ul> с id, равным movieList:

<ul id="movieList"></ul>

На рис. 2 показан шаблон movie TemplateMedium, который начинает с рендеринга <li>. Для каждого movie в массиве элемент <li> будет подвергаться рендерингу в теге <ul> контейнера.

Рис. 2. Рендеринг списка элементов и применение условных тегов

{{for movies}}
  <li class="movieListItemMedium">
  <div class="caption">{{:name}}</div>
  {{if boxArt.smallUrl}}
    <img src="{{:boxArt.smallUrl}}"/>
  {{else}}
    <img src="../images/icon-nocover.png"/>
  {{/if}}
  <br/>
  <div class="text">Year Released: {{:releaseYear}}</div>
  <div class="text">rating: {{:rating}}</div>
  <ul>
    {{for ratingStars}}
      <li class="rating {{:#data}}"/>
      {{/for}}
  </ul>
  <br/>
  <div class="text">${{:salePrice}}</div>
  {{if fullPrice !== salePrice}}
    <div class="text highlightText">PRICED TO SELL!</div>
  {{/if}}
  </li>
{{/for}}

Шаблон на рис. 2 принимает объект my.vm как контекст, а затем перебирает массив фильмов в цикле из-за наличия конструкции {{for movies}}. Если бы шаблон получал массив my.vm.movies вместо my.vm, блок {{for}} {{/for}} можно было бы удалить, так как тогда шаблон выполнялся бы для каждого элемента в массиве.

Прохождение путей

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

<img src="{{:#parent.parent.data[2].boxArt.smallUrl}}"/>

Контекст для этого шаблона — элемент в массиве my.vm.movies. Поэтому, когда вы дважды поднимаетесь в иерархии через #parent, контекстом становится объект my.vm. Затем вы можете получить movie с индексом 2 и использовать его свойство boxArt.smallUrl для изображения. В результате для каждого movie в массиве будет отображаться одна и та же картинка (третьего movie, как показано на рис. 3). Не слишком хороший результат в данной ситуации, но пример иллюстрирует, как проходить по иерархии объектов вверх и вниз.

Traversing Paths to Display the Same Movie Image for Every Movie
Рис. 5. Прохождение путей для отображения одной и той же картинки для всех фильмов

Условные теги

Синтаксис JsRender также поддерживает условные теги {{if}} и {{else}} (табл. 3). За тегом {{if}} может быть указан ноль, один или более тегов {{else}}, а затем замыкающий тег {{/if}}. Содержимое блока между тегами {{if}} и {{/if}} (или до первого тега {{else}}, если таковой есть) будет визуализироваться в выводе, только если значение выражения в {{if}} истинно.

Табл. 3. Условные теги, выражения и операторы

Описание Пример Вывод
Создание блока if, оцениваемого по результату сравнения

{{if fullPrice !== salePrice}}

    <div class="text highlightText">

    PRICED TO SELL!</div>

{{/if}}

PRICED TO SELL!
Блок if/else

{{if qtyInStock >= 10}}

    In Stock

{{else qtyInStock}}

    Only {{:qtyInStock}} left!

{{else}}

    Not available.

{{/if}}

Only 5 left!

Тег {{else}} может действовать как ifelse, когда он включает свое выражение. Обратите внимание на второй пример в табл. 3, который демонстрирует ветвление по условию с помощью двух тегов else. В этом коде последовательно оцениваются три условия.

Блок, следующий за тегом {{else someCondition}}, будет выводом, если условие будет оценено как true (или, точнее, truthy). Этот синтаксис можно фактически использовать как выражение switch/case для оценки n-ного количества условий.

Выражения, используемые в тегах JsRender, могут быть весьма сложными и включать пути, строки, числа, вызовы функций и такие операторы сравнения, как ===, !== и <=.

Использование счетчиков и сложных условных выражений

Иногда нужно отображать или использовать переменную-счетчик в шаблоне. Один из таких случаев — вам нужно показывать номер строки в шаблоне. Другой случай. Вы хотите присвоить уникальный идентификатор каждому элементу в шаблоне, чтобы потом можно было на него ссылаться. Например, вы перебираете список фильмов и обертываете их все в тег <div>. Вы могли бы присваивать идентификатор тега div так: movieDiv1, movieDiv2 и т. д. Затем искать элемент по его идентификатору, скажем, для связывания с обработчиком событий, используя jQuery-функцию delegate. В обоих случаях вы можете использовать ключевое слово #index в JsRender (его значения начинаются с 0). Как показано в табл. 4, отображать #index в шаблоне совсем не сложно.

Табл. 4. Счетчики и условные теги в нескольких выражениях

Описание Пример Вывод
Использование индекса для отображения счетчика в шаблоне

{{for movies}}

<div class="caption">

    {{:#index}}: {{:name}}

</div>

{{/for}}

3: The Princess Bride
Применение логических операторов

{{if stars.length || cast.length}}

    <div class="text">Full Cast:</div>

{{/if}}

Full Cast:

Зачастую единственного выражения не хватает. JsRender поддерживает несколько выражений при использовании логических операторов вроде || и && («или» и «и» соответственно). Это упрощает комбинирование нескольких выражений, когда вы хотите оценивать одно из них.

Итерация нескольких массивов вместе

Тег {{for}} позволяет перебирать сразу несколько массивов. Это полезно, когда у вас есть два массива, которые нужно проходить вместе. Например, следующий код демонстрирует, как тег for будет одновременно перебирать массивы Stars и Cast:

{{for stars cast}}
  <div class="text">{{:name}}</div>
{{/for}}

Если в массиве Stars есть два элемента, а в массиве Cast — три, будет выполнен рендеринг содержимого всех пяти элементов.

Это хороший случай и для использования ключевого слова #data, если элементы в массивах являются не объектами (со свойствами), а простыми строковыми значениями. Например, это могли бы быть строковые массивы MobilePhones и HomePhones, и, возможно, вы захотели бы перечислить их в одном списке.

Вложенные шаблоны

Приложения часто оперируют объектными графами, где у объектов есть свойства, значения которых являются массивами объектов, в свою очередь, возможно, также содержащими массивы объектов. Эти ситуации могут быстро затруднить управление при вложении множества выражений {{for}}. Допустим, в объекте customer содержатся заказы (orders), те содержат позиции заказа (order items), а они — товар (product), который связан со складами (warehouses). Эти иерархические данные можно было бы визуализировать, используя множество тегов {{for}}. Такая ситуация — отличный пример, где содержимое блока {{for}} можно упаковать в отдельный шаблон и визуализировать как вложенный шаблон. Это позволяет сократить код и облегчить его восприятие и сопровождение, а также обеспечить многократное использование и модульность шаблонов.

Вы можете использовать вложенный шаблон с тегом {{for}}, удалив содержимое блока (сделав его самозамыкающимся) и указав внешний шаблон с помощью параметра tmpl. Например, вы можете присвоить параметру tmpl название именованного шаблона (зарегистрированного через $.templates) или использовать селектор jQuery для шаблона, объявленного в блоке script. В следующем синтаксисе применяется именованный шаблон myTmpl для каждого элемента в массиве myArray:

{{for myArray tmpl="myTmpl"/}}

А в этом фрагменте кода для каждого элемента в массиве movies используется шаблон с id, равным movieTemplateMedium (из примера 04-tmpl-combo-iterators.html):

<ul>
  {{for movies
    tmpl="#movieTemplateMedium"/}}
</ul>

Вы можете перебирать несколько массивов вместе и заодно применять к ним шаблон. Например, в следующем фрагменте кода для каждого элемента в свойствах Stars и Cast используется castTemplate (оба свойства являются массивами из примера 07-tmpl-combo-iterators.html):

<ul>
  {{for stars cast tmpl="#castTemplate"/}}
</ul>

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

Тег-шаблон {{if}} тоже можно использовать как самозамыкающийся и ссылающийся на другой шаблон — так же, как тег {{for}}. Например, следующий код будет визуализировать именованный шаблон myAddressTmpl, если address является truthy:

{{if address tmpl="myAddressTmpl"/}}

Примеры, доступные для скачивания, демонстрируют все возможности, рассмотренные в этой статье. На рис. 4 показан список фильмов, визуализированный с применением всех средств из файла примера 07-tmpl-combo-iterators.html.

Putting It All Together
Рис. 4. Сводим все воедино

Заключение

В этой статье были продемонстрированы все базовые возможности JsRender, но за ее рамками осталось гораздо больше. Например, хотя условные теги могут содержать несколько тегов {{for}} с условиями (скажем, выражение switch/case), есть более изящные способы для подобных ситуаций, такие как использование собственных тегов. JsRender поддерживает собственные теги для написания сложной логики, вспомогательных функций, навигации по объектному графу с помощью путей, создания пользовательских функций-конвертеров, поддержки JavaScript-кода в шаблонах, инкапсуляции шаблонов и др. Некоторые из этих возможностей мы рассмотрим в следующей статье.


Исходный код можно скачать по ссылке code.msdn.microsoft.com/mag201204ClientInsight.

Джон Папа (John Papa) — бывший идеолог Microsoft в группах Silverlight и Windows 8, вел популярное шоу Silverlight TV. Выступал с программными речами и докладами на различных секциях конференций BUILD, MIX, PDC, TechEd, Visual Studio Live! и DevConnections. Сейчас является ведущим рубрики «Papa’s Perspective» в журнале «Visual Studio Magazine» и автором обучающих видеороликов в Pluralsight. Следите за его заметками в twitter.com/john_papa.

Выражаю благодарность за рецензирование статьи эксперту Борису Муру (BorisMoore).