Все материалы сюжета:

 

Восемнадцать лет назад компания Netscape Communications выпустила новую версию своего браузера — Netscape 2.0. Помимо таких крутых штук, как возможность задать цвет шрифта, вставить фрейм или анимированную гифку, в нем были представлены скрипты для исполнения в контексте загруженной страницы. Поделка получилась удачной — настолько, что Microsoft в своем ответном Internet Explorer 3.0 запустила поддержку JScript, собственного варианта скриптового языка для браузеров. И понеслось…

Изначально язык предполагалось использовать как прослойку между HTML-разметкой страницы и Java-апплетами для несложных операций на клиенте. Как признался в своем комментарии Эрик Липперт (Eric Lippert) — один из людей, причастных к разработке JScript в Microsoft, целью создания JavaScript было заставить обезьянку плясать при наведении мышью. Так что в жертву простоте были принесены многие вещи, которые воспринимаются как должное в других языках, — например, приватные переменные. Ситуация осложнялась и тем, что на фоне гонки браузеров разработка и внедрение фич в язык и в API для взаимодействия с загрузившей скрипт страницей происходили с бешеной скоростью, а стандарты отчаянно пытались их догнать. Одни и те же вещи в разных браузерах происходили по-разному, и поэтому делать что-то серьезнее валидации форм с помощью JavaScript было достаточно бессмысленно.

Пока все думали, как еще можно приспособить клиентские скрипты себе на пользу, появился формат XML. Он позволял описывать в виде разметки не только текст, но и любые данные, допускающие строковое представление. Еще чуть позже в Microsoft придумали XMLHttpRequest для динамической подгрузки контента с сервера, а остальные реализовали поддержку его API. Если добавить сюда появившуюся в браузерах возможность изменять DOM с помощью JavaScript, получится AJAX (Asynchronous JavaScript and XML) — набор технологий для асинхронного взаимодействия c сервером и отображения результата на веб-странице без ее перезагрузки. Со временем XML в этой связке уступил место более лаконичному и JS-ориентированному формату JSON, но название прижилось и используется до сих пор.

К этому моменту с загруженным документом уже можно было вытворять практически что угодно — конечно, если автор скрипта был готов жрать этот кактус, невзирая на боль от разницы в браузерных API. А желающих становилось все больше. Простота языка, низкий порог вхождения и желание оживить скучные статичные веб-странички привлекали энтузиастов. А возможность создать легкое, но функциональное веб-приложение и доставлять его пользователям, не требуя от них для этого ничего устанавливать на компьютер, привлекала пацанов посерьезнее. И тут начали выясняться интересные подробности про сам JavaScript.

Оказалось, что под видом Java Lite в браузеры попал язык с забористой смесью парадигм, вобравшей многое как от объектно-ориентированного, так и от функционального подходов. Только вот прототипная модель наследования работала не так, как привычная классовая. А динамическая типизация значительно усложняла статический анализ кода. И работать нужно было по большей части с асинхронными запросами и событиями. Все это накладывалось на однопоточную среду выполнения в браузере, которая еще и временами безбожно тормозила.

В общем, просто взять имевшиеся знания о программировании и применить их не получалось. Но серьезных пацанов в клиентской разработке становилось все больше, и остановить их на пути в светлое JS-будущее оказалось не так-то просто. Начались попытки создать библиотеки для того, чтобы получить общий интерфейс для всех браузеров, скрывающий разницу в реализации ими различного функционала, — их результатами стали Prototype, MooTools и, конечно, jQuery. Последний обрел такую популярность, что ветераны веб-разработки жалуются — мол, новички сейчас без jQuery вообще ничего делать не умеют.

С появлением библиотек на полезную работу стало уходить больше времени, а на борьбу с браузером — меньше. А еще стало понятно, что JavaScript — это не обертка для работы с DOM и браузером, а вполне себе отдельный и весьма неплохой при правильном использовании язык программирования, и в серверной разработке он бы тоже не помешал (на самом деле серверные реализации JavaScript существовали с первого дня его появления, но никто про них особо не вспоминал). В итоге появился проект CommonJS, цель которого — распространение JavaScript на другие платформы, стандартизация API и создание стандартной библиотеки. И во многом благодаря этому проекту сегодня существуют такие вещи, как Node.js и RequireJS.

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

 

INFO

Нужно сказать, что способ изолировать часть кода и данных от доступа снаружи в JavaScript все-таки есть, и способ этот — замыкания. На основе замыканий обычно и строятся все клиентские решения по модуляризации JavaScript-кода. Самое известное из них — RequireJS. Оно позволяет создавать раздельные модули клиентского JS-кода, указывать между ними зависимости, а также подгружать недостающие файлы с сервера.

Параллельно шла оптимизация JavaScript-интерпретаторов в браузерах — появлялись все более быстрые движки, такие как знаменитый V8 от Google, и в итоге стало возможным перенести часть логики приложений с сервера в браузерный JavaScript. Вот тут уже началось массовое внедрение на клиенте различных паттернов и практик, обкатанных в серверной разработке. Например, отлично вписался в клиентскую модель паттерн Model — View — Controller, разделяющий все компоненты на предназначенные для получения и хранения данных (Model), для отображения данных (View) и для организации взаимодействия и управления другими компонентами (Controller). Появилось множество фреймворков и библиотек, которые позволяют частично или полностью реализовать этот подход на клиенте.

С некоторыми из них мы и решили тебя сегодня познакомить.

 

Backbone

backbonejs.org

backbone_2

Backbone.js — это небольшая и простая, но проверенная временем MVVM (Model — View — ViewModel) библиотека, которую давно используют множество крупных компаний. Разработка началась в конце 2010 года, библиотека прошла несколько стадий рефакторинга и год назад получила первую релизную версию. MVVM является архитектурным подходом для связывания моделей и представления в обе стороны. Изменения состояния в представлениях тут же отобразится в модели, и наоборот.

Backbone.js предоставляет такие модули, как модели, коллекции, представления и событийный модуль, связывающий все модели воедино, а также модуль роутинга с поддержкой History API. Если твое фронтенд-приложение работает в первую очередь с множеством данных, где можно явно выделить модели и собрать их в коллекции, — Backbone.js будет идеальным выбором.

Быстрый старт

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

var Book = Backbone.Model.extend({});

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

var Library = Backbone.Collection.extend({
  model: Book
});

var library = new Library();
lirary.add({
  title: 'The Dark Tower',
  author: 'Stephen Edwin King'
});

Коллекция сама создаст модель и заполнит их данными в свойстве arguments. Теперь попробуем получить свежие данные с сервера:

library.url = '/books'
library.fetch()

 

INFO

Метод fetch инициализирует функцию Backbone.sync (ты можешь переписать ее для поддержки Web Sockets, например), которая, в свою очередь, сделает AJAX-запрос к серверу по URL, указанному в объекте коллекции.

Сервер должен ответить валидным JSON’ом и обязательно массивом (для коллекции):

[{
  “id”: “334aa7010561cf20fc1e2c4fc63e1b82a9cfd83e”,
  “title”: “The Dark Tower”,
  “author”: “Stephen Edwin King”
},
{
  “id”: ‘9ea598b60837a06038caa01672bcff3a8331134a”,
  “title”: “The Guinness Book of Records”,
  “author”: “Hugh Beaver”
}]

Теперь создадим представления, чтобы вывести нашу коллекцию на экран.

var LibraryView = Backbone.View.extend({
  // Существующий элемент на странице, где будет отображаться наш список
  el: "#books",
  // Генерируемый тег элементов
  tagName: "li",
  // Имя класса для элемента
  className: "row",
  // Шаблон отдельной книги. Можно использовать любой шаблонизатор, в данном случае — underscore
  template: '<span class="title"><%-title%></span><p class="author"><%-author%></p>',
  // В каждом модуле Backbone метод initialize является конструктором
  initialize: function() {
    // При изменении модели будет запущен метод render
    this.listenTo(this.model, "change", this.render);
  }
});

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

var Workspace = Backbone.Router.extend({ routes: { «»: «index» }, index: function() { var library = new Library(); var view = new LibraryView({ collection: library }); library.fetch(); } }); var workspace = new Workspace();

 

INFO

О Backbone’овских событиях DOM, модуле History и Events ты можешь узнать больше в удобной документации Backbone.

 

INFO

Backbone.js требует Underscore.js и jQuery/Zepto в зависимостях, но, если они не нужны в приложении, можешь использовать Exoskeleton — форк Backbone, где никаких зависимостей не требуется.

Extend(Backbone)

Используя Backbone.js «как есть», нужно быть предельно осторожным. Это все-таки библиотека, а не фреймворк, и он не самодостаточен. В реальных проектах тебе придется строить поверх него множество абстракций и обвязок, а также разрабатывать собственную архитектуру приложения, так как библиотека ее не предоставляет в принципе. У тебя есть лишь набор базовых сущностей, но как их связать и построить приложение — решать тебе.

Но и здесь у есть готовые решения в виде фреймворков, таких как Marionette.js и Chaplin.js. Если Backbone позиционируется скорее как идеология и библиотека для работы с данными в моделях и коллекциях, то фреймворки на базе Backbone.js предоставляют структуру приложения, определяют подход к архитектуре, предоставляют руководства к организации кода.

Marionette.js является модульным фреймворком (можно использовать только те его модули, которые нужны, не боясь потерять связанность). Модулей довольно много, Marionette вводит такие сущности, как приложение, контроллеры, модули и субмодули, собственный роутер, лейауты, отдельные представления для коллекций и моделей и понятие регионов, а также композитные представления для централизованного управления регионами.

Marionette решает главную проблему Backbone — непроработанность части представлений. И конечно же, привносит осознанную архитектуру приложения. Для Marionette.js написано несколько книг и присутствует великолепная документация, она очень расположена к новичкам.

Chaplin.js — менее известный, но не менее интересный MVP/MVC-фреймворк на основе Backbone.js. В отличие от Marionette.js он монолитен, предоставляет меньшее число модулей, и использовать их независимо друг от друга нельзя. Но от этого фреймворк не теряет в функциональности, а даже кое-что приобретает, например более четкие соглашения внутри приложения. Каркас и структура приложения четко регламентированы. Судя по графикам на GitHub, основная разработка завершена и проект стабилен.

Итак, Chaplin.js предоставляет нам следующие модули: приложение, модели и коллекции, роутер, dispatcher — загружающий и инициализирующий контроллеры, mediator — реализующий Publish/Subscribe-паттерн, который позволяет связывать модули событийно, также он хранит и кеширует данные для последующего многократного использования.

Также Chaplin привносит целую группу представлений: Layout — представления высшего уровня, управляет основными регионами страницы, Collection View и (Item) View, где представление коллекции может итерировать в себе любые представления. В каждом представлении можно создать множество регионов и вкладывать в них необходимые шаблоны. Вообще в плане решения проблемы с представлениями Chaplin.js более конкретный и связный, нежели Marionette.js, хотя второй гибче и позволяет писать с разными подходами, когда Chaplin.js четко декларирует подход к написанию приложения.

Главная идея Chaplin.JS — вычищаемые из памяти контроллеры. Фреймворк заточен на производительность и «волшебный» менеджмент памяти. После того как срабатывает роутинг, все экземпляры представлений, моделей, коллекций и их события, в том числе привязанные к DOM, аккуратно уничтожаются, высвобождая память. Конечно, ты можешь предусмотрительно сохранить необходимые объекты в Composer, но все, что не нужно в текущий момент, удаляется, тем самым жизнь браузерного сборщика мусора (garbage collector) становится заметно легче. Такой подход уменьшает количество утечек памяти в крупных приложениях и позволяет приложению корректно работать долгое время, не отжирая дополнительную память.

Также у Chaplin.js есть собственная платформа со сборщиком Brunch, который имеет на борту Bower.js, CoffeeScript (включая sourse maps v3), CommonJS-модули, собирающиеся в AMD-модули (что особенно полезно тем, кто пишет на Node.js), а также CSS-препроцессор Stylus (если ты предпочитаешь Sass, то используй библиотеку libsass, написанную на C++) и шаблонизатор Handlebars (который ты вполне можешь заменить на быстрый ECT.js, лаконичный Jade или привычный Haml).

Выводы

Backbone.js — довольно минималистичная библиотека, которая позволяет удобно работать с моделями, объединять их в коллекции и предлагает минимальный функционал для их представления. Используя эту библиотеку в среднем или крупном проекте, отдавай себе отчет в том, что придется очень многое реализовывать самому. И самый простой выход — использовать фреймворки на основе Backbone — Marionette.js или Chaplin.js.

 

Angular

angularjs.org

Логотип AngularJS
Логотип AngularJS

Если ты в последние два-три года следил за трендами в веб-разработке, то наверняка в курсе и такого явления, как Angular, тем более в ][ уже как-то была про него статья. Ну а если вдруг почему-то он не попал в поле твоего внимания, то сейчас самое время это исправить. Даже если заложенная во фреймворк философия окажется тебе не близка, там есть на что взглянуть и помимо нее.

Дзен Angular

Собственно, все построено вокруг следующих принципов:

  • представление (View в паттерне MVC) данных описывается декларативно в разметке страницы;
  • код, манипулирующий представлением (читай: DOM), отделен от бизнес-логики;
  • написание тестов не менее важно, чем написание кода приложения;
  • простые операции должны делаться просто.

Если помнить про эти четыре пункта, то будет понятно, почему многое в Angular выглядит именно так, а не иначе.

Первые шаги

Создатели Angular облегчили начало работы с ним до неприличия, создав на гитхабе шаблон для новых проектов:

$ git clone https://github.com/angular/angular-seed
$ cd angular-seed
$ npm install

Никаких других глобальных зависимостей не требуется — все нужное будет скачано и автоматически поставлено с помощью npm. После окончания установки можно набрать npm start и отправиться любоваться на работающий сайт, на котором есть MVC-роутер с двумя пустыми страничками.

Весь код приложения, отвечающий за работу роутера, умещается в несколько строчек в /app/js/app.js:

angular.module('myApp', [
  /* Сейчас нам это не нужно */
]).config(['$routeProvider', function($routeProvider) {
  $routeProvider.when('/view1', {templateUrl: 'partials/partial1.html', controller: 'MyCtrl1'});
  $routeProvider.when('/view2', {templateUrl: 'partials/partial2.html', controller: 'MyCtrl2'});
  $routeProvider.otherwise({redirectTo: '/view1'});
}]);

и одну — в /app/index.html:

<div ng-view></div>

Вкратце: в JS-файле указано, какой частичный шаблон представления и с каким контроллером нужно использовать в случае изменения части урла после хеша, а в index.html — в какое место на странице его подставить. Всю остальную работу на себя берет внутренний сервис $route. Это, кстати, одно из достоинств Angular: для многого из того, что почти наверняка понадобится при разработке клиентского MVC-приложения (а именно роутинга, запросов к бэкенду, работы с cookie, location и так далее), уже есть готовые решения, идущие в комплекте с фреймворком. Также присутствуют свои минималистичные реализации promise и подмножества jQuery для работы с DOM — jqLite. При этом если jQuery уже был загружен на страницу до Angular, то используется именно он.

Test all the things!

Если теперь вернуться в консоль и там набрать npm test, то внезапно стартанет тест-раннер Karma и покажет, что код проекта, оказывается, уже покрыт юнит-тестами и они даже все проходят. Вдумайся: все уже долго и много говорят про TDD, а сколько ты знаешь фреймворков, которые действительно создадут тебе возможность для написания тестов раньше кода приложения? И это не только юнит-тесты — в комплекте с шаблоном проекта идет обертка над WebDriverJS под названием Protractor для тестирования end-to-end сценариев. По умолчанию вместе с проектом она не ставится, как раз потому, что тащит за собой WebDriver (которому нужен еще и JDK), — но если возникнет необходимость, это исправляется еще одной командой npm. В общем, к тестам тут подходят очень по-взрослому.

Еще одно подтверждение этому — сама архитектура JavaScript-части фреймворка. Весь код организован в модули вида var myModule = angular.module('myModule', ['dependencyOne', 'dependencyTwo']), у которых есть функции для создания различных компонентов в этих модулях. Все эти функции оформлены, например, таким образом:

myModule.controller('CtrlName', ['dep1', 'dep2', 'dep3', function (dep1, dep2, dep3) { /* ... */ }]);

Во время выполнения создается объект injector, который занимается разрешением зависимостей и подстановкой нужных объектов в качестве аргументов в функции компонентов. Все это называется Dependency Injection и давно и с успехом используется нашими серверными друзьями. В целом очень похоже на то, что делает RequireJS или require в ноде, но отличия все же есть — например, в зависимости от типа компонента инжектор может передавать в качестве зависимости либо каждый раз новый его экземпляр (для контроллеров), либо все время один и тот же (для всего остального). К тому же в Angular возможна такая конструкция (выжимка из того самого angular-seed):

angular.module('myApp', [
  'myApp.filters',
  'myApp.services',
])
angular.module('myApp.services', []).value('version', '0.1');
angular.module('myApp.filters', []).
  filter('interpolate', ['version', function(version) {
    return function(text) {
      return String(text).replace(/\%VERSION\%/mg, version);
    };
  }]);

Получается, что, хотя myApp.filters использует в своих компонентах сервис version(возвращающий просто константу), у него нет прямой зависимости от модуляmyApp.services, в котором он определяется, — главное чтобы в модуле приложения они повстречались. Require такого не допускает, там все жестко.

Ложка дегтя

До этого момента я говорил только хорошее — но во фреймворке есть еще один ключевой момент, который оказался достаточно противоречивым. Это его декларативное оформление представлений с помощью директив. То есть в идеологии Angular поверх HTML появляется свой Domain-Specific Language на основе кастомных атрибутов, тегов и классов, который и используется в разметке. Связь представления с контроллером устанавливается через контекст (scope), который обеспечивает двустороннюю привязку данных: изменения в контексте со стороны контроллера отражаются в представлении, а операции пользователя со страницей меняют состояние контекста внутри контроллера.

В общем-то, любой MVC-фреймворк делает именно это — увязывает данные с представлением. Другой вопрос в том, как он это делает, — где-то используется подписка на события DOM, a где-то специальные JS-объекты для модели. Создатели Angular пошли по еще одному пути, который называется Dirty Checking, — на каждую привязку контекста к представлению добавляется функция проверки, которая вызывается контекстом. С одной стороны, это позволяет значительно упростить разметку шаблонов и код в директивах, отслеживающий изменения в контексте. С другой же стороны, если функция проверки работает медленно (обычно так бывает, если она делает что-то с DOM), а элементов с привязками много — или функция работает быстро, но элементов с привязками очень много, скажем несколько тысяч, — это может привести к значительным проблемам с производительностью. Не то чтобы это фатальный недостаток — в интернете достаточно много статей, таких как вот эта или эта, где описываются различные методы борьбы с данной проблемой, — но самым оптимальным решением и по сей день остается добавлять в контекст только те данные, которые планируется отобразить.

Выводы

В общем и целом Angular — это вполне достойный фреймворк для разработки клиентских MVC-приложений на JavaScript, сильными сторонами которого являются:

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

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

 

KnockoutJS

knockoutjs.com

knockout

Knockout предлагает тебе архитектурный шаблон MVVM (Model — View — ViewModel) практически такой же, каким его видят в WPF (Windows Presentation Foundation), а там он выглядит весьма и весьма привлекательно!

Что же в коробке?

  • Ванильный JS (без зависимостей)
  • 17 Кб в gzip’e
  • Двустороннее связывание данных
  • Декларативные биндинги
  • IE6+

 

INFO

Knockout, хоть и был создан сотрудником Microsoft, влиянию корпорации зла не подвержен и является законным open source с лицензией MIT.

Сама суть MV* паттернов, как, например, Model — View — Controller (MVC), проста и логична — отделение логики от представления. Это помогает держать код приложения чистым и организованным, облегчая его поддержку и расширение. Практически невозможно долгое время обходиться без MV* или подобных соглашений при разработке достаточно большого и сложного приложения, вот почему MVC пришел в JavaScript, и вот почему MVVM пришел ему на замену, эволюционировав таким образом для облегчения разработки интерфейсов. Камнем преткновения MVVM стало понятие связывания данных, представляющее собой привязку данных модели к пользовательскому интерфейсу и двустороннюю их синхронизацию: меняем данные в интерфейсе — они автоматически меняются в модели, и наоборот. В MVC подобные связи не могут существовать by-design и попытка реализовать такой функционал, не выпадая из общей парадигмы, приводит к усложнению приложения из-за добавления новых вспомогательных абстракций. MVVM лишен такого недостатка за счет наличия ViewModel — текущего состояния модели, спроецированного на представление и включающее методы их взаимодействия. Концепция проще, чем кажется с первого взгляда, и в этом можно убедиться, познакомившись ближе с Knockout, что мы и сделаем.

Quick start

Knockout из коробки не дает нам какую-либо файловую структуру для проекта и не предлагает ее соблюдать, хотя для многих разработчиков этот вопрос важен при выборе фреймворка. Я же склонен считать это скорее плюсом, чем минусом, и для того есть основания. Структура реального проекта редко бывает неизменной, и в ходе сложных рефакторингов ее зачастую приходится изменять, порой кардинально. А в этом случае важно не иметь ограничений со стороны фреймворка. Однако же, если ты со мной не согласен и по какой-то причине хочешь использовать придуманную кем-то другим структуру проекта, то советую обратить внимание на следующие проекты:

  • knockout-amd-helpers — небольшой плагин от ведущего контрибьютора проекта, Райана Нимьера (Ryan Niemeyer), предлагающий AMD-модули для Knockout-приложений;
  • Durandal — решение для создания одностраничных приложений, включающее jQuery + Knockout + RequireJS;
  • pagerjs — аналогичное Durandal решение, ставящее своей целью помочь тебе с организацией большого проекта;
  • Falcon.js — надстройка над Knockout, предлагающая свое видение сущностей модели, коллекции и представления.

Со структурой более-менее определились? Отлично, идем дальше!

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

Таким образом, Knockout все равно, что представляют собой твои модели и как ты их будешь синхронизировать, встроенных средств для этого из коробки не предоставляется. Единственное требование — получить на выходе валидный JSON (точнее, JS-объект), так что не будет плохой практикой вставка данных, необходимых для инициализации приложения, даже прямо в head внутри тега script. Просто чтобы не делать лишний аякс-запрос для запуска приложения.

<head>
   …
   <script>
       var storedGifts = [
           { name: "Tall Hat", price: "39.95" },
           { name: "Long Cloak", price: "120.00" }
       ];
   </script>
</head>

Но не забывай, что добавлять глобальные переменные плохо! Не ленись и используй пространства имен, например window.MyGiftShop = { storedGifts: … }.

View (представлением) в мире Knockout является конечный HTML (говоря строго, это DOM-дерево или его часть), содержащий так называемые биндинги — специальные синтаксические конструкции, находящиеся в атрибуте data-bind элемента и указывающие, как именно он должен быть связан с данными из модели.

Яркий образец View из официального примера Grid editor. Он выводит список подарков, часть которого мы описали выше, и формирует интерфейс с биндингами для его редактирования.

<form action='/someServerSideHandler'>
   <p>You have asked for <span data-bind='text: gifts().length'>&nbsp;</span> gift(s)</p>
   <table data-bind='visible: gifts().length > 0'>
       <thead>
           <tr>
               <th>Gift name</th>
               <th>Price</th>
               <th />
           </tr>
       </thead>
       <tbody data-bind='foreach: gifts'>
           <tr>
               <td><input class='required' data-bind='value: name, uniqueName: true' /></td>
               <td><input class='required number' data-bind='value: price, uniqueName: true' /></td>
               <td><a href='#' data-bind='click: $root.removeGift'>Delete</a></td>
           </tr>
       </tbody>
   </table>
   <button data-bind='click: addGift'>Add Gift</button>
   <button data-bind='enable: gifts().length > 0' type='submit'>Submit</button>
</form>

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

  • Контроль потока: foreach, if, ifnot, with.
  • Контроль внешнего вида и текста: visible, text, html, css, style, attr.
  • Работа с полями формы и событиями: click, event, submit, enable, disable, value, hasFocus, checked, options, selectedOptions, uniqueName.
  • Рендеринг шаблонов: template.

 

INFO

С помощью встроенных биндингов Knockout ты сможешь сделать многие базовые вещи, которые нужны в любом приложении, но, конечно же, можно создавать новые с произвольным функционалом.

Стоит отдельно обратить внимание на биндинг template, точнее, на то, что лежит за ним. Как ты уже, наверное, догадался, он служит для вывода результата работы шаблонизатора. По умолчанию в Knoсkout есть встроенный шаблонизатор, который лежит в основе вcех биндингов контроля потока, таких как if или foreach, но принцип работы биндинга позволяет тебе использовать любой строковый шаблонизатор (где на выходе получается строка с HTML), хотя для этого и придется написать немного кода. Пример интеграции Underscore шаблонизатора с Knockout.

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

var GiftModel = function(gifts) {
   var self = this;
   self.gifts = ko.observableArray(gifts);
   self.addGift = function() {
       self.gifts.push({
           name: "",
           price: ""
       });
   };
   self.removeGift = function(gift) {
       self.gifts.remove(gift);
   };
   self.save = function(form) {
       ko.utils.postJson($("form")[0], self.gifts);
   };
};
var viewModel = new GiftModel(storedGifts);
ko.applyBindings(viewModel);

Спорю, ты уже догадался, что ключевая фишка этого кода — ko.observableArray(gifts), которая и делает большую часть всей работы. Так и есть, observable — это специальные объекты, которые умеют уведомлять всех своих подписчиков об изменении данных и автоматически отслеживать зависимости. Именно на них основано связывание данных в Knockout, и очень важно понимать, что они собой представляют. Хоть официальная документация и утверждает обратное (чтобы завлечь тебя), нельзя эффективно работать с Knockout без знания того факта, что отслеживание зависимостей внутри observables работает в реальном времени, то есть цепочка зависимостей будет всегда выстроена заново при обращении к observable. Именно по этой причине не стоит злоупотреблять зависимостями и не создавать круговых зависимостей. Хотя Knockout и не даст тебе выстрелить себе в ногу, это может в итоге привести к существенным потерям производительности и большим разочарованиям.

Метод ko.applyBindings(viewModel) служит для инициализации твоей модели представления, фактически «запуская» приложение. Он может принимать вторым параметром указатель на DOM-элемент, который станет корневым для переданной ViewModel. Таким образом можно создавать отдельные модели представления для каждой части UI.

Выводы

Говорить о Knockout и принципах его работы можно долго и все равно не прийти к какому-то решающему выводу. MVVM-подход в разработке интерфейсов сам по себе еще достаточно молод и не успел завоевать должного доверия вне .NET-тусовки. Knockout же, с одной стороны, имеет достаточно низкий порог вхождения и позволяет окунуться в MVVM с головой, не выходя из браузера. С другой — внутренние механизмы довольно сложны и порой неоднозначны. Если ты попытаешься использовать его в большом одностраничном приложении, то рано или поздно обнаружишь себя зарывшимся глубоко в дебри исходников для observable и биндингов.

 

1 комментарий

  1. 28.11.2014 at 20:26

    Напишите про React. Забыли же!

Оставить мнение

Check Also

LUKS container vs Border Patrol Agent. Как уберечь свои данные, пересекая границу

Не секрет, что если ты собрался посетить такие страны как США или Великобританию то, прежд…