0,0
рейтинг
7 января 2013 в 19:41

Разработка → Применение, советы и особенности knockout.js

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

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

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


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

    Верстка:
    <div>
        <span data-bind=”text: attribute1”></span>
        <span data-bind=”text: attribute2”></span>
    </div>
    

    Привязка:
    ko.applyBindings( ViewModel );
    

    Результат:
    <div>
        <span data-bind=”text: attribute1”>Hello</span>
        <span data-bind=”text: attribute2”>world!</span>
    </div>
    

    Но такая привязка чаще всего не используется, ведь при изменении модели не изменяется DOM и наоборот.
  2. Создавать двустороннюю наблюдаемую (observable) привязку интерфейса и модели, т.е. в реальном времени будет обновляться интерфейс при изменении модели, а модель при изменении в интерфейсе (рабочий пример при вводе текста в формах). Тут есть один нюанс, в input полях обновление модели произойдёт только при событии onblur (убрать фокус с элемента), данную ситуацию можно исправить, подписавшись на событие input, соответственно вручную обновлять модель. Пример на jsfiddle. Пользователь m_z предложил более удобный вариант обновления модели с использованием valueUpdate: 'input' (Пример).
  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 внутри — её значение. Как эту проблему обойти — навскидку не придумал. Но пользователь lega предложил использовать вариант с созданием объекта-обёртки (пример), если у вас будет несколько полей, то такой метод оправдан и будет работать.

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


Получать значение из 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
карма
28,7
рейтинг 0,0
JavaScript developer.
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Разработка

Комментарии (27)

  • +4
    Статья получилась очень отрывчатая…
    • +1
      Да, верно, но это первый опыт, надеюсь, дальше получше пойдёт.
  • +1
    Ждем продолжения. knockoutjs интересен.
  • +1
    Как эту проблему обойти — навскидку не придумал.

    Можно «завернуть» элемент jsfiddle.net/RJ7sr/1/

    Именно эту функцию я использую для обновления модели в одном проекте

    Т.к. у нас тут knockout.js то по хорошему нужно сделать биндинг к редактору (ko.bindingHandlers.redactor) — использовать будет удобней:
    <textarea data-bind="redactor: content_value"></textarea>
    • 0
      Можно «завернуть» элемент jsfiddle.net/RJ7sr/1/

      Вы правы, можно и так, но это дополнительный объект, который может быть оправдан если полей будет несколько. Добавил в статью.
      нужно сделать биндинг к редактору

      Да, так можно сделать, но только в том случае, если известный textarea будет контейнером редактора, а если область для редактирования создаётся динамически, то всё равно придётся писать плагин или править шаблон в исходниках редактора.
  • +1
    Всем нравится knockout, но я так и не нашел примеров организации кода, и особенно таких частей, как синхронизация с REST-сервером, чтобы не изобретать велосипед и стыдно не было. Может кто что подскажет?
    P.S. курс от John Papa видел.
    • +1
      А нокаут и не предоставляет никаких методов для работы с сервером, разве что работа с json, просто задача библиотеки другая. И как мне кажется, не собирается. В одном проекте у меня вобще написан отдельный модуль для работы с запросами к серверу, получением данных (+обёртка в наблюдаемые) и кэшированием. Но так же можно использовать knockback, хотя это уже некоторое нагромождение.
  • +1
    Создавать двустороннюю наблюдаемую (observable) привязку интерфейса и модели, т.е. в реальном времени будет обновляться интерфейс при изменении модели, а модель при изменении в интерфейсе (рабочий пример при вводе текста в формах). Тут есть один нюанс, в input полях обновление модели произойдёт только при событии onblur (убрать фокус с элемента), данную ситуацию можно исправить, подписавшись на событие input, соответственно вручную обновлять модель. Пример на jsfiddle.

    можно сделать при помощи установки valueUpdate как «afterkeydown»
    • 0
      + не думаю, что это хорошо, если view model занимается обработкой событий инпутов и тд (т.к. жестко привязывается ко вьюхе), для этого есть механизмы экстендеров, биндинг хандлеров.
      • 0
        Чтобы сделать качественный интерфейс, придётся реагировать не только на обновления модели, но и на другие события из DOM. В данный момент мне кажется, что привязка обработчиков через биндинг — это нормально, возможно я не прав или не имею достаточного опыта, можете привести пример вашей реализации?
      • 0
        А чем это плохо? Где тогда лучше обрабатывать?
        • 0
          Получается спагетти-код. Например, в биндинг хандлерах.

          Допустим, есть какой-нибудь компонент или jquery-плагин, его можно красиво обернуть в биндинг хандлер и декларативно описывать вьюхи таким образом:

          <textarea class="tinymce" data-bind="tinymce: description, tinmymceoptions: { fontsize: 14 }}"></textarea>

          Во вью-модели будет только логика и данные, в биндинг-хандлере будет инициалиция компонета/плагина и обработка изменений вьюмодели.
          • 0
            Спасибо!
            У любой медали 2 стороны. То что Вы описали, действительно хорошо, но только тогда, когда необходимо повторное использование и когда кода много настолько, что можно получить спагетти. В противном случае, можно и не вводить к вью и вью модели еще и биндинг хандлер, чтобы не усложнять (по поводу связанности вью и вью модели — вряд ли это так страшно).
    • +1
      Можно, но событие input будет срабатывать даже при вставке текста через Ctrl + V и через обычный drag-n-drop.
      • +1
        Можно:
        valueUpdate: 'input'
        
        • 0
          Как это круто, спасибо, не подумал, что можно к любым событиям привязываться таким образом (видать в документации пропустил этот момент). Добавлю это в статью.
  • +1
    Автор, почему в первом примере для привязки используется html если поля во viewModel обычный текст:
    <span data-bind=”html: attribute1”>Hello</span>

    к то муже после привязки они еще магическим образом превращаются в value:
    <span data-bind=”value: attribute1”>Hello</span>

    хотя там самое место быть бы text.

    Мне кажется Вы вводите новичков в заблуждение касаемо использования bindings имеющихся по умолчанию в knockout. Либо поясните пожалуйста как так получается.
    • 0
      Вы абсолютно правы, исправил этот момент в статье. Спасибо за замечание.
  • 0
    Тут есть один нюанс, в input полях обновление модели произойдёт только при событии onblur (убрать фокус с элемента)…


    Что бы было в «реальном времени» разработчики советуют использовать valueUpdate, а в него можно передать: keyup, keypress, afterkeydown… Лучшем выбором будет afterkeydown.

    документация
    • 0
      В комментариях выше мы уже разобрали этот момент.
  • +1
    Интересно, спасибо :) В будущих статьях хотелось бы почитать как вы делите нокаут-код на модули (чтобы на разных страницах использовать различные их комбинации). И ещё, возможно, вы сталкивались и решили как-то проблему пред-заполнения моделей из отрендеренного на сервере html.
    • 0
      Что вы имеете в виду под модульностью? Возможность использовать несколько моделей на одной странице, использование разделяемой модели (когда одна модель и несколько шаблонов)? Или просто приведите пару примеров.

      С задачей заполнения моделей из уже готового html не сталкивался. И насколько я знаю, декларативная модель нокаута это и не собирается поддерживать. Полагаю, что лучше получить данные с сервера и поместить их в DOM уже на клиенте. Можно пример, в каких условиях вам это понадобилось?
      • +1
        И первое и второе, видимо. Допустим есть у вас на отдельной странице фотки пользователя. И хочется часть фоток показать на странице профиля. Получается, что у модели фоток будет несколько шаблонов (разные для разных страниц). И на странице профиля будет кроме модели фоток ещё своя модель.

        Заполнение моделей из html нужно для поисковой оптимизации. Из за этого приходится рендерить страницы на сервере. И сейчас просто встраиваем в страницу json для нокаута. Но это увеличивает размер страниц примерно на треть.
        • 0
          Нокаут очень хорош для одностраничных приложений (можно иметь одну большую разделяемую модель с данными — этакое хранилище и несколько маленьких для специфических шаблонов), но если реальных страниц несколько, можно выносить используемые модели данных в отдельные файлы. Тут еще зависит от конкретного случая, путей решения может быть несколько. Иногда лучше расширять модель, а иногда иметь несколько не связанных.

          А поисковики же научились исполнять js и индексировать ajax запросы, есть даже официальные туториалы от яндекса и гугла и много статей на хабре. Может я не знаю каких-то особенностей, но если вам это не подходит, то можно для поисковиков отдавать статическую страницу, а для браузеров нормальную. Или после загрузки страницы запрашивать json (или парсить страницу) и заменять статические данные — шаблонами, т.е. рендерить заново, но только уже нокаутом.
  • +1
    Вопрос. Очень заинтересовался этой библиотекой. К сожалению углублённое изучение и использование просто умирает. Посоветуйте, пожалуйста, что-нибудь из гридов для использования с этой библиотекой. Перерыл весь гугль — старые посты с невнятными, отрывочными и неполными решениями и «рекомендациями с коленки». Как в анекдоте — берём, а потом «долго пилим напильником»

    На данный момент нашёл:
    — datatables — проблема связки не решена, есть глюки, которые, возможно, решатся в будущем релизе.
    — «нативная» koGrid — отличный грид, но более полугода, кажется, не обновлялся и… глюк за глюком… Мне так и не удалось сделать элементарный autosize viewport'a. Надо курить css, что в отсутствии документации более чем времязатратно.
    — kendo — вроде там всё красиво, но платить сотни долларов для своих бесплатных, но полезных людям проектов, я не готов. Смысл?

    Если можете посоветовать что-то, что просто сочетается с knockout.js — я был бы очень благодарен ибо сама библиотека просто великолепна. Переписал один из своих проектов просто для изучения — код уменьшился втрое и стал более понятен и нагляден.

    Да и вообще, не только гриды. Любые библиотеки, которые бесплатны но легко сочетаются с konockout были бы интересны. Понятно что всё это подбирается под задачу, но всегда лучше знать что что-то есть, чем как я — просто убить неделю на лазание по гуглям. Просто что-то из используемого или знакомого — специально искать не надо.

    Жалко своего времени. Я могу, конечно, допилить koGrid до нужного уровня, но может быть есть что-то уже готовое и бесплатное?

    Буду «настоятельно» благодарен за советы.
    • 0
      Готовых хороших гридов я не искал, реализовываю всё сам, под конкретную задачу. Мне кажется, что если хотите готовых контролов, лучше посмотреть на ангуляр. Но согласен с вами, что под нокаут мало качественных готовых контролов, приходится писать всё самому.
      • 0
        Спасибо, посмотрел. Подход knockout мне понравился больше, но тоже, очевидно, мощная штука. Иногда у меня опускаются руки :) Вокруг такое обилие интереснейших и удобнейших технологий, библиотек, паттернов, что иногда просто опускаются руки. За всем не уследишь, а так жаль. Столько вокруг интересного, где бы купить пару сотен лет жизни? :)

        Если что-то придёт в голову в сторону knockout — я буду очень благодарен. Я знаю где и как её применить в большом проекте, что почти сразу облегчит жизнь массе людей.

        К сожалению гриды это как раз то, что лень писать самому. Даже не лень, а жалко своего времени на очередной велосипед. Буду пилить kogrid. В работу такое не пустишь, а для одного из моих проектиков поиска по онлайн магазинам настольных игр — то что надо.

        Спасибо за ответ

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.