Заметки из мира IT

В стиле минимализм

Идеалогия AngularJS и Knockout очень близка. AngularJS и Knockout являются фреймворками для создания динамических веб-приложений, с использованием HTML в качестве шаблона. Данные фремвокри позволяют...
http://dynamic.versusit.ru/Files/2013/4f71fd4c-d1f2-47ea-b77f-6e9cb3abf70e.png

Идеалогия AngularJS и Knockout очень близка. AngularJS и Knockout являются фреймворками для создания динамических веб-приложений, с использованием HTML в качестве шаблона. Данные фремвокри позволяют расширить синтаксис HTML, чтобы позволить программисту более ясно и лаконично описывать компоненты web-приложения. Их использование позволяет избежать необходимости писать код, который раньше необходимо было писать для реализации связи model-view-controller. AngularJS и Knockout — это по сути то, чем мог бы стать HTML в связке с JavaScript, если бы они специально разрабатывались для создания современных web-приложений.

Возможности
-Data-binding: позволяет просто и удобно связывать UI и модели данных.
-Легко расширяемый инструментарий

Архитектура приложения
Angular предлагает при написании приложения разбивать его на модули. Каждый модуль состоит из:
-функции для конфигурирации модуля — она выполняется сразу после загрузки модуля;
-контроллера;
-сервисов;
-директив.

Контроллер в терминологии Angular — это функция, для контроля модели данных. Сама модель данных создается используя сервис $scope, о нем поговрим немного дальше. Директивы служат для расширения возможностей HTML.

Knockout предлагает создавать приложение, разделяя его на ModelView, которые представляю себе микс модели и контроллера. В границах объекта ko.bindingHandlers находятся data-bindings (аналоги директив Angular). Связь между моделью и представлением организуется черезobservable и observableArray.

Тут же нельзя не вспмонить про шаблон AMD (Asynchronous Module Definition). У Angular и Knockout нет собственной реализации шаблона AMD. Поэтому очень кстати оказывается библиотека RequireJS, которая зарекомендовала себя и имеет очень хорошую совместимость с Angular и Knockout. Почитать про RequireJS можно тут:http://www.kendoui.com/blogs/teamblog/posts/13-05-08/requirejs-fundamentals.aspx и http://habrahabr.ru/post/152833/.

Шаблонизация

http://habrastorage.org/storage2/d5a/bfc/c8a/d5abfcc8a733d73a1ab3265e1a04d90f.jpg

Существует огромное количество шаблонизаторов. Большинство из них работают по принципу: взять статический 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 анимация
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 анимация
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.

Форматирование цены
AngularJS: http://jsfiddle.net/vmuha/k6ztB/1/
Knockout: http://jsfiddle.net/vmuha/6yqDw/

Производительность

Перейдем к вопросу производительности. Были произведены 2 теста: холодный старт приложения “Hello World!” и рендеринг массива из 1000 элементов.

http://habrastorage.org/storage2/0ea/2b2/a42/0ea2b2a423a889343eaa878423467e83.jpg

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

http://habrastorage.org/storage2/79f/d6f/b7a/79fd6fb7ae5c346e773ae5fa3f7cd7aa.jpg

А вот, когда речь заходит о рендеринге, здесь очевидно лидирует Angular. Как мы видим для рендеринга 1000 строк Knockout тратит до 2,5 секунд в то же время Angular хватает меньше 500 миллисекунд для выполнения этой задачи. Кроме того, отображение отрендеренных элементов на экране пользователя также занимает разное время: для Angular это 1-3 секунды, а для Knockout — 14-20 секунд. Это происходит из-за того что Knockout генерирует строки, а Angular — DOM-элементы.

Резюме
Самый главный вопрос для меня заключался в определении области применения Angular и Knockout. Проведя несколько простых экспериментов, я сделал следующие выводы:

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

Относительно Angular я пришел к выводу, что он будет полезен в тех случаях, когда требуется создание RichUI. Настоящего и полноценного one-page приложения со сложной архитектурой и сложными связями.

На всех схемах по вертикали — миллисекунды, по горизонтали номер эксперимента.

admin

Яндекс.Метрика