Все материалы сюжета:
- Зачем нужен менеджер пакетов, или почему именно Bower
- Frontend - rак все поменялось за последние 15 лет
- Клиентский JavaScript тогда и теперь
Восемнадцать лет назад компания 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
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
Хакер #184. Современный фронтенд
Если ты в последние два-три года следил за трендами в веб-разработке, то наверняка в курсе и такого явления, как 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 не для тебя, тогда стоит поискать что-то другое.
WWW
KnockoutJS
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'> </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 и биндингов.