Добрый день уважаемые, читатели.
В данной статье я хочу поделиться с вами своим опытом работы с такими фреймворками как AngularJS и Knockout.
Cтатья будет интересна тем, кто хорошо знаком с JavaScript-ом и имеет представление хотя бы об одном из упомянутых фреймворков и естественно желает расширить свой кругозор.
Overview
AngularJS и Knockout очень близки по своей идеологии. Они являются фреймворками для динамических веб-приложений и используют HTML в качестве шаблона. Они позволяют расширить синтаксис HTML для того, чтобы описать компоненты вашего приложения более ясно и лаконично. Из коробки они устраняют необходимость писать код, который раньше создавался для реализации связи model-view-controller.AngularJS и Knockout — это по сути то, чем HTML и JavaScript были бы, если бы они разрабатывались для создания современных веб-приложений. HTML — это прекрасный декларативный язык для статических документов. Но, к сожалению, в нем нет многого, что необходимо для создания современных веб-приложений.
Features
- Data-binding: простой и хороший способ связи UI и модели данных.
- Мощный набор инструментов для разработчика (в частности у AngularJS, Knockout имеет достаточно бедный набор)
- Легко расширяемый инструментарий
How to organize an application
Согласно документации, Angular предлагает структурировать приложение, разделяя его на модули. Каждый модуль состоит из:
- функции, конфигурирующей модуль — она запускается сразу после загрузки модуля;
- контроллера;
- сервисов;
- директив.
Контроллер в понимании Angular — это функция, которая конструирует модель данных. Для создания модели используется сервис $scope, но о нем немного дальше. Директивы — это расширения для HTML.
В свою очередь, Knockout предлагает строить приложение, разделяя его на ModelView, которые являются миксом из модели и контроллера. В пределах объекта ko.bindingHandlers размещены data-bindings, которые являются аналогами директив Angular. Для построения связи между моделью и ее представлением используются observable и observableArray.
Говоря о модульности, нельзя не вспомнить про шаблон AMD — Asynchronous Module Definition. Angular и Knockout не имеют собственной реализации AMD шаблона. Советую использовать библиотеку RequireJS. Она себя очень хорошо зарекомендовала в плане совместимости и с Angular, и с Knockout. Больше интерсеной информации о ней вы найдете тут: http://www.kendoui.com/blogs/teamblog/posts/13-05-08/requirejs-fundamentals.aspx и http://habrahabr.ru/post/152833/.
Шаблонизация
(Отдельная благодарность разработчикам AngularJS за такую прекрасную картинку)
На данный момент уже существует огромное количество шаблонизаторов. К примеру, jQuery Templates (к сожалению, уже не поддерживается). Большинство из них работают по принципу: возьми статический template как string, смешай его с данными, создав новую строку, и полученную строку вставь в необходимый DOM-елемент посредством innerHTML свойства. Такой подход означает ререндеринг темплейта каждый раз после какого-либо изменения данных. В данном подходе существует ряд известных проблем, к примеру: чтение вводимых пользователем данных и соединение их с моделью, потеря пользовательских данных из-за их перезаписи, управление всем процессом обновления данных и/или представления. Кроме того, данный подход, на мой взгляд негативно сказывается на производительности.
Angular и Knockout используют иной подход. А именно two-way binding. Отличительная особенность данного подхода — это создание двунаправленной связи элемента страницы с элементами модели. Такой подход позволяет получить достаточно стабильный DOM. В Knockout двунаправлення связь реализована посредством функций observable и observableArray. Для анализа шаблона используется HTML парсер jQuery (если подключен, в противном случае аналогичный родной парсер). Результатом работы упомянутых функций является функция, которая инкапсулирует текущее состояние элемента модели и отвечает за two-way binding. Данная реализация, на мой взгляд, не очень удобна поскольку возникает проблема связанная с копированием состояния модели: скоуп функции не копируется, поэтому необходимо сперва получить данные из элемента модели обратившись к нему, как к функции и только после этого клонировать результат.
В Angular двунаправленная связь строится непосредственно компилятором (сервис $compile). Разработчику нет необходимости использовать функции подобные observable. На мой взгляд, это намного удобнее поскольку нет необходимости использовать дополнительные конструкции и не возникает проблемы при копировании состояния элемента модели.
Ключевой же разницей в реализации шаблонизаторов в Angular и Knockout является способ рендеринга элементов: Angular генерирует DOM-элементы, которые потом использует; Knockout — генерирует строки и innerHTML-ит их. Поэтому генерация большого числа элементов занимает у Knockout больше времени (наглядный пример немного ниже).
Модель данных
Говоря о модели данных в Angular, обязательно стоит остановится на сервисе $scope. По сути это и есть модель данных. Поскольку Angular предполагает наличие достаточно сложной архитектуры приложения, $scope также имеет более сложную структуру.
Внутри каждого модуля создается новый экземпляр $scope, который является наследником $rootScope. Существует возможность програмно создать новый экземпляр $scope из существующего. В таком случае созданный экземпляр будет наследником того $scope, из которого он был создан. Разобратся с иерархией $scope в Angular не составит труда для тех, кто хорошо знает JavaScript. Такая возможность очень удобна, когда есть необходимость создания различных widgets, к примеру pop-ups.
Data-binding
Binding в Knockout, directive в Angular используются для расширения синтаксиса HTML, то есть для обучения браузера новым трюкам. Детально разбирать концепцию data-bindings и directives я не буду. Хочу лишь отметить, что data-binding это единственный в Knockout способ отображения данных и их связи с представлением.
Более подробно данній вопрос рассмотрен в статьях:
AngularJS: http://habrahabr.ru/post/164493/, http://habrahabr.ru/post/179755/, http://habrahabr.ru/post/180365/
KnockoutJS: http://www.knockmeout.net/2011/07/another-look-at-custom-bindings-for.html
Отдельно хочется упомянуть про наличие фильтров у Angular. Фильтры используются для форматирования выводимых на экран данных. К сожалению, Knockout для всего использует bindings.
Примеры
Fade-in animation
AngularJS: http://jsfiddle.net/yVEqU/
var ocUtils = angular.module("ocUtils", []);
ocUtils.directive('ocFadeIn', [function () {
return {
restrict: 'A',
link: function(scope, element, attrs) {
$(element).fadeIn("slow");
}
};
}]);
function MyCtrl($scope) {
this.$scope = $scope;
$scope.items = [];
$scope.add = function () {
$scope.items.push('new one');
}
$scope.pop = function () {
$scope.items.pop();
}
}
Knockout: http://jsfiddle.net/fH3TY/
var MyViewModel = {
items: ko.observableArray([]),
fadeIn: function (element) {
console.log(element);
$(element[1]).fadeIn();
},
add: function () {
this.items.push("fade me in aoutomatically");
},
pop: function () {
this.items.pop();
}
};
ko.applyBindings(MyViewModel, $("#knockout")['0']);
Думаю, что проще этого примера будет сложно что-то найти, он отлично демонстрирует синтаксис фреймворков.
Fade-out animation
AngularJS: http://jsfiddle.net/SGvej/
var FADE_OUT_TIMEOUT = 500;
var ocUtils = angular.module("ocUtils", []);
ocUtils.directive('ocFadeOut', [function () {
return {
restrict: 'A',
link: function(scope, element, attrs) {
scope.$watch(attrs["ocFadeOut"],
function (value) {
if (value) {
$(element).fadeOut(FADE_OUT_TIMEOUT);
}
});
}
};
}]);
function MyCtrl($scope, $timeout) {
this.$scope = $scope;
$scope.items = [];
$scope.add = function () {
$scope.items.push({removed: false});
}
$scope.pop = function () {
$scope.items[$scope.items.length - 1].removed = true;
$timeout(function () {
$scope.items.pop();
console.log($scope.items.length);
}, FADE_OUT_TIMEOUT);
}
}
Knockout: http://jsfiddle.net/Bzb7f/1/
var MyViewModel = {
items: ko.observableArray([]),
fadeOut: function (element) {
console.log(element);
if (element.nodeType === 3) {
return;
}
$(element).fadeOut(function () {
$(this).remove();
});
},
add: function () {
this.items.push("fade me in aoutomatically");
},
pop: function () {
this.items.pop();
}
};
ko.applyBindings(MyViewModel, $("#knockout")['0']);
Данный пример не намного сложнее, чем предыдущий, но есть несколько нюансов.
В случае с Angular, fadeOut должен быть выполнен до удаления елемента, поскольку DOM-елемнт связан с этим элементом модели и будет удален в тот же миг, когда будет удален элемент. Также важно отметить, что удаление элемента модели из массива стоит выполнять через сервис $timeout. Этот сервис по сути является оберткой для функции setTimeout и гарантирует целостность модели данных.
У Knockout возникает проблема другого характера. Функция fadeOut получает в качестве первого аргумента массив DOM-элементов, относящихся к данному элементу модели. Иногда при странном стечении обстоятельств в процессе рендеринга шаблона могут быть созданы и соответственно они будут присутствовать в получаемом массиве, поэтому необходимо делать проверку элементов прежде чем выполнять fadeOut. Также по окончанию процесса fadeOut не забывайте удалять DOM-елементы (они не удаляются автоматически).
Popup
AngularJS: http://jsfiddle.net/vmuha/EvvY7/, http://angular-ui.github.io/bootstrap/ (по второй ссылке вы найдете достаточно много хороших и полезных решений)
var ocUtils = angular.module("ocUtils", []);
function MyCtrl($scope, $compile) {
var me = this;
this.$scope = $scope;
$scope.open = function (data) {
var popupScope = $scope.$new();
popupScope.data = data;
me.popup = $("<div class="popup">{{data}}<br /><a href="#" ng-click="close($event)"> Close me</a></div>");
$compile(me.popup)(popupScope);
$("body").append(me.popup);
}
$scope.close = function () {
if (me.popup) {
me.popup.fadeOut(function () {
$(this).remove();
});
}
}
}
Knockout: http://jsfiddle.net/vmuha/uwezZ/, http://jsfiddle.net/vmuha/HbVPp/
var jQueryWidget = function(element, valueAccessor, name, constructor) {
var options = ko.utils.unwrapObservable(valueAccessor());
var $element = $(element);
setTimeout(function() { constructor($element, options) }, 0);
//$element.data(name, $widget);
};
ko.bindingHandlers.dialog = {
init: function(element, valueAccessor, allBindingsAccessor, viewModel) {
console.log("init");
jQueryWidget(element, valueAccessor, 'dialog', function($element, options) {
console.log("Creating dialog on " + $element);
return $element.dialog(options);
});
}
};
ko.bindingHandlers.dialogcmd = {
init: function(element, valueAccessor, allBindingsAccessor, viewModel) {
$(element).button().click(function() {
var options = ko.utils.unwrapObservable(valueAccessor());
$('#' + options.id).dialog(options.cmd || 'open');
});
}
};
var viewModel = {
label: ko.observable('dialog test')
};
ko.applyBindings(viewModel);
Реализовать popup можно по разному. Через директиву или байндинг и как часть ViewModel или модуля.
В Angular для popup необходимо будет создавать новый экземпляр $scope, об этом я уже упоминал выше, и использовать сервис $compile для компиляции шаблона.
В Knockout также скорей всего понадобится создание новой ModelView и вызова функции applyBindings для связи модели и представления.Думаю стоит заметить, что в случае, если для popup будет создана новая модель данных, то в Knockout возникнет проблема получения доступа к $rootModel из шаблона popup. Иерархия модели данных в Knockout построена на DOM-елементах, соответственно, если контейнер popup находится за пределами контейнера для приложения, то popup не будет иметь доступ к $rootModel.
Price formatting
AngularJS: http://jsfiddle.net/vmuha/k6ztB/1/
Knockout: http://jsfiddle.net/vmuha/6yqDw/
Performance
Перейдем к вопросу производительности. Были произведены 2 теста: холодный старт приложения “Hello World!” и рендеринг массива из 1000 элементов.
На всех схемах по вертикали — милисекунды, по горизонтали номер эксперимента.
Здесь хорошо видно, что холодный старт у Knockout происходит на много быстрее, чем у Angular.
А вот, когда речь заходит о рендеринге, здесь очевидно лидирует Angular. Как мы видим для рендеринга 1000 строк Knockout тратит до 2,5 секунд в то же время Angular хватает меньше 500 милисекунд для выполнения этой задачи. Кроме того, отображение отрендеренных элементов на экране пользователя также занимает разное время: для Angular это 1-3 секунды, а для Knockout — 14-20 секунд. Это происходит из-за того что Knockout генерирует строки, а Angular — DOM-елементы.
Резюме
Самый главный вопрос для меня заключался в определнии области применения Angular и Knockout. Проведя несколько простых експериментов, я сделал следующие выводы:
Knockout применим в случаях, когда нет необходимости в создании сложной архитектуры, сложных workflow-ов. Его основная функция — связь модели и представления, поэтому его лучше всего использовать для простых одностраничных приложений. К примеру, создание различного уровня сложности форм.
Относительно Angular я пришел к выводу, что он будет полезен в тех случаях, когда требуется создание RichUI. Настоящего и полноценного one-page приложения со сложной архитектурой и сложными связями.
P.S.:
Надеюсь, данная статья будет всем интересна. Буду рад прочитать ваши комментарии, отзывы и конструктивную критику! Желаю всем приятной работы!
Автор: vmuha