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