Клиентские библиотеки
Использование JsRender с JavaScript и HTML
На многих платформах разработки используются шаблоны для сокращения объема кода и упрощения сопровождения, так что 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, где рядом с картинкой отображаются звездочки рейтинга фильма.
Рис. 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). Не слишком хороший результат в данной ситуации, но пример иллюстрирует, как проходить по иерархии объектов вверх и вниз.
Рис. 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.
Рис. 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).