Применение, советы и особенности knockout.js

в 15:41, , рубрики: javascript, knockout, Knockout.js, knockoutjs, mvvm, Веб-разработка, метки: , , , ,

О библиотеке knockout.js на хабре написано не так много, но кое что есть (и конечно же есть официальный туториал и другие материалы на оф. сайте и хороший ресурс на англ. языке knockmeout.net, статьи которого смогу перевести, если будет спрос). Данная статья возможно перерастёт в цикл статей по javascript и нокауту, если нло не похитит меня.

Изначально готовил материал для людей, уже знакомых с нокаутом и mvvm, но в комментариях к другим статьям меня попросили рассказать как готовить нокаут для чайников. Предполагаю, что вы уже прочитали предыдущие статьи о нокауте на хабре. Поехали!

Зачем нужен Knockout.js?

  1. Иметь возможность легко наполнять интерфейс данными из json-подобного объекта (модели):
    var ViewModel = {
        attribute1: ”Hello”,
        attribute2: ”world!”,
    };
    

    Верстка:

    <div>
        <span data-bind=”html: attribute1”></span>
        <span data-bind=”html: attribute2”></span>
    </div>
    

    Привязка:

    ko.applyBindings( ViewModel );
    

    Результат:

    <div>
        <span data-bind=”value: attribute1”>Hello</span>
        <span data-bind=”value: attribute2”>world!</span>
    </div>
    

    Но такая привязка чаще всего не используется, ведь при изменении модели не изменяется DOM и наоборот.

  2. Создавать двустороннюю наблюдаемую (observable) привязку интерфейса и модели, т.е. в реальном времени будет обновляться интерфейс при изменении модели, а модель при изменении в интерфейсе (рабочий пример при вводе текста в формах). Тут есть один нюанс, в input полях обновление модели произойдёт только при событии onblur (убрать фокус с элемента), данную ситуацию можно исправить, подписавшись на событие input, соответственно вручную обновлять модель. Пример на jsfiddle.
  3. Дополнительно подписываться на изменения модели. Код из документации:
    myViewModel.personName.subscribe(function(newValue) {
        alert("Новое имя: " + newValue);
    });
    

    Отписываемся от изменений:

    var subscription = myViewModel.personName.subscribe(function(newValue) { /* что-то делаем */ });
    // ...чуть позже...
    subscription.dispose(); // Отменяем подписку.
    

Что такое атрибут data-bind и bindings?

Этот атрибут сильно похож на json-объект, он позволяет нам привязать данные и обработчики событий к текущему DOM-узлу. Knockout.js парсит этот атрибут и выполняет JavaScript выражения (подробно описано в доках). Эта конструкция запускается в контексте переданной модели данных. Соответственно и синтаксис атрибута должен быть корректным с точки зрения javascript-кода.

Knockout.js предлагает набор стандартных привязок (bindings), которые позволяют редактировать стили, контент, обработчики и прочее, создавая магические вещи. Кроме того, можно писать свои привязки, про это отлично рассказано опять же в доках.

На этом закончим описание того, зачем же нужен нокаут, всё остальное вы сами знаете на каком сайте найти и перейдём к некоторым не очевидным новичку вещам.

Работа с observableArray

Допустим, вам надо динамически изменять массив, на который подписаны какие-то обработчики (или интерфейс). Например, вы получили новую партию данных с сервера и в цикле добавляете их в массив.
Если сделать все в лоб — то обработчик изменения отработает столько раз, сколько элементов вы добавите. И это скажется на производительности. Пример взят из knockmeout.net

Так делать не надо:

var items = ko.observableArray([]);

for (var i = 0, j = newData.length; i < j; i++) {
    items.push( newData[i] );
}

Гораздо лучше так:

var items = ko.observableArray([]);
// Получим чистый массив элементов.
var underlyingArray = items();
for (var i = 0, j = newData.length; i < j; i++) {
    // Добавляем новые элементы как обычно.
    underlyingArray.push( newData[i] );
}
// Говорим, что наш массив изменился и стоит обновить интерфейс и вызвать обработчики.
self.items.valueHasMutated();

Полный пример на jsfiddle.

В комментариях на stackoverflow даже рекомендуют такую функцию:

ko.observableArray.fn.pushAll = function(valuesToPush) {
    var underlyingArray = this();
    this.valueWillMutate();
    ko.utils.arrayPushAll(underlyingArray, valuesToPush);
    this.valueHasMutated();
    return this;
};

// Применяем
this.users.pushAll(dataFromServer);

Шаблоны

Knockout.js нужен в проекте прежде всего для наполнения интерфейса данными. Самый распространенный сценарий для этого — получение AJAX-запросом JSON-данных с сервера и рендеринг, т.е. обновление контента без перезагрузки страницы.
Для этого нам нужны фрагменты страницы, которые мы сможем подменять в интерфейсе и наполнять данными — т.е. нам нужны шаблоны для различных данных. Knockout.js умеет работать с различными видами шаблонов (jQuery.tmpl, underscore, нативный) — остановимся на последнем (хотя нам даже позволительно написать свой).

Не нативные шаблонизаторы не поддерживают использование некоторых привязок внутри него (в частности, очень приятный with, который сменяет контекст модели для шаблона).

Поэтому в некоторых случаях хочется использовать нативный шаблонизатор от knockout. Для этого в опциях надо передать templateEngine: ko.nativeTemplateEngine.instance. Что интересно (и не сказано в документации) — этот параметр можно передавать и в привязке template.

Для программного рендеринга шаблонов используется команда:

ko.renderTemplate(templateName, viewModel, options, domNodeToRender);

Эту команду удобно вызывать, например, в методе _create() при создании jquery-виджетов, которые используют шаблоны, и делается это так:

ko.renderTemplate('templateName', this, {
    templateEngine: ko.nativeTemplateEngine.instance
}, this.element.get(0) );

Так же для вызова шаблона из другого шаблона используется стандартная привязка template:

<ul data-bind="template: {
    name: templateName,
    data: viewModel,
    templateEngine: ko.nativeTemplateEngine.instance
}"></ul>

templateName — имя шаблона, на практике это id блока, в котором находится верстка с шаблоном:

<script type="text/html" id="templateName">
   <h3 data-bind="text: name"></h3>
   <p>Credits: <span data-bind="text: credits"></span></p>
</script>

При этом движок шаблона не “наследуется” при вызове вложенных шаблонов — если мы вызываем шаблон из шаблона, обрабатываемого нативным шаблонизатором, то внутренний шаблон (если не указать шаблонизатор принудительно) все равно использует дефолтный шаблонизатор (например, jQuery.tmpl, если такой был подключен в проекте).
Поэтому движок надо указывать каждый раз. Но эта особенность позволяет нам одновременно использовать разные шаблонизаторы для вложенных шаблонов.

Шаблоны без корневого элемента

Иногда не хочется создавать лишний контейнер для содержимого шаблона, чтобы не получилось, что в него вложен всего один элемент. В самом шаблоне при этом элемент может быть необходим из-за привязок или стилей.
В таком случае подходит стандартный для нокаута вариант (виртуальные элементы):

<!-- ko template: {
    name: 'viewReportTemplate',
    templateEngine: ko.nativeTemplateEngine.instance,
    data: $root
} --><!-- /ko -->

Хотя в документации он и не описан.

Отладка в шаблонах

Самая частая операция при программировании — отладка (хотя некоторые могут поспорить). Способ ниже позволит вам выводить сообщения в консоль используя оператор запятая и оператор группировки (подробнее про эти операторы).

<div data-bind="html: ( console.log( details ), details )"></div>

Смена контекста при помощи with и foreach — подводный камень

Предположим, что у нас есть массив, в котором хранятся строки, которые мы хотим отслеживать.
Выглядит инициализация массива примерно так:

var obArray = ko.observableArray([
    ko.observable(“Task One”),
    ko.observable(“Task Two”),
    ko.observable(“Task Three”)
]);

И мы хотим вывести форму, состоящую из input для её редактирования как-то вот так:

<div data-bind=”foreach: obArray”>
    <input type=”text” data-bind=”value: $data”/>
</div>

В такой записи текстовые поля не будут работать в две стороны — изменения текста в поле не будет влиять на элементы массива, но изменение элементов массива будет влиять на содержимое текстовых полей. Пример на jsfiddle.

Причина в том, что шаблон выше по сути своей эквивалентен следующему:

<div data-bind=”foreach: obArray”>
    <!-- ko with: $data -->
        <input type=”text” data-bind=”value: $data”/>
    <!-- /ko -->
</div>

with: $data — здесь происходит “распаковка” $data: unwrapObservable( $data ) для того, чтобы обращения вида $data.somefield внутри шаблона работали. Но эта распаковка подставляет вместо $data внутри — её значение. Как эту проблему обойти — навскидку не придумал.

Работа с текстовыми редакторами

Получать значение из input используя одноимённое событие в реальном времени мы уже научились, а как быть с редакторами где контент — это html элементы? Воспользуемся MutationObserver и т.к. этот api работает не везде, подстрахуемся другими DOM событиями и напишем не совсем кроссбраузерный (современные браузеры работают нормально) код, который можно встроить в любимый редактор:

// Допустим это наш контейнер редактора.
var $editor = $( '#editor_wrapper' );

// Инициализировать наблюдатель активности написания текста.
function initActivityObserver( callback ){
   // Получим ссылку на Mutation интерфейс с учётом браузера.
   var MutationObserver = window.MutationObserver ||
                          window.WebKitMutationObserver ||
                          window.MozMutationObserver,
        // Переменная для очистки таймаута.
        timeoutId,
        delay = 1000;

   // Обработка события изменения содержимого в области редактора.
   function handler( mutations ){
        // Если изменения непрерывные - очистим таймаут
        if ( timeoutId ) clearTimeout( timeoutId );
   
        // Колбэк вызывается по окончанию активности пользователя.
        timeoutId = setTimeout( callback.bind( window ), delay );
    }

    if ( MutationObserver ){
        // Инстанс Mutation Observers
        var observer = new MutationObserver( handler );

        // Привяжем наблюдатель к редактору по нужным нам изменениям
        observer.observe( $editor.get(0), {
            attributes: true,
            childList: true,
            characterData: true,
            subtree: true
        });

    // Не поддерживаем мутации (например ie9+)
    } else {
        $editor.on('DOMAttrModified DOMCharacterDataModified DOMNodeInserted DOMNodeRemoved DOMSubtreeModified', function(e){
            // Если изменения непрерывные - очистим таймаут
            if ( timeoutId ) clearTimeout( timeoutId );

            // Колбэк вызывается по окончанию активности пользователя
            timeoutId = setTimeout( callback.bind( window ), delay );
        });
    }
}

Именно эту функцию я использую для обновления модели в одном проекте, который использует неплохой редактор от imperavi, если кому понадобится этот код в виде плагина, смогу написать и выложить на гитхаб.

Заключение

Статья получилась нормальных размеров, а материал еще остался — работа с деревьями, написание собственных привязок, jQuery виджетов с нокаутом и некоторые другие хитрости и примеры. Голосуйте зарядом, комментируйте, указывайте на ошибки.

По ошибкам в орфографии и пунктуации прошу в личку.

Автор: arXangel

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js