AngularJS vs. KnockoutJS

от автора

Добрый день уважаемые, хабрачеловеки.
В данной статье я хочу поделиться с вами своим опытом работы с такими фреймворками как 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.:

Надеюсь, данная статья будет всем интересна. Буду рад прочитать ваши комментарии, отзывы и конструктивную критику! Желаю всем приятной работы!

ссылка на оригинал статьи http://habrahabr.ru/post/187808/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *