СЕНТЯБРЬ 2015

ТОМ 30, НОМЕР 9

Доступ к данным - Снова о связывании с данными в JavaScript — на этот раз с помощью Aurelia

Джули Лерман

Джули ЛерманЯ никогда не была разработчиком клиентской части ПО, но время от времени у меня появлялись причины на эксперименты с UI. В своей рубрике за июнь 2012 года после посещения презентации по Knockout.js я копнула клиентскую часть и написала статью о связывании с данными OData на веб-сайте, используя Knockout (msdn.microsoft.com/magazine/jj133816). Спустя несколько месяцев я написала о добавлении Breeze.js, что еще больше упрощает связывание с данными через Knockout.js (msdn.microsoft.com/magazine/jj863129). Наконец, когда я вновь вернулась к Knockout в 2014 году и написала о модернизации старого приложения на основе ASP.NET 2.0 Web Forms, некоторые из моих друзей стали дразнить меня тем, что Knockout остался на уровне 2012 года. Более новые инфраструктуры вроде Angular тоже обеспечивают связывание с данными и очень много другое. Но это «очень многое другое» на самом деле мне было не интересно, поэтому Knockout меня вполне устраивал.

Что ж, на дворе уже 2015 год, и, хотя Knockout по-прежнему популярен и превосходно обеспечивает связывание с данными в JavaScript, я захотела повозиться какое-то время с одной из новых инфраструктур и выбрала Aurelia (Aurelia.io), поскольку знаю, как восхищаются ею очень многие веб-разработчики. Проект Aurelia был начат Робом Эйзенбергом (Rob Eisenberg), стоявшим у истоков Durandal, другой клиентской JavaScript-инфраструктуры; однако он забросил Durandal, присоединившись к группе разработчиков Angular в Google. В конечном счете он оставил и эту группу, а потом вместо возрождения Durandal создал с нуля инфраструктуру Aurelia. В Aurelia уйма интереснейших вещей. Мне еще надо многое изучить, но сегодня я хочу поделиться с вами некоторыми освоенными мной приемами связывания с данными на основе этой инфраструктуры, а также кое-какими фокусами с EcmaScript 6 (ES6), новейшей версией JavaScript, которая стала стандартом в июне 2015 года.

ASP.NET Web API для обслуживания моего веб-сайта данными

Я использую ASP.NET Web API, созданный мной для предоставления данных, сохраняемых через Entity Framework 6. В этом Web API есть набор простых методов, вызываемых по HTTP.

Метод Get (рис. 1) принимает запрос и параметры разбиения на страницы (paging), а затем передает их методу репозитария, который извлекает список объектов Ninja и связанных с ними объектов Clan, используя Entity Framework DbContext. Получив результаты, метод Get преобразует их в набор ViewListNinja Data Transfer Objects (DTO), определенных в каком-то другом месте. Это важный шаг из-за способа сериализации JSON, который слегка злоупотребляет круговыми ссылками от Clan к другим объектам Ninja. С помощью DTO я избегаю передачи по сети лишних данных и добиваюсь формы результатов, ближе соответствующей используемой на клиенте.

Рис. 1. Метод Get из Web API

public IEnumerable<ViewListNinja> Get(string query = "",
  int page = 0, int pageSize = 20)
  {
    var ninjas = _repo.GetQueryableNinjasWithClan(
      query, page, pageSize);
    return ninjas.Select(n => new ViewListNinja
                              {
                                ClanName = n.Clan.ClanName,
                                DateOfBirth = n.DateOfBirth,
                                Id = n.Id,
                                Name = n.Name,
                                ServedInOniwaban =
                                  n.ServedInOniwaban
                              });
    }

На рис. 2 показано представление JSON-результатов от этого метода на основе запроса, который извлек два объекта Ninja.

JSON-результаты запроса от Web API списка объектов Ninja
Рис. 2. JSON-результаты запроса от Web API списка объектов Ninja

Запрос Web API с помощью инфраструктуры Aurelia

Парадигма Aurelia выражается в попарном соединении одной модели представления (JavaScript-класса) с одним представлением (HTML-файлом) и выполнением связывания с данными между ними. Поэтому у меня есть файлы ninjas.js и ninjas.html. Модель представления Ninjas определяется наличием массива Ninjas, а также объекта Ninja:

export class Ninja {
  searchEntry = '';
  ninjas = [];
  ninjaId = '';
  ninja = '';
  currentPage = 1;
  textShowAll = 'Show All';

  constructor(http) {
    this.http = http;
  }

Самый важный метод в ninjas.js — retrieveNinjas, который вызывает Web API:

retrieveNinjas() {
  return this.http.createRequest(
    "/ninjas/?page=" + this.currentPage +
    "&pageSize=100&query=" + this.searchEntry)
    .asGet().send().then(response => {
      this.ninjas = response.content;
    });
  }

В каком-то месте веб-приложения я задала базовый URL, который Aurelia будет находить и включать в URL запроса:

x.withBaseUrl('http://localhost:46534/api');

Стоит отметить, что файл ninjas.js — простой JavaScript. Если вы использовали Knockout, то, вероятно, помните, что модель представления нужно было настраивать согласно концепции Knockout, чтобы при связывании объекта с разметкой Knockout понимал, что с этим делать. Это не относится к Aurelia.

Ответ теперь включает список объектов Ninja, и я присваиваю его массиву ninjas в своей модели представления, которая в конечном счете возвращается странице ninjas.html, инициировавшей запрос. В разметке нет ничего, что идентифицирует модель, — заботиться об этом не надо потому, что модель спарена с HTML. По сути, большая часть страницы представляет собой стандартный HTML и какой-то JavaScript-код всего с несколькими специальными командами, которые Aurelia распознает и обрабатывает.

Связывание с данными, интерполяция строк и форматирование

Самая интересная часть ninjas.html — это div, используемый для отображения списка ninja:

<div class="row">
  <div  repeat.for="ninja of ninjas">
    <a href="#/ninjas/${ninja.Id}"
      class="btn btn-default btn-sm" >
      <span class="glyphicon glyphicon-pencil" />  </a>
    <a click.delegate="$parent.deleteView(ninja)"
      class="btn btn-default btn-sm">
      <span class="glyphicon glyphicon-trash" />  </a>
    ${ninja.Name}  ${ninja.ServedInOniwaban ? '[Oniwaban]':''}
    Birthdate:${ninja.DateOfBirth | dateFormat}
  </div>
</div>

Первая специфичная для Aurelia разметка в этом коде — repeat.for="ninja of ninjas", за которой следует цикл в парадигме ES6. Поскольку Aurelia понимает модель представления, она распознает, что ninjas — это свойство, определенное как массив. У переменной ninja может быть любое имя, например foo. Она просто представляет каждый элемент в массиве ninjas. Теперь остается лишь перебрать в цикле все элементы массива ninjas. Пропускаем часть разметки до мета, где отображаются свойства, скажем, ${ninja.Name}. Это функциональность ES6, называемая интерполяцией строк (string interpolation), которой пользуется Aurelia. Интерполяция строк упрощает композицию строк со встроенными в них переменными без конкатенации. Поэтому при наличии имени переменной «Julie» можно написать в JavaScript:

`Hi, ${name}!`

и это будет интерпретировано как «Hi, Julie!» Aurelia использует преимущества интерполяции строк из ES6 и предполагает одностороннюю привязку данных, встречая такой синтаксис. Поэтому последняя строка кода, начинающаяся с ${ninja.Name}, выведет свойства ninja наряду с остальным HTML-текстом. Если вы кодируете на C# или Visual Basic, то стоит отметить, что интерполяция строк — новая функциональность как C# 6.0, так и Visual Basic 14.

Попутно мне пришлось немного подучить синтаксис JavaScript, например условная оценка ServedInOniwaban Boolean оказывается имеет тот же синтаксис, что и в C#: condition ? true : false.

Форматирование даты, примененное мной к свойству DateOfBirth, — еще один функционал Aurelia, который может быть знаком вам, если вы работали с XAML. Aurelia использует конвертеры значений (value converters). Мне нравится использовать JavaScript-библиотеку moment, помогающую форматировать даты и время, и я задействовала ее преимущества в классе date-format.js:

import moment from 'moment';

export class dateFormatValueConverter {
  toView(value) {
  return moment(value).format('M/D/YYYY');
  }
}

Учтите, что вы должны указывать в имени класса «ValueConverter».

Вверху HTML-страницы, прямо под начальным элементом <template> я указываю ссылку на тот файл:

<template>
  <require from="./date-format"></require>

Теперь функциональность интерполяции строк способна найти dateFormat[ValueConverter] в моей разметке и применить ее к выводу, как показано на рис. 3.

Отображение всех Ninja с помощью свойств, односторонне связанных Aurelia через интерполяцию строк
Рис. 3. Отображение всех Ninja с помощью свойств, односторонне связанных Aurelia через интерполяцию строк

Я хочу указать в div другой экземпляр привязки, но это привязка событий, а не данных. Заметьте, что в первом теге hyperlink я применяю стандартный синтаксис, встраивая URL в атрибут href. Но во втором я использую не href, а click.delegate. Delegate — не новая команда, однако Aurelia обрабатывает ее особым образом (детали см. по ссылке bit.ly/1Jvj38Z). А здесь я продолжу говорить о том, что относится к связыванию с данными.

Значки правки ведут к URL, который включает идентификатор ninja. Я указала механизму переадресации (routing mechanism) Aurelia отправлять пользователя при щелчке такого значка на страницу Edit.html. Она связана с моделью представления, которая находится в классе Edit.js.

Самые важные методы в Edit.js предназначены для получения и сохранения выбранного ninja. Начнем с retrieveNinja:

retrieveNinja(id) {
  return this.http.createRequest("/ninjas/" + id)
    .asGet().send().then(response => {
      this.ninja = response.content;
    });
  }

Это формирует примерно тот же запрос к моему Web API, что и раньше, но на этот раз к запросу добавляется id.

В классе ninjas.js я связала результаты со свойством-массивом ninjas модели представления. Здесь я присваиваю результат (единственный объект) свойству ninja текущей модели представления.

Вот метод Web API, который будет вызван из-за добавления id к URI:

public Ninja Get(int id)
  {
    return _repo.GetNinjaWithEquipmentAndClan(id);
  }

Результаты этого метода гораздо богаче возвращаемых для списка ninja. На рис. 4 показан JSON, возвращаемый одним из запросов.

JSON-результаты запроса одного ninja от Web API
Рис. 4. JSON-результаты запроса одного ninja от Web API

Передав результаты в свою модель представления, я могу связать свойства ninja с элементами HTML-страницы. На этот раз я использую команду .bind. Aurelia логически распознает, какая привязка нужна — односторонняя, двухсторонняя или иная. По сути, как вы видели в ninjas.html, она использует свой нижележащий рабочий процесс связывания при интерполяции строк. В том случае она задействовала разовую одностороннюю привязку. Здесь, поскольку я использую команду .bind и осуществляю связывание с элементами input, Aurelia логически распознает, что мне требуется двухсторонняя привязка. Это ее выбор по умолчанию, который можно было бы переопределить, указав .one-way или другую команду вместо .bind.

Для краткости я извлеку только релевантную разметку, а не все окружающие элементы. Вот элемент input, связанный со свойством Name свойства ninja в модели, передаваемой обратно из класса modelview:

<input value.bind="ninja.Name" />

А вот другой элемент input, на этот раз связанный с полем DateOfBirth. Мне нравится, как легко повторно использовать конвертер значений даты и времени даже в этом контексте, применяя ранее показанный синтаксис:

<input value.bind="ninja.DateOfBirth | dateFormat" />

Я также хочу перечислить на той же странице боевые навыки (equipment) по аналогии с тем, как я перечисляла объекты ninja, чтобы их можно было редактировать или удалять. Для демонстрации я зашла так далеко, что вывела список в виде строк, но не реализовала ни средства редактирования и удаления, ни возможность добавления боевых навыков:

<div repeat.for="equip of ninja.EquipmentOwned">
  ${equip.Name} ${equip.Type}
</div>

На рис. 5 показана форма с полями, привязанными к данным.

Страница Edit, отображающая список боевых навыков
Рис. 5. Страница Edit, отображающая список боевых навыков

В Aurelia также имеется механизм, называемый адаптивной привязкой (adaptive binding), который позволяет адаптировать средства привязки на основе доступной в браузере функциональности или даже передаваемых объектов. Это потрясающе и предназначено для работы с браузерами и библиотеками, постепенно развиваемыми с течением времени. Подробнее об адаптивной привязке см. по ссылке bit.ly/1GhDCDB.

В настоящее время я редактирую только имя ниндзя, дату рождения и индикатор Oniwaban. Когда пользователь сбрасывает флажок Served in Oniwaban и нажимает кнопку Save, происходит вызов метода save моей модели представления, который делает кое-что интересное, прежде чем отправить данные обратно в Web API. Сейчас, как видно на рис. 4, объект ninja является глубоким графом (deep graph). Для сохранения не требуется отправлять все содержимое графа обратно — достаточно передачи релевантных свойств. Поскольку на другом конце я использую EF, я хочу быть уверенной в том, что не отредактированные мной свойства не будут переданы заодно с отредактированными и соответственно не будут заменены null-значениями в базе данных. Поэтому я создаю «на лету» DTO с именем ninjaRoot. Я уже объявляла ninjaRoot как свойство в модели представления. Но определение ninjaRoot будет задаваться тем, как я создаю его в своем методе Save (рис. 6). Я была осторожной, используя одинаковые имена свойств и упаковывая их так, как ожидает Web API, чтобы он мог десериализовать это в известный для API тип Ninja.

Рис. 6. Метод Save в модели представления Edit

save() {
        this.ninjaRoot = {
          Id: this.ninja.Id,
          ServedInOniwaban: this.ninja.ServedInOniwaban,
          ClanId: this.ninja.ClanId,
          Name: this.ninja.Name,
          DateOfBirth: this.ninja.DateOfBirth,
          DateCreated: this.ninja.DateCreated,
          DateModified: this.ninja.DateModified
        };
        this.http.createRequest("/ninjas/")
          .asPost()
          .withHeader('Content-Type',
          'application/json; charset=utf-8')
          .withContent(this.ninjaRoot).send()
          .then(response => {
            this.myRouter.navigate('ninjas');
          }).catch(err => {
            console.log(err);
          });

    }

Обратите внимание на метод asPost в этом вызове. Он гарантирует, что запрос будет направлен методу Post в Web API:

public void Post([FromBody] object ninja)
{
  var asNinja =
    JsonConvert.DeserializeObject<Ninja>
    (ninja.ToString());
  _repo.SaveUpdatedNinja(asNinja);
}

JSON-объект десериализуется в локальный объект Ninja, а затем передается методу репозитария, который знает, как обновить этот объект в базе данных.

Когда я возвращаюсь к списку Ninjas на своем веб-сайте, изменение отражается на выводе.

Больше, чем просто связывание с данными

Помните, что Aurelia — это инфраструктура с гораздо большей простого связывания с данными функциональностью, но, оставаясь верной своей натуре, я сосредоточилась на данных, как делаю всегда на первых этапах освоения нового инструментария. Чтобы узнать намного больше, отправляйтесь на веб-сайт Aurelia; кроме того, вы сможете пообщаться там с большим сообществом (gitter.im/Aurelia/Discuss).

Я хочу выразить огромную благодарность авторам публикаций на веб-сайте tutaurelia.net, особенно серии статей по комбинированному использованию ASP.NET Web API и Aurelia. На первой итерации кода, который отображает список ninjas я опиралась на часть 6, написанную автором Бартом ван Хой (Bart Van Hoey). Возможно, ко времени публикации этой статьи в этой серии на сайте появится больше публикаций.

Пакет исходного кода, сопутствующий этой статье, включает как решение Web API, так и веб-сайт на основе Aurelia. Решение Web API можно использовать в Visual Studio. Я создавала его в Visual Studio 2015, используя Entity Framework 6 и Microsoft .NET Framework 4.6. Если вы хотите запустить веб-сайт, то должны зайти на сайт Aurelia.io и изучить, как установить Aurelia и запустить веб-сайт. Вы также можете посмотреть демонстрацию этого приложения в моем учебном курсе Pluralsight «Getting Started with Entity Framework 6» по ссылке bit.ly/PS-EF6Start.


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

Джули Лерман (Julie Lerman) — Microsoft MVP, преподаватель и консультант по .NET, живет в Вермонте. Часто выступает на конференциях по всему миру и в группах пользователей по тематике, связанной с доступом к данным и другими технологиями Microsoft .NET. Ведет блог thedatafarm.com/blog и является автором серии книг «Programming Entity Framework» (O’Reilly Media, 2010), в том числе «Code First Edition» (2011) и «DbContext Edition» (2012), также выпущенных издательством O’Reilly Media. Вы можете читать ее заметки в twitter.com/julielerman и смотреть ее видеокурсы для Pluralsight на juliel.me/PS-Videos.


Выражаю благодарность за рецензирование статьи эксперту Durandal Inc. Робу Эйзенбергу (Rob Eisenberg).