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

Приступаем к работе с Knockout

Джон Папа

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

John PapaСегодня связывание с данными — одна из наиболее популярных функций в разработке, и JavaScript-библиотека Knockout предоставляет такие функции при разработке с применением HTML и JavaScript. Простота синтаксиса декларативного связывания и беспроблемная интеграция с шаблонами разделения обязанностей (separation patterns), такими как Model-View-ViewModel (MVVM), значительно упрощают распространенные задачи передачи и приема данных, в то же время облегчая поддержку и совершенствование кода. В этой новой рубрике я расскажу о ситуациях, в которых применение Knockout просто идеально, объясню, как приступить к работе с ней, и продемонстрирую использование ее фундаментальных средств. Примеры кода, которые можно скачать по указанной в конце статьи ссылке, демонстрируют, как использовать декларативное связывание, создавать различные типы связующих объектов и писать ориентированный на данные JavaScript-код, который следует хорошим шаблонам разделения обязанностей вроде MVVM.

Приступаем

Knockout, разработанная Стивом Сандерсоном (Steve Sanderson), — небольшая библиотека JavaScript с открытым исходным кодом и лицензией MIT. На сайте Knockoutjs.com ведется обновляемый список браузеров, которые поддерживает Knockout (в настоящее время поддерживаются все основные браузеры, в том числе Internet Explorer 6+, Firefox 2+, Chrome, Opera и Safari). Чтобы приступить к разработке с использованием Knockout, вам потребуется несколько важных ресурсов. Начните со скачивания самой свежей версии Knockout (2.0.0 на данный момент) по ссылке bit.ly/scmtAi и добавьте ссылку на нее в свой проект. Однако, если вы используете Visual Studio 2010, настоятельно рекомендую вам предварительно установить NuGet Package Manager — расширение Visual Studio. И скачивать Knockout (а также любые другие нужные вам библиотеки) только с его помощью, потому что он обеспечивает управление версиями и уведомляет вас о появлении новых версий. NuGet скачает Knockout и поместит два JavaScript-файла в папку scripts вашего проекта. Для производственных целей рекомендуется файл с более коротким именем (knockout-x.y.z.js, где x.y.z — основной, вспомогательный номера и номер ревизии). Имеется также файл knockout-x.y.x-debug.js, содержащий исходный код Knockout в форме, читаемой человеком. Советую ссылаться на этот файл при изучении Knockout и в процессе отладки.

Получив файлы, откройте свою HTML-страницу (или Razor-файл, если вы используете ASP.NET MVC), создайте скрипт и добавьте ссылку на библиотеку Knockout:

<script src="../scripts/knockout-2.0.0.js" type="text/javascript"></script>

Никаких привязок

Освоение Knockout лучше всего начать с примера того, как вы писали бы код для передачи данных от объекта-источника в HTML-элементы без Knockout (соответствующий исходный код см. на странице 01-without-knockout.html в пакете для скачивания). Затем я покажу, как выполнить то же самое с применением Knockout. А пока возьмем несколько целевых HTML-элементов и передадим (push) некоторые значения из объекта-источника в эти элементы:

<h2>Without Knockout</h2>
<span>Item number:</span><span id="guitarItemNumber"></span>
<br/>
<span>Guitar model:</span><input id="guitarModel"/>
<span>Sales price:</span><input  id="guitarSalesPrice"/>

Если у вас есть объект, из которого вы хотите передавать данные в стандартные HTML-элементы, можно было бы использовать jQuery:

$(document).ready(function () {
  var product = {
    itemNumber: "T314CE",
    model: "Taylor 314ce",
    salePrice: 1199.95
    }; 
    $("#guitarItemNumber").text(product.itemNumber);
    $("#guitarModel").val(product.model);
    $("#guitarSalesPrice").val(product.salePrice);
});

В этом примере кода jQuery используется для поиска HTML-элементов с соответствующими идентификаторами и присваивания их значений подходящим свойствам объекта.

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

Тот же HTML можно было переписать с применением Knockout:

<h2>With Knockout</h2>
<span Item number</span><span data-bind="text: itemNumber"></span>
<br/>
<span>Guitar model:</span><input data-bind="value: model"/>
<span>Sales price:</span><input data-bind="value: salePrice"/>

Обратите внимание на то, что атрибуты id были заменены на атрибуты data-bind. Как только вы вызываете функцию applyBindings, Knockout связывает данный объект (product — в этом примере) со страницей. Это устанавливает объект product в качестве контекста данных для страницы, т. е. целевые элементы могут теперь идентифицировать свойства этого контекста данных, с которыми их требуется связать:

ko.applyBindings(product);

Значения в объекте-источнике будут передаваться целевым элементам на этой странице. (Все функции Knockout находятся в собственном пространстве имен: ko.) Связь между целевыми элементами и объектом-источником определяется атрибутом data-bind. В предыдущем примере Knockout обнаруживает такой атрибут для первого тега span и узнает, что его значение text должно быть связано со свойством itemNumber. Затем Knockout передает значение для свойства product.itemNumber в элемент.

Как видите, использование Knockout могло бы значительно сократить объем кода. По мере увеличения количества свойств и элементов единственное, что нужно добавлять из JavaScript-кода, — функция applyBindings. Однако это не решает всех проблем. Этот пример пока не позволяет обновлять ни HTML-элементы при изменениях в объекте-источнике, ни сам источник при изменениях в HTML-элементах. Для этой цели нужны наблюдаемые объекты (observables).

Наблюдаемые объекты

Knockout добавляет отслеживание зависимостей через наблюдаемые объекты (observables), которые являются объектами, способными уведомлять слушателей при изменении нижележащих значений (эта концепция аналогична интерфейсу INotifyProperty¬Changed в технологии XAML). Knockout реализует наблюдаемые свойства, обертыванием свойств объекта с помощью пользовательской функции observable. Например, вместо задания свойства в объекте таким образом:

var product = { 
  model: "Taylor 314ce" 
}

вы можете определить свойство как наблюдаемое:

var product = { 
  model: ko.observable("Taylor 314ce") 
}

Как только свойства определяются в качестве наблюдаемых, связывание с данными обретает свои истинные очертания. JavaScript-код на рис. 1 демонстрирует два объекта, которые Knockout связывает с HTML-элементами. Первый объект (data.product1) определяет собственные свойства с использованием простого объектного литерала, тогда как второй (data.product2) — с применением observable.

Рис. 1. Объекты с наблюдаемыми свойствами и без

$(document).ready(function () {
  var data = {
    product1: {
      id: 1002,
      itemNumber: "T110",
      model: "Taylor 110",
      salePrice: 699.75
    },
    product2: {
      id: ko.observable(1001),
      itemNumber: ko.observable("T314CE"),
      model: ko.observable("Taylor 314ce"),
      salePrice: ko.observable(1199.95)
    }
  };
 
  ko.applyBindings(data);
});

В HTML для этого примера на рис. 2 видны четыре набора привязок элементов. Первый и второй теги div содержат HTML-элементы, связанные с не наблюдаемыми свойствами. Когда значение в первом div изменяется, ничего другого не происходит. Третий и четвертый теги div содержат HTML-элементы, связанные с наблюдаемыми свойствами. Заметьте, что при изменении значений в третьем div элементы в четвертом div обновляются. Вы можете опробовать эту демонстрацию, используя пример 02-observable.html.

Рис. 2. Связывание с наблюдаемыми и не наблюдаемыми свойствами

<div>
  <h2>Object Literal</h2>
  <span>Item number</span><span data-bind="text: product1.itemNumber"></span>
  <br/>
  <span>Guitar model:</span><input data-bind="value: product1.model"/>
  <span>Sales price:</span><input data-bind="value: product1.salePrice"/>
</div>
<div>
  <h2>Underlying Source Object for Object Literal</h2>
  <span>Item number</span><span data-bind="text: product1.itemNumber"></span>
  <br/>
  <span>Guitar model:</span><span data-bind="text: product1.model"></span>
  <span>Sales price:</span><span data-bind="text: product1.salePrice"></span>
</div>
<div>
  <h2>Observables</h2>
    <span>Item number</span><span data-bind="text: product2.itemNumber"></span>
  <br/>
    <span>Guitar model:</span><input data-bind="value: product2.model"/>
    <span>Sales price:</span><input data-bind="value: product2.salePrice"/>
</div>
<div>
  <h2>Underlying Source Object for Observable Object</h2>
  <span>Item number</span><span data-bind="text: product2.itemNumber"></span>
  <br/>
    <span>Guitar model:</span><span data-bind="text: product2.model"></span>
    <span>Sales price:</span><span data-bind="text: product2.salePrice"></span>
</div>

(Заметьте, что Knockout не требует обязательного использования наблюдаемых свойств. Если вам нужно, чтобы DOM-элементы получали значения лишь раз, а потом больше не обновлялись при изменениях в объекте-источнике, будет достаточно простых объектов. Но, если вы хотите синхронизации объекта-источника и целевых DOM-элементов, т. е. двухстороннего связывания, тогда вам следует подумать о применении наблюдаемых свойств.)

Встроенные привязки

До сих пор примеры демонстрировали, как выполнять связывание с привязками текста и значений в Knockout. Однако в Knockout много встроенных привязок, упрощающих связывание свойств объекта с целевыми DOM-элементами. Скажем, когда Knockout видит привязку текста, он устанавливает свойство innerText (используя Internet Explorer) или эквивалентное свойство в других браузерах. Когда используется привязка текста, любой предыдущий текст перезаписывается.
Некоторые из наиболее часто применяемых встроенных привязок для отображения данных перечислены в табл. 1. Полный список вы найдете в онлайновой документации Knockout — в панели навигации слева (bit.ly/ajRyPj).

Табл. 1. Распространенные привязки, встроенные в Knockout

Пример Описание
text: model Связывает свойство (модель) со значением text целевого элемента. Часто используется в элементах только для чтения, например в span
visible: isInStock Связывает значение свойства (isInStock) с видимостью целевого элемента. Значение свойства оценивается как true или false
value: price Связывает значение свойства (price) с целевым элементом. Часть применяется с элементами input, select и textarea
css: className Связывает значение свойства (className) с целевым элементом. Часть применяется для установки или переключения имен класса css для DOM-элементов
checked: isInCart Связывает значение свойства (isInCart) с целевым элементом. Используется для элементов checkbox
click: saveData Добавляет обработчик событий для связанной JavaScript-функции (saveData), когда пользователь щелкает DOM-элемент. Работает с любым DOM-элементом, но чаще используется для элементов button, input и a
attr: {src: photoUrl, alt: name} Связывает любой указанный для DOM-элемента атрибут с объектом-источником. Часто применяется, когда другие встроенные привязки не годятся, например для атрибута src тега img

Наблюдаемые массивы

Теперь, когда предыдущие примеры позволили вам слегка прочувствовать Knockout, пора перейти к более практичному и в то же время фундаментальному примеру с иерархическими данными. Knockout поддерживает много типов привязок, в том числе привязку к простым свойствам (как было показано в предыдущих примерах) и к массивам JavaScript, а также вычисляемые привязки (computed bindings) и пользовательские (custom bindings) (которые мы обсудим в будущей статье по Knockout). Следующий пример демонстрирует, как связать массив объектов product со списком, используя Knockout (рис. 3).

Рис. 3. Связывание с наблюдаемым массивом

Связывание с наблюдаемым массивом

При работе с графами объектов и связыванием с данными, полезно инкапсулировать все данные и функции, необходимые странице, в один объект. Такой объект часто называют ViewModel из шаблона MVVM. В данном примере представлением (View) является HTML-страница и ее DOM-элементы. Model — это массив объектов product. ViewModel «склеивает» Model и View; в качестве клея используется Knockout.

Массив объектов product задается с использованием функции observableArray. Это похоже на ObservableCollection в XAML-технологиях. Поскольку свойство products является observableArray, всякий раз, когда в массив добавляется какой-нибудь элемент или удаляется из него, целевые элементы уведомляются, и соответствующий элемент добавляется или удаляется в DOM:

var showroomViewModel = {
  products: ko.observableArray()
};

Корневым объектом является showroomViewModel, который будет связан с целевыми элементами. Он содержит список продуктов, получаемый от сервиса данных в формате JSON. Функция, которая загружает этот список, — showroomViewModel.load. Она показана на рис. 4 наряду с остальным JavaScript-кодом, настраивающим объект showroomViewModel (полный исходный код и образцы данных для этого примера содержатся в 03-observableArrays.html). Функция load перебирает в цикле данные product и использует функцию Product для создания новых объектов product до передачи их в observableArray.

Рис. 4. Определение связываемых данных

var photoPath = "/images/";
function Product () {
  this.id = ko.observable();
  this.salePrice = ko.observable();
  this.listPrice = ko.observable();
  this.rating = ko.observable();
  this.photo = ko.observable();
  this.itemNumber = ko.observable();
  this.description = ko.observable();
  this.photoUrl = ko.computed(function () {
    return photoPath + this.photo();
  }, this);
};
var showroomViewModel = {
  products: ko.observableArray()
};
showroomViewModel.load = function () {
  this.products([]); // reset
  $.each(data.Products, function (i, p) {
    showroomViewModel.products.push(new Product()
      .id(p.Id)
      .salePrice(p.SalePrice)
      .listPrice(p.ListPrice)
      .rating(p.Rating)
      .photo(p.Photo)
      .itemNumber(p.ItemNumber)
      .description(p.Description)
      );
  });
};
ko.applyBindings(showroomViewModel);

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

Функция Product определяет все свои свойства как наблюдаемые с помощью Knockout (кроме photoUrl). Когда Knockout связывает данные, к свойству-массиву products можно обращаться, и это упрощает использование стандартных функций, например length для отображения того, сколько элементов связано с данными в настоящее время:

<span data-bind="text: products().length"></span>

Управляющие привязки

Далее массив products можно связать с DOM-элементом, который будет выступать в роли анонимного шаблона для отображения списка. В следующем HTML-коде показан тег ul, который использует управляющую привязку (control-of-flow binding) foreach для связывания с products:

<ul data-bind="foreach:products">
  <li class="guitarListCompact">
    <div class="photoContainer">
      <img data-bind="visible: photoUrl, attr: { src: photoUrl }" 
        class="photoThumbnail"></img>
    </div>
    <div data-bind="text: salePrice"></div>
  </li>
</ul>

Элементы внутри тега ul будут использоваться как шаблоны каждого product. Внутри foreach контекст данных также сменяется с корневого объекта showroomViewModel на каждый индивидуальный product. Вот почему вложенные DOM-элементы могут быть напрямую связаны со свойствами photoUrl и salePrice объекта product.

Существует четыре основных управляющих привязки: foreach, if, ifnot и with. Эти привязки позволяют декларативно определять логику управления без создания именованного шаблона. Если условие в управляющей привязке if равно true которое или условие в управляющей привязке ifnot оценивается как false, то содержимое их блоков связывается и отображается:

<div data-bind="if:onSale">
  <span data-bind="text: salePrice"></span>
</div>

Привязка with переключает контекст данных на любой указанный вами объект. Это особенно полезно при переборе объектных графов со множеством отношений «предок-потомок» или отдельных ViewModel в странице. Например, если у вас есть ViewModel-объект sale, связанный со страницей, и у него имеются дочерние объекты customer и salesPerson, то привязку with можно было бы использовать для упрощения восприятия кода связывания с данными:

<div data-bind="with:customer">
  <span data-bind="text: name"></span><br/>
  <span data-bind="text: orderTotal"></span>
</div>
<div data-bind="with:salesPerson">
  <span data-bind="text: employeeNum"></span><br/>
  <span data-bind="text: name"></span>
</div>

Вычисляемые наблюдаемые объекты

Вероятно, вы заметили, что функция Product определяет photoUrl как особый тип вычисляемого свойства (computed property); ko.computed определяет функцию связывания (binding function), которая оценивает значение для операции связывания с данными. Вычисляемое свойство автоматически обновляется, когда любой из наблюдаемых объектов, от которых зависит оценка его значения, изменяется. Это особенно полезно, когда значение не представлено конкретной величиной в объекте-источнике. Кроме создания URL, другой распространенный пример — создание свойства fullName из свойств firstName и lastName.

(Заметьте, что в предыдущих версиях Knockout вычисляемые свойства назывались dependentObservable. Оба варианта по-прежнему работают в Knockout 2.0.0, но я советую использовать более новую функцию computed.)

Вычисляемое свойство принимает функцию для оценки значения и объект, представляющий объект, к которому вы подключите привязку. Второй параметр очень важен, потому что объектные литералы в JavaScript не позволяют ссылаться на себя. На рис. 4 ключевое слово this (представляющее showroomViewModel) передается для того, чтобы функция зависимого свойства могла использовать его для получения свойства photo. Без этого функция photo оказалась бы неопределенной, и в процессе оценки не удалось бы получить ожидаемый URL:

this.photoUrl = ko.computed(function () {
  return photoPath +  photo();  // photo() will be undefined
});

Понимание фундаментальных свойств привязок

В первой статье этой новой рубрики мы начали со связывания с данными средствами JavaScript-библиотеки Knockout. Самое важное в Knockout — понять фундаментальные свойства привязок: observable, observableArray и computed. С помощью этих наблюдаемых объектов вы можете создавать надежные HTML-приложения, используя устоявшиеся шаблоны разделения обязанностей. Я также рассказал о наиболее распространенных типах встроенных привязок и продемонстрировал работу управляющих привязок. Однако Knockout поддерживает гораздо больший функционал. В следующий раз мы подробнее изучим встроенные привязки.

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

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

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