Knockout.js MVVM

Steve Smith

Knockout - это популярная JavaScript библиотека, которая упрощает создание сложных, основанных на данных пользовательских интерфейсов. Она может использоваться одна либо с другими библиотеками, например, jQuery. Его основной целью является связывание UI элементов с основной моделью данных, определенной как JavaScript объект, например, если в UI внесены изменения, обновляется и модель, и наоборот. Knockout упрощает использование паттерна Model-View-ViewModel (MVVM).

Начинаем работу с Knockout в ASP.NET Core

Knockout разворачивается как один JavaScript файл, так что его очень легко использовать с помощью bower. У вас уже есть bower и gulp, откройте bower.json в проекте ASP.NET Core и добавьте зависимость knockout:

     {
             "name": "KnockoutDemo",
             "private": true,
             "dependencies": {
                     "knockout" : "^3.3.0"
             },
             "exportsOverride": {
             }
     }

После этого вы можете вручную запустить bower, открыв Task Runner Explorer (в View ‣ Other Windows ‣ Task Runner Explorer), а затем под Tasks кликните правой клавишей мышки по bower и выберите Run. Результат должен быть таким:

../_images/bower-knockout.png

Теперь если вы взглянете на папку wwwroot, то увидите, что knockout установлен под папкой lib.

../_images/wwwroot-knockout.png

Мы рекомендуем, чтобы в среде production вы ссылались на knockout через Content Delivery Network, или CDN, поскольку это увеличивает шансы на то, что у пользователей уже будет кэшированная копия файла, и тогда вообще не будет необходимости его скачивать. Knockout доступен на нескольких CDN, включая Microsoft Ajax CDN:

http://ajax.aspnetcdn.com/ajax/knockout/knockout-3.3.0.js

Чтобы добавить Knockout на страницу, просто добавьте элемент <script>, ссылающийся на файл, откуда бы вы его ни хостили (с вашим приложением или через CDN):

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

Наблюдаемые элементы, ViewModels и простое связывание

Возможно, вы уже использовали JavaScript, чтобы управлять элементами на веб странице либо по прямому доступу к DOM, либо используя такую библиотеку, как jQuery. Обычно вы добиваетесь такого поведения, создавая код для прямой установки значения элементов в ответ на конкретные действия пользователей. С Knockout вместо этого мы используем декларативный подход, когда все элементы на странице связаны со свойствами объекта. Вы не пишете код для управления DOM элементами - пользовательские действия просто взаимодействуют с объектом ViewModel, а Knockout заботится о том, что элементы страницы синхронизированы.

Вот наш пример страницы. Здесь есть элемент <span> с атрибутом data-bind, который показывает, что текстовый контекст должен быть связан с authorName. Далее, в JavaScript блоке у переменной viewModel есть одно свойство, authorName, установленное на некоторое значение. Наконец, делается вызов ko.applyBindings, передавая эту переменную viewModel.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
     <html>
             <head>
                     <script type="text/javascript" src="lib/knockout/knockout.js"></script>
             </head>
             <body>
                     <h1>Some Article</h1>
                     <p>
                             By <span data-bind="text: authorName"></span>
                     </p>
                     <script type="text/javascript">
                             var viewModel = {
                                     authorName: 'Steve Smith'
                             };
                             ko.applyBindings(viewModel);
                     </script>
             </body>
     </html>

При просмотре в браузере контекст элемента <span> заменяется значением переменной viewModel:

../_images/simple-binding-screenshot.png

Теперь у нас есть одно простое связывание. Обратите внимание, что нигде в коде мы не использовали JavaScript, чтобы присвоить значение контексту span. Если мы хотим управлять ViewModel, мы можем добавить текстовое поле для ввода HTML и сделать связывание с этим значением:

<p>
        Author Name: <input type="text" data-bind="value: authorName" />
</p>

Перезагрузим страницу, мы увидим, что это значение связано с полем для ввода:

../_images/input-binding-screenshot.png

Однако если мы изменим значение текстового поля, соответствующее значение элемента <span> не изменится. Почему нет?

Суть в том, что ничто не указывало на то, что <span> должен быть обновлен. Простое обновление ViewModel само по себе не так значимо, пока свойства ViewModel связаны с конкретным типом. Нам нужно использовать observables во ViewModel для всех свойств, которые нужно автоматически обновлять, если происходят изменения. Если ViewModel будет использовать ko.observable("value") вместо “value”, тогда ViewModel обновит все HTML элементы, которые связаны с его значением, если произойдет изменение. Обратите внимание, что поля ввода не обновляют свои значения, пока не теряют фокус, так что вы не увидите изменения, если набираете текст.

Примечание

Чтобы добавить поддержку для мгновенного обновления после каждого нажатия клавиши, нужно просто добавить valueUpdate: "afterkeydown" в атрибут data-bind.

Наш viewModel после обновления использует ko.observable:

     var viewModel = {
             authorName: ko.observable('Steve Smith')
     };
     ko.applyBindings(viewModel);

Knockout поддерживает различные виды связывания. Мы уже видели, как делать связывание к text и value. А связывание можно делать к разным атрибутам. Например, чтобы создать гиперссылку с якорным тэгом, нужно связать атрибут src с viewModel. Knockout также поддерживает связывание с функциями. Чтобы вы увидели это, давайте обновим viewModel, чтобы он обрабатывал twitter автора и отображал это как ссылку на страницу автора в twitter. Мы сделаем это в три этапа.

Во-первых, добавляем HTML для отображения гиперссылки, которая будет показана в круглых скобках после имени автора:

     <h1>Some Article</h1>
     <p>
             By <span data-bind="text: authorName"></span>
             (<a data-bind="attr: { href: twitterUrl}, text: twitterAlias" ></a>)
     </p>

Далее, обновляем viewModel, чтобы он включал свойства twitterUrl и twitterAlias:

     var viewModel = {
             authorName: ko.observable('Steve Smith'),
             twitterAlias: ko.observable('@ardalis'),
             twitterUrl: ko.computed(function() {
                     return "https://twitter.com/";
             }, this)
     };
     ko.applyBindings(viewModel);

Обратите внимание, что на данном этапе мы еще не обновили twitterUrl, чтобы он содержал корректный URL – он просто указывает на twitter.com. Также обратите внимание, что для twitterUrl мы используем новую Knockout функцию, computed. Эта функция определяет все UI элементы, в которых произошли изменения. Чтобы у нее был доступ к свойствам viewModel, нам нужно по иному создать viewModel, чтобы у каждого свойства был свой оператор.

Обновленный viewModel представлен ниже. Теперь он объявлен как функция. Обратите внимание, что теперь у каждого свойства есть свой оператор, который заканчивается точкой с запятой. Также обратите внимание, что чтобы получить доступ к значению свойства twitterAlias, функция должна быть выполнена, так что ссылка включает в себя ().

     function viewModel() {
             this.authorName = ko.observable('Steve Smith');
             this.twitterAlias = ko.observable('@ardalis');

             this.twitterUrl = ko.computed(function() {
                     return "https://twitter.com/" + this.twitterAlias().replace('@','');
             }, this)
     };
     ko.applyBindings(viewModel);

Вот результат:

../_images/hyperlink-screenshot.png

Knockout также поддерживает связывание с конкретными событиями элементов UI. Это позволяет вам легко и просто связать UI элементы с функциями внутри viewModel. В качестве примера мы добавим кнопку, при нажатии которой twitterAlias будет прописан большими буквами.

Во-первых, мы добавляем кнопку, связанную с событием, а затем ссылаемся на имя функции, которую собираемся добавить к viewModel:

     <p>
             <button data-bind="click: capitalizeTwitterAlias">Capitalize</button>
     </p>

Далее, добавляем функцию к viewModel и меняем состояние viewModel. Чтобы присвоить новое значение свойству twitterAlias, мы вызываем его в качестве метода и передаем ему новое значение.

     function viewModel() {
             this.authorName = ko.observable('Steve Smith');
             this.twitterAlias = ko.observable('@ardalis');

             this.twitterUrl = ko.computed(function() {
                     return "https://twitter.com/" + this.twitterAlias().replace('@','');
             }, this);

             this.capitalizeTwitterAlias = function() {
                     var currentValue = this.twitterAlias();
                     this.twitterAlias(currentValue.toUpperCase());
             }
     };
     ko.applyBindings(viewModel);

Вот результат:

../_images/hyperlink-caps-screenshot.png

Поток работы

Knockout включает в себя связывание для условных операторов и циклов. Циклы особенно полезны для связывания списков данных со списками UI, меню и таблицами. Связывание foreach будет проходить циклом по массиву. При использовании с массивом observable оно автоматически обновит UI элементы, если элементы удаляются из массива или добавляются в него, не пересоздавая каждый элемент в UI дереве. В следующем примере используется новый viewModel, который включает в себя массив observable. Он связан с таблицей с двумя колонками с помощью связывания foreach для элемента <tbody>. Каждый элемент <tr> внутри <tbody> будет связан с элементом коллекции gameResults.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<h1>Record</h1>
<table>
        <thead>
                <tr>
                        <th>Opponent</th>
                        <th>Result</th>
                </tr>
        </thead>
        <tbody data-bind="foreach: gameResults">
                <tr>
                        <td data-bind="text:opponent"></td>
                        <td data-bind="text:result"></td>
                </tr>
        </tbody>
</table>
<script type="text/javascript">
        function GameResult(opponent, result) {
                var self = this;
                self.opponent = opponent;
                self.result = ko.observable(result);
        }

        function ViewModel() {
                var self = this;

                self.resultChoices = ["Win", "Loss", "Tie"];

                self.gameResults = ko.observableArray([
                        new GameResult("Brendan", self.resultChoices[0]),
                        new GameResult("Brendan", self.resultChoices[0]),
                        new GameResult("Michelle", self.resultChoices[1])
                ]);
        };
        ko.applyBindings(new ViewModel);
</script>

Обратите внимание, что сейчас мы используем ViewModel с большой “V”, поскольку он будет использовать “new” (при вызове applyBindings). Вы увидите такой результат после выполнения задачи:

../_images/record-screenshot.png

Чтобы вы увидели, что коллекция observable является рабочей, давайте добавим немного функционала. Мы можем включить туда возможность записывать результаты другой игры во ViewModel, а затем добавить кнопку и некоторый UI для работы с этим новым функционалом. Во-первых, давайте создадим метод addResult:

// add this to ViewModel()
self.addResult = function() {
        self.gameResults.push(new GameResult("", self.resultChoices[0]));
}

Связываем этот метод с кнопкой с помощью click:

<button data-bind="click: addResult">Add New Result</button>

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

../_images/record-addresult-screenshot.png

Есть несколько способов добавить новые записи в UI, обычно либо поочередно, либо в отдельной форме. Мы можем просто изменить таблицу, чтобы в ней использовались текстовые поля и выпадающие списки и чтобы все это можно было редактировать. Просто измените элемент <tr>:

<tbody data-bind="foreach: gameResults">
        <tr>
                <td><input data-bind="value:opponent" /></td>
                <td><select data-bind="options: $root.resultChoices,
                        value:result, optionsText: $data"></select></td>
        </tr>
</tbody>

Обратите внимание, что $root ссылается на корневую директорию ViewModel. $data ссылается на определенный контекст - в данном случае он ссылается на конкретный элемент массива resultChoices, каждый из которых является простой строкой.

Вот результат:

../_images/editable-grid-screenshot.png

Если бы мы не использовали Knockout, то могли бы сделать все это с помощью jQuery, но это было бы не столь эффективно. Knockout отслеживает, какие связанные элементы данных во ViewModel соответствуют нужным UI элементам, и обновляет те элементы, которые должны быть добавлены, удалены или обновлены. Нам потребовалось бы больше усилий, если бы мы использовали jQuery или напрямую работали с DOM, и даже если бы мы захотели отобразить полученный результат, основываясь на табличных данных, нам потребовался бы еще один цикл и парсинг HTML элементов. С Knockout отображение элементов является довольно простым делом. Мы может использовать подсчет внутри самого элемента ViewModel, а затем отображать его с помощью простой текстовой связки и <span>.

Обратите внимание, что ссылки на свойства внутри ViewModel должны быть вызовами функций, oиначе они не получат значение observable (например, gameResults(), а не gameResults в показанном коде):

self.displayRecord = ko.computed(function () {
        var wins = self.gameResults().filter(function (value) { return value.result() == "Win"; }).length;
        var losses = self.gameResults().filter(function (value) { return value.result() == "Loss"; }).length;
        var ties = self.gameResults().filter(function (value) { return value.result() == "Tie"; }).length;
        return wins + " - " + losses + " - " + ties;
}, this);

Свяжите эту функцию со span внутри элемента <h1> вверху страницы:

<h1>Record <span data-bind="text: displayRecord"></span></h1>

Результат:

../_images/record-winloss-screenshot.png

Добавление строк или изменение выбранных элементов приводит к тому, что во всех строках колонки Result будет обновлена запись, показанная вверху окна.

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

<div data-bind="visible: customerValue > 100"></div>

Этот <div> будет виден только тогда, когда customerValue больше 100.

Шаблоны

Knockout поддерживает шаблоны, так что вы можете легко разделить UI от поведения или по требованию загружать UI элементы в большое приложение. Мы можем обновить наш предыдущий пример, чтобы сделать для каждой строки собственный шаблон, просто добавив в шаблон HTML и указав имя шаблона при вызове <tbody>.

<tbody data-bind="template: { name: 'rowTemplate', foreach: gameResults }">
</tbody>
<script type="text/html" id="rowTemplate">
        <tr>
                <td><input data-bind="value:opponent" /></td>
                <td><select data-bind="options: $root.resultChoices,
                        value:result, optionsText: $data"></select></td>
        </tr>
</script>

Knockout также поддерживает другие движки шаблонов, например, библиотеку jQuery.tmpl и движок Underscore.js.

Компоненты

Компоненты позволяют организовывать и заново использовать UI код, обычно наряду с данными ViewModel, от которых зависит UI код. Чтобы создать компонент, вам нужно просто указать его шаблон и его viewModel, а также назвать его. Это делается с помощью вызова ko.components.register(). Кроме того, шаблон и viewmodel можно загрузить из внешних файлов, используя такие библиотеки, как require.js, что приводит к чистому и эффективному коду.

Работа с API

Knockout работает с любыми данными в формате JSON. Обычно мы получаем и сохраняем данные, используя Knockout, с помощью jQuery, который поддерживает функцию $.getJSON(), чтобы получать данные, и метод $.post(), чтобы отправлять данные из браузера в API. Конечно, если вы предпочитаете другой способ отправки и получения JSON данных, Knockout также будет с этим работать.

Резюме

Knockout предоставляет простой, элегантный способ связывать UI элементы с текущим состоянием клиентского приложения, определенного во ViewModel. Синтаксис Knockout использует атрибут data-bind, применяемый к обрабатываемым HTML элементам. Knockout может эффективно отображать и обновлять большие наборы данных, отслеживая UI элементы и изменения. В больших приложениях можно использовать шаблоны и компоненты, которые можно загружать из внешних файлов.

Поделись хорошей новостью с друзьями!
Следи за новостями!