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

Более сложная функциональность шаблонов JsRender

Джон Папа

Исходный код можно скачать по ссылке

David PlattШаблоны — мощная штука, но иногда требуется нечто большее стандартной функциональности, предоставляемой механизмом шаблонов в готовом виде. Вам может понадобиться преобразование данных, определение собственных вспомогательных функций или создание своего тега. Хорошая новость в том, что вы можете делать все и это и многое другое, используя базовые средства JsRender.

В прошлой статье (msdn.microsoft.com/magazine/hh882454) мы исследовали основополагающие средства библиотеки поддержки шаблонов JsRender. В этой статье мы продолжим исследование JsRender в более сложных сценариях, таких как рендеринг внешних шаблонов, смена контекста с помощью тега {{for}} и применение комплексных выражений. Я также продемонстрирую, как пользоваться некоторыми более продвинутыми средствами JsRender, в том числе для создания собственных тегов, конвертеров, вспомогательных функций контекста и др. Все примеры кода можно скачать по ссылке, приведенной в конце статьи, а JsRender — по ссылке  bit.ly/ywSoNu.

Вариации {{for}}

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

<!-- цикл {{for}} -->
{{for students}}
{{/for}}

<!-- комбинированные итераторы {{for}} -->
{{for teachers students staff}}
{{/for}}

Тег {{for}} (или любой блочный тег) можно преобразовать из блочного тега (с контентом) в самозакрывающийся тег (self-closing tag), заменив содержимое блока внешним шаблоном, который вы декларативно указываете как свойство tmpl. В таком случае тег осуществляет рендеринг внешнего шаблона вместо подставляемого контента.

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

<!-- самозакрывающийся тег {{for}} -->
{{for lineItems tmpl="#lineItemsDetailTmpl" /}}

Данные редко имеют линейную структуру, и именно поэтому в шаблонах столь важно иметь возможность проходить иерархии объектов. В прошлой статье я продемонстрировал базовые способы вхождения в иерархии объектов, используя нотацию с точкой и квадратные скобки, но вы также можете применять тег {{for}}, который позволяет сокращать код. Это становится еще очевиднее, когда у вас есть объектная структура, где вы входите в иерархию объектов, и вам нужно обеспечить рендеринг набора свойств дочернего объекта. Например, для рендеринга адреса объекта person вы могли написать следующий шаблон, где член address в пути повторяется несколько раз:

<div>{{:address.street1}}</div>
<div>{{:address.street2}}</div>
<div>{{:address.city}}, {{:address.state}}
  {{:address.postalCode}}</div>

Тег {{for}} может значительно упростить код для рендеринга адреса, исключив необходимость в повторении объекта address, как показано ниже:

<!-- "with" {{for}} -->
{{for address}}
  <div>{{:street1}}</div>
  <div>{{:street2}}</div>
  <div>{{:city}}, {{:state}} {{:postalCode}}</div>
{{/for}}

Здесь {{for}} работает со свойством address, которое является объектом со свойствами, а не массивом объектов. Если address достоверен (true) (содержит некое значение, отличное от false), выполняется рендеринг содержимого блока {{for}}. Блок {{for}} также меняет текущий контекст данных с объекта person на объект address; таким образом, он действует подобно команде with, которая присутствует во многих библиотеках и языках. В предыдущем примере тег {{for}} меняет контекст данных на address, а затем осуществляет однократный рендеринг содержимого шаблонов (поскольку адрес только один). Если в объекте person нет address (свойство address равно null или не определено), рендеринг содержимого вообще не происходит. Это делает блок {{for}} очень удобным для включающих шаблонов (containing templates), которые следует отображать лишь в определенных обстоятельствах. Следующий пример (из файла 08-for-variations.html в сопутствующем коде) демонстрирует, как использовать {{for}} для отображения информации о ценах (pricing), если она есть:

{{for pricing}}
  <div class="text">${{:salePrice}}</div>
  {{if fullPrice !== salePrice}}
    <div class="text highlightText">PRICED TO SELL!</div>
  {{/if}}
{{/for}}

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

Повторное использование кода — одно из важнейших преимуществ применения шаблонов. Если шаблон определен внутри тега <script> в той же странице, где он используется, он не является в той мере повторно используемым, в какой мог бы быть. Шаблоны, которые должны быть доступны из нескольких страниц, нужно создавать в своих файлах и извлекать по мере необходимости. JavaScript и jQuery упрощают получение шаблона из внешнего файла, а JsRender облегчает его рендеринг.

При работе с внешними шаблонами я предпочитаю пользоваться соглашением, по которому имена их файлов начинаются со знака подчеркивания, что является популярным соглашением по именованию частичных представлений. Я также ставлю суффикс .tmpl.html в имена всех файлов шаблонов. Часть «.tmpl» обозначает, что это шаблон, а расширение .html просто позволяет средствам разработки вроде Visual Studio распознавать, что шаблон содержит HTML. На рис. 1 показан рендеринг внешнего шаблона.

Рис. 1. Код для рендеринга внешнего шаблона

my.utils = (function () {
  var
    formatTemplatePath = function (name) {
      return "/templates/_" + name + ".tmpl.html";
    },
    renderTemplate =
      function (tmplName, targetSelector, data) {
      var file = formatTemplatePath(tmplName);
      $.get(file, null, function (template) {
        var tmpl = $.templates(template);
        var htmlString = tmpl.render(data);
        if (targetSelector) {
          $(targetSelector).html(htmlString);
        }
        return htmlString;
          });
        };
    return {
      formatTemplatePath: formatTemplatePath,
        renderExternalTemplate: renderTemplate
    };
})()

Один из способов получить шаблон из внешнего файла — написать вспомогательную функцию, которую может вызывать JavaScript-код в веб-приложении. Обратите внимание на рис. 1, что функция renderExternalTemplate в объекте my.utils сначала извлекает шаблон через функцию $.get. Когда этот вызов завершается, функция $.templates создает шаблонJsRender на основе содержимого ответа. Наконец, рендеринг шаблона выполняется с помощью функции render шаблона, и полученный HTML отображается в целевом месте. Этот код можно было бы вызывать, используя следующий код, где имя шаблона, DOM-мишень и контекст данных передаются пользовательской функции renderExternalTemplates:

my.utils.renderExternalTemplate("medMovie",
  "#movieContainer", my.vm);

Внешний шаблон для этого примера находится в файле _medMo­vie.tm­pl.html и содержит только HTML и теги JsRender. Он не обернут в тег <script>. Я предпочитаю эту методику работы с внешними шаблонами, потому что среда разработки будет распознавать, что их содержимым является HTML. Почему это важно? Дело в том, что тогда при написании кода вы будете сажать меньше ошибок, так как в этом варианте сразу же получаете поддержку IntelliSense. Однако в файле могли бы находиться несколько шаблонов, причем каждый из них обернут в тег <script> и каждому присвоен уникальный идентификатор. Это просто другой способ обработки внешних шаблонов. Конечный результат представлен на рис. 2.

Рис. 2. Результат рендеринга внешнего шаблона

Пути представления

JsRender содержит несколько особых путей представления (view paths), которые упрощают доступ к текущему объекту view. Например, #view обеспечивает доступ к текущему представлению, #data — к текущему контексту данных для этого представления, #parent поднимает на ступень выше в иерархии объектов и #index возвращает свойство index:

<div>{{:#data.section}}</div>
<div>{{:#parent.parent.data.number}}</div>
<div>{{:#parent.parent.parent.parent.data.name}}</div>
<div>{{:#view.data.section}}</div>

При использовании путей представления (отличных от #view) они оперируют с текущим view. Другими словами, следующие строки эквивалентны:

#data
#view.data

Пути представления полезны при навигации по таким иерархиям объектов, как «customer – order – order details» или movies (как показано в файле примера 11-view-paths.html).

Выражения

Общие выражения (common expressions) — важная часть логики, и они могут быть полезны при выборе способа рендеринга шаблона. JsRender поддерживает общие выражения, перечисленные (но не ограниченные ими) в табл. 1.

Табл. 1. Общие выражения в JsRende

Выражение

Пример

Описание

+ {{ :a + b }} Сложение
- {{ :a - b }} Вычитание
* {{ :a * b }} Умножение
/ {{ :a / b }} Деление
|| {{ :a || b }} Логическое «Или»
&& {{ :a && b }} Логическое «И»
! {{ :!a }} Отрицание
? : {{ :a === 1 ? b * 2: c * 2 }} Выражение третьего порядка
( ) {{ :(a||-1) + (b||-1) }} Порядок вычислений, определяемый круглыми скобками
% {{ :a % b }} Операция по модулю
<=, >=, < и > {{ :a <= b }} Операции сравнения
  {{ :a === b }} Равенство и неравенство

 

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

Например, выполнение {{:a++}} в JsRender привело бы к ошибке, так как здесь предпринимается попытка увеличить значение переменной a. Кроме того, выполнение {{:alert('hello')}} тоже генерирует ошибку, поскольку это выражение пытается вызвать несуществующую функцию#view.data.alert.

Регистрация собственных тегов

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

{{myConverter:name}}

{{myTag name}}

{{:~myHelper(name)}}

{{:~myParameter}}

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

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

{{:rating}}

Однако может оказаться предпочтительнее использовать логику в виде JavaScript-кода для визуализации таких оценок с применением CSS и набора изображений пустых и заполненных звездочек:

{{createStars averageRating max=5/}}

Логику создания звездочек можно (и нужно) отделить от их визуализации. JsRender предоставляет способ создания собственного тега, который обертывает эту функциональность. В коде на рис. 3 определен собственный тег с именем createStars, и он регистрируется в JsRender, чтобы его можно было использовать в любой странице, которая загружает этот скрипт. Применение этого тега требует, чтобы его JavaScript-файл (в примере кода — jsrender.tag.js) включался в страницу.

Рис. 3. Создание собственного тега

$.views.tags({
  createStars: function (rating) {
    var ratingArray = [], defaultMax = 5;
    var max = this.props.max || defaultMax;
    for (var i = 1; i <= max; i++) {
      ratingArray.push(i <= rating ?
      "rating fullStar" : "rating emptyStar");
    }
    var htmlString = "";
    if (this.tmpl) {
      // Используем контент или шаблон,
      // переданный со свойством template
      htmlString = this. renderContent(ratingArray);
    } else {
        // Используем скомпилированный именной шаблон
        htmlString = $.render.compiledRatingTmpl(ratingArray);
    }
    return htmlString;
  }
...

У собственных тегов могут быть декларативные свойства, например max=5 в {{createStars}}, показанном ранее. Они доступны в коде через this.props. Так, в следующем коде регистрируется собственный тег с именем sort, который принимает array (если свойство reverse установлено в true, {{sort array reverse=true/}}, array возвращается в обратном порядке):

$.views.tags({
sort: function(array){
  var ret = "";
  if (this.props.reverse) {
    for (var i = array.length; i; i--) {
      ret += this.tmpl.render(array[i - 1]);
    }
  } else {
      ret += this.tmpl.render(array);
  }
  return ret;
}}

Хорошее правило — использовать собственный тег, когда требуется рендеринг чего-то более сложного (вроде createStars или sort) и его нужно повторно использовать. Для разовых случаев собственные теги подходят не столь идеально.

Конвертеры

Хотя собственные теги идеальны для создания контента, конвертеры лучше подходят для простой задачи преобразования исходного значения в другое. Конвертеры могут изменять исходное значение (например, булево значение true или false) в нечто совершенно иное (скажем, в зеленый или красный цвет соответственно). Так, в следующем коде используется конвертер priceAlert для возврата строки, содержащей уведомление о текущей котировке (price alert) на основе значения salePrice:

<div class="text highlightText">{{priceAlert:salePrice}}</div>

Конвертеры очень хороши и для изменения URL:

<img src="{{ensureUrl:boxArt.smallUrl}}" class="rightAlign"/>

В следующем примере конвертер ensureUrl должен преобразовать значение boxArt.smallUrl в полный URL (оба этих конвертера используются в файле 12-converters.html и регистрируются в jsrender.helpers.js с помощью JsRender-функции $.views.converters):

$.views.converters({
  ensureUrl: function (value) {
    return (value ? value : "/images/icon-nocover.png");
  },
  priceAlert: function (value) {0
    return (value < 10 ? "1 Day Special!" : "Sale Price");
  }
});

Конвертеры предназначены для непараметрического преобразования данных в визуализируемое значение. Если в вашей ситуации требуются параметры, тогда вам больше подойдет вспомогательная функция или собственный тег. Как мы уже видели, собственные теги поддерживают именованные параметры, поэтому тег createStars мог бы иметь параметры, определяющие размер звездочек и их цвета, а также CSS-классы, применяемые к ним, и т. д. Самое главное в том, что конвертеры рассчитаны на простые преобразования, тогда как собственные теги предназначены для более сложного рендеринга.

Вспомогательные функции и параметры шаблонов

В процессе рендеринга шаблона можно передавать вспомогательные функции или параметры несколькими способами. Один из них — регистрация с помощью $.views.helpers по аналогии с регистрацией тегов или конвертеров:

$.views.helpers({
  todaysPrices: { unitPrice: 23.40 },
  extPrice:function(unitPrice, qty){
    return unitPrice * qty;
  }
});

Это сделает их доступными во всех шаблонах в приложении. Другой способ — передача в вызове render:

$.render.myTemplate( data, {
  todaysPrices: { unitPrice: 23.40 },
  extPrice:function(unitPrice, qty){
    return unitPrice * qty;
  }
});

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

{{: ~extPrice(~todaysPrices.unitPrice, qty) }}

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

Например, вы могли бы создать вспомогательную функцию с именем getGuitars, которая выполняла бы поиск в массиве товаров и находила бы все товары, относящиеся к гитарам. Она также могла бы принимать параметр для типа гитары. Потом результат можно было бы использовать для рендеринга единственного значения или перебирать в цикле полученный массив (поскольку вспомогательные функции могут возвращать что угодно). Следующий код мог бы получать массив всех товаров, которые являются акустическими гитарами, и перебирать их в цикле, используя блок {{for}}:

{{for ~getGuitars('acoustic')}} ... {{/for}}

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

{{:~totalPrice(~extendedPrice(lineItems, discount), taxRate}}

Вспомогательные функции, доступные нескольким шаблонам, определяются передачей объектного литерала, содержащего вспомогательные функции, в JsRender-функцию $.views.helpers. В следующем примере функция concat сцепляет несколько аргументов:

$.views.helpers({
  concat:function concat() {
    return "".concat.apply( "", arguments );
  }
})

Вспомогательную функцию concat можно вызывать, используя {{:~concat(first, age, last)}}. Если значения first, age и last доступны и составляют соответственно John, 25 и Doe, она вернет значение John25Doe.

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

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

$.render.shoppingCartTemplate( data, {
  todaysPrices: { unitPrice: 23.40 },
  extPrice:function(unitPrice, qty){
    return unitPrice * qty;
  }
});

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

Что использовать?

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

Рис. 4. Дерево решений для выбора правильного варианта

if (youPlanToReuse) {
  if (simpleConversion && !parameters){
    // Регистрируем конвертер
  }
  else if (itFeelsLikeAControl && canBeSelfContained){
    // Регистрируем собственный тег
  }
  else{
    // Регистрируем вспомогательную функцию
  }
}
else {
  // Передаем вспомогательную функцию с параметрами для шаблона
}

Если функция используется лишь раз, нет нужды делать ее доступной в рамках приложения. Это идеальная ситуация для «разовой» функции, которая передается при необходимости.

Встраивание кода в шаблон

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

Код можно встраивать в шаблон, обертывая этот код в блок с префиксом в виде звездочки — {{* }} — и присваивая allowCode значение true. Например, в шаблон myTmpl (рис. 5) встроен код, оценивающий подходящие места для рендеринга команды или слова «and» в ряде языков. Полный пример вы найдете в файле 13-allowcode.html. Логика не слишком сложна, но код может оказаться трудным для чтения в шаблоне.

Рис. 5. Встраивание кода в шабло

<script id="myTmpl" type="text/x-jsrender">
  <tr>
    <td>{{:name}}</td>
    <td>
      {{for languages}}
        {{:#data}}{{*
         if ( view.index === view.parent.data.length - 2 ) {
        }} and {{*
         } else if (view.index < view.parent.data.length - 2) {
        }}, {{* } }}
      {{/for}}
    </td>
  </tr>
</script>

JsRender не позволит выполнять этот код, пока свойство allowCode не будет установлено в true (по умолчанию — false). В следующем коде определяется компилируемый шаблон movieTmpl, ему присваивается разметка из тега script, показанного на рис. 5, и указывается, что этот код должен выполняться в шаблоне (allowCode равно true):

$.templates("movieTmpl", {
  markup: "#myTmpl",
  allowCode: true
});

$("#movieRows").html(
  $.render.movieTmpl(my.vm.movies)
);

Как только шаблон создан, можно выполнять его рендеринг. Функциональность allowCode может приводить к тому, что код окажется трудным в чтении, и в некоторых случаях ту же работу способна выполнить вспомогательная функция. Так, в примере на рис. 5 функциональность allowCode в JsRender используется для добавления запятых и слова «and» там, где это необходимо. Однако то же самое можно было бы сделать, создав такую вспомогательную функцию:

$.views.helpers({
  languagesSeparator: function () {
    var view = this;
    var text = "";
    if (view.index === view.parent.data.length - 2) {
      text = " and";
    } else if (view.index < view.parent.data.length - 2) {
      text = ",";
    }
    return text;
  }
})

Эта вспомогательная функция — languagesSeparator — вызывается заданием префикса «~» в ее имени. Благодаря этому код шаблона, вызывающего эту вспомогательную функцию, становится гораздо понятнее:

{{for languages}}
  {{:#data}}{{:~languagesSeparator()}}
{{/for}}

Выделение логики во вспомогательную функцию позволило изъять ее из шаблона и перенести в JavaScript-код.

Производительность и гибкость

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


Джон Папабывший идеолог 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).