Pull to refresh

Knockout, практический опыт использования

Reading time 12 min
Views 70K
Некоторое время назад я обещал рассказать о нашем опыте работы с Knockout. Мы используем данную библиотеку в одном из проектов в течение последних 4 месяцев. Это немного, но за это время команда набрала некоторый опыт, который, я думаю, может быть интересен читателям.

Но для начала совсем чуть-чуть теории.

MVC, MVP и MVVM.



Если с первыми двумя шаблонами большинство более-менее знакомо, о о последнем слышали очень немногие: шаблон MVVM появился в Microsoft и получил распространение в среде .net-разработчиков, но и среди них он не так широко известен.

В чем весь сыр-бор? MVC прижился в вэбе во многом потому, что он отлично ложится на сценарий запрос-ответ. Пришел запрос, он передался на соответствующий контроллер, тот инициировал какие-то процессы в модели, а затем создается View. Обычно это темплейт, который заполняется данными из модели. Затем полученный маркап передается клиенту в браузер.



Основная черта такого сценария — краткое время жизни View — он создается на каждый запрос, просто заполняется данными в один проход — и все.

Когда появился Ajax, а вслед за ним single-page applications, оказалось, что Представление живет долго, и следовательно простым темплейтом уже не обойтись. Увеличенное время жизни означает усложнение того, что принято называть lifecycle, и следовательно — появления логики на View.

Естественно, никто этого не хочет, все хотят — чтобы логика приложения оказывалась в коде, а не в маркапе. Так ее легче содержать, легче тестировать и изменять. Что в таких случаях делают?

Идея 1: Enter MVP. Логику представления выносят в особый слой и объединяют с контроллером в Презентер. Задача презентера — отделить модель от представления, отвечать за передачу данных между ними и содержать в себе логику изменения представления.



Презентер — посредник. Он по идее мог бы быть stateful или stateless. Однако на деле в нем состояние есть всегда — это состояние вида. Альтернативой было бы содержать его в модели, но этого, естественно, тоже никто не хочет. Поэтому примем, что в Презентере состояние есть.

Если посмотрим на MVP применительно к web, то наш Презентер мог бы быть объектом в JavaScript с полями состояния. Как оно меняется через HTML и взаимодействие с пользователем? — Через события DOM. Т.е. наш Презентер — это объект с полями и event listener’ами для связи с View.

Идея 2: Для связи презентера с моделью также можно использовать эвенты. Т.е. всякий раз, когда в полях объекта-презентера происходит изменение, запускается соответствующий эвент. Обработчики событий вызывают бизнес-методы в модели, и результат также отражается в виде изменений значений других полей презентера. Опять вызываются события, но на этот раз это могут быть обработчики представления, например. Тогда изменения в модели отражаются в интерфейсе пользователя. Вот такой вот event-driven workflow.

Идея 3: Теперь если инфраструктуру обработки сообщений вынести из презентера, спрятать в отдельный фреймворк, то сам объект презентер превращается в простой объект с полями. Меняя значения полей, мы инициируем какие-то процессы в модели или в представлении. Такой простой объект получил название ViewModel — он с точки зрения нашей бизнес-логики моделирует интерфейс пользователя, не вдаваясь при этом в подробности.



В .net паттерн прижился во многом благодаря тому, что для лейаута UI компонентов они стали использовать XAML — маркап, в котором привязку элементов к полям ViewModel можно описать декларативно. С другой стороны, events поддерживаются на уровне фреймворка, поэтому реализовать привязку ViewModel и Model тоже можно очень удобно.

В JavaScript у нас такой поддержки событий нет, поэтому для реализации MVVM-шаблона требуется две вещи:
  1. Механизм привязки свойств DOM-элементов к полям ViewModel.
  2. PubSub-инфраструктура для поддержки евентов, связанных с изменением значений полей ViewModel.


Knockout JS



Knockout JS — как раз такой фреймворк, который реализует эти два механизма. Для связи View-ViewModel используются data-атрибуты HTML5.

Пример:

<div id=”customerDetailsdata-bind=”visible: currentCustomer() !== null>

* This source code was highlighted with Source Code Highlighter.


currentCustomer — поле в объекте ViewModel. Вообще-то это не совсем поле:

var vewModel = {
  currentCustomer: ko.observable(defaultCustomer),

}

* This source code was highlighted with Source Code Highlighter.


Как видите, значение поля оборачивается в ko.observable. Через эту обертку Knockout следит за тем, как меняется значение поля. Т.е. там вызываются соответствующие обработчики событий. Недостатком, конечно, является то, что приходится везде работать через вызов метода, писать скобочки.

viewModel.currentCustomer = joeCoder;

* This source code was highlighted with Source Code Highlighter.


превращается в

viewModel.currentCustomer(joeCoder);

* This source code was highlighted with Source Code Highlighter.


Привыкнуть можно, но иногда скобочки в геттере забываются.

Чтобы подписаться, требуется писать следующее:

viewModel.currentCustomer.subscribe(function (newValue) {
// this === context<br>
}, context);


* This source code was highlighted with Source Code Highlighter.


Так можно привязывать ViewModel к нашим бизнес-методам. Внутри фреймворка для всех data-bind-атрибутов на самом деле создаются такие же обработчики. Вся привязка инициируется вызовом

ko.applyBindings(viewModel)

* This source code was highlighted with Source Code Highlighter.


Так что на поверхности все вполне доступно — никакой магии.

Конечно, если б дело ограничивалось только этим, то уже было бы довольно мило. Но дядя Сандерсон пошел дальше.

Во-первых, он сделал обертку для массивов: ko.observableArray. Он, правда, умеет немного: pop, push, reverse, shift, sort, splice, unshift, slice, remove, removeAll, indexOf. Вполне достаточно для работы, но некоторых изысков underscore не хватает. Понятное дело, можно прочитать значение — сам массив, — провести с ним операции, а потом записать его обратно целиком. Но получается некрасиво (почему, объясню дальше). Поэтому лучше держаться того, что уже есть.

Второе немаловажное дополнение — ko.dependentObservable(). Т.е. можно определить свойство в ViewModel, которое будет зависеть от других свойств. При этом нокаут ведет автоматический треккинг зависимостей! Это одна из самых важных фич библиотеки — настоятельно рекомендую пользоваться. Опять же, внутри никакой магии. При расчете ko следит за тем, к каким observabl’ам идет обращение и добавляет их в треккер. В случае, если значение одного из этих свойств изменилось, вызывается пересчет для зависимого свойства. Понятное дело, что в качестве свойств-основ тоже могут выступать зависимые свойства.

Что можно делать с помощью dependentObservable? Основное его назначение — упрощать логику. Часто бывает, что в data-bind оказывается сложное условие. Выглядит это очень неуютно — JavaScript в моем HTML!!1. Дебаг этого условия так же оказывается неудобным — дебаггер не будет заходить в тело атрибута. Поэтому проще вынести условие или его компонент в виде dependentObservable и в дальнейшем работать с ним.

Кстати, к вопросу об отладке. Как я уже сказал, в тело атрибута дебаггер не заходит. Все потому, что в applyBindings фреймворк пробегается по всем тегам и для всех байндингов создает свои dependentObservables. Делает он это, естественно, через eval. Не очень хорошо, но это выбор между читаемостью и чистотой. Кстати, для тех, кто считает, что видеть куски JavaScript кода в атрибутах HTML — это возврат к inline обработчикам зари интернета навроде onclick, спешу сообщить, что никто не заставляет этого делать. Можно навесить их через DOM-api, как, например, показано здесь: Unobtrusive Knockout support library for jQuery

Что еще? Есть поддержка темплейтов jQuery-Tmpl. В свое время ожидалось, что этот плагин станет частью jQuery, однако планы разработчиков поменялись. С другой стороны Микрософт его тоже активно уже не разрабатывает — единственный девелопер перешел к другим проектам. Так что проект немного мертворожденный — мой прогноз: он будет находиться в состоянии beta1 еще примерно год. Потом либо выкинут, либо доведут до ума.

Но даже для beta1 мне он показался достаточным, особенно в связке с Knockout. На самом деле в нашем проекте мы не используем темплейтной логики — всех этих выражений в фигурных скобках. Итерируемся мы через Knockout foreach, подставляем значения в тело и атрибуты тегов тоже через ko. Во-первых, начав применять data-bind-атрибуты, мы бы хотели применять их везде, а во-вторых при рендеринге через foreach в сочетании с операциями из ko.observableArray перерисовывается не вся коллекция, а только измененные элементы. Вот почему я считаю, что по возможности стоит ограничиваться только теми операциями.

Еще не стоит забывать, что в параметрах темплейта можно указать колбэк afterRender. С его помощью можно достроить в темплейте то, что силами байндингов строить неудобно, ну или выполнить какие-то дополнительные действия. Мы в afterRender инициируем виджеты, если таковые должны быть в темплейте. В колбэк передаются два параметра. Первый — это renderedNodesArray — фрагмент DOM, который отрендерил темплейт. Второй — data — данные, переданные темплейту.

Почему мне понравился этот фреймворк?


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

UI код отлаживать очень нелегко. С одной стороны, он не кажется особенно сложным, по сравнению с той же бизнес-логикой. С другой, к нему трудно подступиться, трудно выдрать его из контекста, трудно поместить его в юнит-тест, например. Knockout дает мне возможность описывать UI не с помощью кода, а с помощью правил. Это огромный плюс.

Тот факт, что правила описываются в маркапе, это тоже плюс — на мой взгляд. Вот секция, отвечающая за панель инструментов, и вот правила ее поведения. Очень удобно! Особенный жирный плюс за то, что маркап остается понятным для дизайнерских инструментов — никаких кастомных тегов, никаких XML неймспейсов. Любой редактор откроет ваш HTML без проблем.

Knockout vs. Backbone


Альтернативой Knockout сегодня для многих является Backbone. В нем есть много всего хорошего. Коллекции работают с underscore — это большой плюс при написании логики преобразования, фильтрации и т.п. Есть поддержка синхронизации данных с сервером, есть history management. Но при работе с UI я должен прописывать все связи модели с DOM руками. Это не сложно, но очень утомительно. Кода получается немного, особенно если сравнить вариант unobstrusive Knockout binding. Но тот факт, что этот монотонный код приходится писать вручную, очень огорчает.

С другой стороны, в Knockout нет ничего со стороны модели за пределами эвентов во ViewModel. Там-то как раз Backbone и силен. На деле ваш выбор в пользу той или иной библиотеки должен основываться на том, насколько большой в вашем коде составляющая UI или Model. Представьте, что вы пишите проект с нуля, не используя ни одну из этих библиотек. Если вы предвидите, что большая часть проблем возникнет на стороне DOM, то наверное, лучше взять Knockout, чтобы он облегчил вам жизнь. Если же основная сложность возникнет на стороне интеграции, то вам, возможно подойдет Backbone.

Основной плюс Backbone перед Knockout я лично вижу в Sync — способности синхронизировать данные с сервером. Однако Sync работает хорошо только в случае, когда серверное API подчиняется правилам REST. Многие думают, мол, здорово, сейчас я заставлю это работать. Однако они упускают з виду тот факт, что REST и JSON-over-HTTP — не одно и то же! Если с первым Backbone работает из коробки — просто указываешь URL ресурса, — то со вторым его нужно подпиливать. И тогда задаешься вопросом: а так ли уж Sync лучше, чем простой вызов $.ajax?

В нашем проекте пришлось иметь дело именно с JSON-over-HTTP. Кроме того, число видов сообщений, которые мы отправляем и принимаем с сервера, невелико, пока только четыре. Только один из них — синхронизация данных. Все остальное — разовые действия пользователей. Даже несмотря на то, что приходится писать эти вызовы самим, для моей команды это не слишком сложно.

Кстати о команде. Из четырех человек только я один имею опыт работы с JavaScript вообще. Чтобы начать работу, я провел с ними курс языка и библиотек. Оказалось, что для всех именно DOM api представляет наибольшую сложность, даже в облике jQuery. Отчасти это связанно с CSS и с тем, что они просто не знают, какой атрибут за что отвечает. Поэтому им очень понравилось, что можно всю работу с ним локализовать вокруг одного объекта — ViewModel. Работая над разными узлами, они практически не наступают друг другу на ноги.

Если вам интересно сравнение двух библиотек, есть серия постов. Там описывается применение для простого сценария. Но сценарии бывают разными :)

Практика


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

1. Отличие ViewModel от Model.

Не все чувствуют разницу. Простое правило:
  • Model отвечает на вопрос “Что есть в моем приложении и что оно умеет делать?”
  • ViewModel — на вопрос “Что я показываю на странице?”

К чему я это? А к тому, что многие начинают смотреть на ViewModel, как на модель. Тут у них возникает ряд вопросов. Например: в Презентере могут быть указаны список товаров, выделенный товар и индекс выделенного товара. Сразу же найдется кто-то, кто скажет, что не надо хранить и выделенный товар, и индекс — мол, данные дублируются. Однако в отличие от модели нам не важно, присутствует ли во ViewModel какая-то избыточность. Если она позволяет нам существенно упростить код приложения, то почему бы нет?

Многие также спрашивают, можно ли использовать несколько ViewModel. Можно, но нужно ли? Мы предпринимали такую попытку, но нашей ошибкой стало то, что делили мы обязанности между ViewModels в соответствии с их содержимиым — то есть работали с ними как с сущностями модели. Тогда как ViewModels представляет участки страницы и каждый из них работает только со своим участком. Потом, когда вам потребуется данные из одного ViewModel показать на другом участке страницы, напрямую через data-bind ничего не выйдет.

Так что мой вам совет: используйте один ViewModel для всей страницы — все будет просто работать. Есть риск, что объект станет большим, но никто не мешает вам в коде писать несколько раз

_.extend(ViewModel, {
// еще свойства и методы<br>
})


* This source code was highlighted with Source Code Highlighter.


Такими блоками можно разбить функциональность на модули.

2. Несмотря на то, что в примерах к Knockout Model отсутствует напрочь, а сами сторонники заявляют вам с улыбкой, что она-де на сервере, не верьте им.

На клиенте модель тоже есть, и ее придется писать ручками — в отличие от Backbone Нокаут вам в этом помогать не будет. С другой стороны, не так оно и страшно, а ваш здравый смысл и общие правила гигиены вам помогут. Помните две вещи: события и модульность. Если ajax код становится для вас проблемой, посмотрите в сторону AmplifyJS.

3. К сожалению, даже в тех редких случаях, когда Knockout хочет нам помочь в моделестроении, у него не очень получается.

Метод ko.toJSON не умеет работать с датами, и пишет в них null. Есть способы это обойти, но все они немного корявые. Самый простой — добавить еще одно dependentObservable свойство со значением JSON.stringify(date()) — там окажется просто строка, которую можно отправить на сервер. Но некрасиво! — два поля на каждую дату, а маршаллинг работает только в одну сторону. Поэтому мы пошли другим путем: просто написали собственный обходчик для ViewModel, который осторожно обходится с датами. Благо ko предлагает метод unwrapObservable который возвращает простой объект. Работает он, правда, только на верхнем уровне, так что потом приходится обходить все свойства полученного объекта на случай, если где-то еще observables остались

4. Несмотря на темплейтинг, иногда придется создавать DOM другими методами.

Особенно это справедливо для виджетов и компонентов третьих сторон. В таких случаях ko тоже может мешать: например, когда компоненту требуется простой массив, а у нас это observableArray, т.е. в коде компонента:

items.pop()

* This source code was highlighted with Source Code Highlighter.


а нам бы хотелось, чтобы там было

items().pop()

* This source code was highlighted with Source Code Highlighter.


Тут все зависит от ситуации. Иногда мы патчим компоненты для внутреннего использования — особенно, если они небольшие или если какие-то изменения в них уже были внесены. Но в большинстве случаев мы передаем туда unwrapped array, при этом приходится работать с подписками на изменения, чаще всего — писать два обработчика — на изменение данных в компоненте и на изменение данных во ViewModel.

5. Knockout работает очень хорошо в рамках традиционных бизнес-сценариев.

Формы, вывод данных, таблицы — если это именно то, что делает ваше приложение, то он вам подойдет на 100%.

Если у вас что-то экзотическое, например, работа с SVG и Canvas, много динамически создаваемых и изменяемых без вашего контроля DOM-элементов, как в нашем проекте, то у вас должны быть сомнения. Мы выбрали ko, так как в нашем проекте и от него, и от Backbone толку в целом не очень много — две трети кода вообще не связано с задачами, которые выполняют эти библиотеки. Но даже в оставшейся трети Knockout нам показался полезнее.

Knockout и Backbone


У многих из вас в процессе чтения статьи могла возникнуть идея объединить Backbone и Knockout. К сожалению об этом проще сказать, чем сделать. Функциональность фреймворков во многом пересекается, трудно определить, где нужно провести грань между ними. Мы попробовали это сделать между Knockout ViewModel и Backbone Model, но код в результате стал слишком громоздким: пришлось писать синхронизатор данных, который по размеру в сочетании с моделью Backbone стал больше, чем то, что мы писали для связи ViewModel с сервером. Отладка тоже усложнилась, а Sync оказался не таким удобным в применении для наших сервисов, как мы надеялись.

Так что пока приходится выбирать между одним или другим. В будущем наверняка появятся байндинги в Backbone и аналог Sync в Нокауте. Если вы чувствуете себя первопроходцем, являетесь ярым сторонником Backbone и хотите, чтобы в нем тоже появились байндинги, я дам вам еще один совет. Присмотритесь к jQuery-Link. Это еще один плагин от Микрософт, который они также вяло дорабатывают, как и jQuery-Tmpl. Но тем не менее, он может стать основой для реализации байндингов.

Сами Microsoft по всей видимости заняли выжидательную позицию. Steve Sunderson — автор Knockout — работает у них, а видео с его презентации на MIX11 стало самым популярным, обгоняя все кейноуты и все презентации по IE10, performance optimization и будущем HTML5, EcmaScript и любым другим технологиям компании. Сейчас Стив совместно со звездой .net Скоттом Гатри колесит по миру и показывает всем свое творение. А на этой неделе он опубликовал новые обучающие материалы. Если фреймворк “взлетит”, то компания будет поддерживать его так же, как и jQuery.

Backbone тоже никуда не денется, к нему большой интерес среди рубистов, программистов NodeJS и всего авангарда JavaScript-сообщества. Так что выбирая любой из них можете быть уверены — не прогадаете!
Tags:
Hubs:
+56
Comments 23
Comments Comments 23

Articles