Персональный блог Игоря Антонова aka "spider_net"

Введение в Angular.JS. Героический фреймворк от Google

Введение в Angular.JS. Героический фреймворк от Google

С самого появления JavaScript получил репутацию гадкого утенка. Лишь спустя годы ему удалось стать настоящей рок-звездой среди языков программирования. Создатели ему пророчили грандиозное будущее, но осуществиться этой мечте, было суждено не скоро. Профессиональные разработчики, хорошо знакомые с разработкой на Java, сразу откинули JavaScript в сторону за неуклюжесть и отсутствие привычного объектно-ориентированного подхода в классическом понимании. Низкая скорость первых версий интерпретаторов, отсутствие кросс-браузерности, нюансы работы некоторых операторов и куча других неочевидных вещей напрочь отбили охоту писать код на этом языке у профессионалов.

Невостребованность и отсутствие четкого позиционирования затормозили развитие языка и превратили в инструмент, пригодный для совсем элементарных вещей. Создания простейших калькуляторов и оживление отдельных элементов страницы – вот удел языка с большими перспективами. Черная полоса JavaScript продолжалась вплоть до 2006 года, пока Джон Резиг не представил альфа версию библиотеки jQuery. Крохотной библиотеки было суждено сыграть роль революционера и заставить разработчиков взглянуть на JavaScript с другой стороны. Библиотека элегантно решила ряд проблем (взаимодействие с Ajax, кросс-браузерность, работа с DOM) и web-разработчики повсеместно начали внедрять jQuery в свои проекты.

Библиотека jQuery стала инструментов номер один в разработке web-приложений. Тысячи вдохновленных разработчиков подхватили новую волну и начали вносить свой вклад в виде разнообразных плагинов. Плагины, решающие определенные задач стали первым шагом в сторону модульности и повторного использования кода.

JavaScript получил второе рождения: jQuery решила ряд проблем, разработчики начали оптимизировать интерпретаторы, позволив JS-коду исполнятся существенно быстрей. Улучшения были на лицо, но новая проблема не заставила себя долго ждать.

Количество кода на JavaScript стало зашкаливать, и типичное web-приложение стало превращаться в лапшу из html/css/js кода. Никто не задумывался о паттернах, методах разработки и других проверенных временем практикам разработки. Все наслаждались простой разработки и распихивали тонны кода по всему приложению. Популярность, низкий порог вхождения очередную волну хаоса и создали в нестабильном мире JS новую глобальную проблему.

Новая ступень развития JavaScript

Взрослые языки программирования, прошедшие длинный путь становления, прекрасно отработали различные паттерны проектирования. Оставалось лишь собрать лучшие из них и перенести в мир JavaScript. Так стали появляться первые фреймворки. В основе большинства лежал модный архитектурный MV*-паттерн (MVC, MVVM, MVP, HMVC и т.д.) и само собой библиотека jQuery (чуть позже начали избавляться и от нее).

Первые фреймворки эффективно решали вопросы структуры разрабатываемого приложения, и боролись с хаосом с помощью стандартов. JavaScript поднялся на очередную ступень вверх и вновь заставил обратить на себя внимание.

Разработчики получили готовый каркас, стандарты кодирования, функциональные компоненты и возможность сосредоточится на решении собственных задач, а не изобретении очередной структуры.

Angular.JS – героический фреймворк

Впервые angular.js был представлен в 2009 году двумя разработчиками Misko Hovery и Adam Abrons из Brat Tech LLC. Чуть позже Adam покинул проект, и дальнейшим развитием angular.js стал заниматься Misko Hovery (в тот момент сотрудник Google) со своими коллегами. Так angular перекатился под крыло всемирно известной корпорации.

Ключевой компонент angular.js – директивы, позволяющие естественным образом расширить возможности html. Они элегантно добавляются к существующим тегам и прокидывают невидимый мост между html и JavaScript. Их можно сравнить с кирпичиками, из которых строится приложение.

Из коробки в angular.js поставляется готовый набор директив, признанный облегчить решение типичных задач. Любой разработчик при необходимости может создать собственные директивы, тем самыми расширив функциональность.

Преимущества angular.js

Добавляя в приложение очередной элемент управления, который должен взаимодействовать с другими, сразу сталкиваешься с проблемой синхронизации. Нужно не забывать прописывать в нужных местах обработчики и надеяться, что они корректно сработают.

Двухстороннее связывание в Angular.js эффективно решает эту проблему. Добавляем соответствующую директиву к элементам управления и получаем синхронизацию в обе стороны. Изменения модели отражаются на представлении, а редактирование представление тут же обновляет модель.

При разработке сложных приложений, двухстороннее связывание существенно упрощает взаимодействие с представлением. Разработчик всегда имеет возможность поменять представление (например, строку ввода поменять на выпадающий список), сохранив связь с моделью.

Декларативный подход выделяет angular.js среди других фреймворков. Разработчики не стали ввязываться в священные войны против HTML. Вместо этого они попытались расширить возможности языка разметки и вывести его на новый уровень с помощью дополнительных директив. Не хватает возможностей – просто создаем директиву в соответствии со своими предпочтениями.

Другая сильная сторона angular.js – безграничные возможности тестирования. Тестировать можно абсолютно все, и чем раньше разработчик приучит себя к написанию тестов, тем больше ему удастся экономить времени.

Низкий порог вхождений можно смело отнести к преимуществам. Начать разработку простых вещей на angular.js достаточно легко, но по мере их усложнения, придется не раз обратиться к источникам знаний и переосмыслить уже имеющиеся.

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

Вопросы производительности angular.js – одна из самых наболевших тем. «Тормозит, ваш angular с крутым двухстороннем связыванием!» - типичная реплика во время сессии вопросов. Любой ненавистник angular.js может несколькими строками кода продемонстрировать тормоза во всей красе.

Перед тем как ругаться на снижение производительности, необходимо досконально разобраться в предмете. В angular.js есть такое понятие как область видимости (scope). Эти объекты связаны с моделью данных и выступают связующим звеном между представлением и контроллером. Все что находится в области видимости – подвергается строгому «наблюдению», тем самым позволяя директивам моментально реагировать на изменение состояния или значения.

Отсюда следует, что область видимости != модель, и не нужно пытаться в нее запихнуть все данные из модели. Ничего хорошего из этого все равно не выйдет. Область видимости – модель представления (вспоминаем про паттерн MVVM) и должна содержать только данные, необходимые для текущего отображения. Разве есть много задач, где требуется отображать несколько тысяч элементов?

Хорошо, вроде все ясно – делаем по спецификации, избавляемся от проблем с производительностью, но из-за чего несколько тысяч объектов в области видимости вызывают подобные проблемы? Ведь по современным меркам, это совсем немного.

Проблема кроется в реализации двухстороннего связывания. Чем больше объектов, тем больше создается наблюдателей ($watch). Они должны оперативно сообщать директивам об изменении свойств, чтобы те в свою очередь успели обновить DOM. Обновляются они очень часто и соответственно, чем больше объектов, тем больше тормозов.

Логотип angular.js

Классический пример

Продемонстрировать angular.js в работе я решил на классическом примере. Мы попробуем разработать простейший список задач (TODO лист). Подобные приложения уже стали своеобразным «Hello World» в мире JavaScript. Каждый новый фреймворк, обязательно комплектуется подобным демонстрационным примером, чтобы разработчик мог сравнить решение одной задачи разными инструментами.

Полный вариант исходного кода будет доступен на сайте журнала и на моем персональном блоге. Теперь определимся с функционалом будущей программы. Что должен уметь любой список задач? Как минимум добавлять/удалять задачи и сохранять добавленные пользователем данные. Попробуем реализовать все выше сказанное на практике.

Начнем с хранилища. В реальном приложении подобные вещи лучше всего хранить на сервере. Angular.js работать на сервере не может, поэтому серверную часть нужно писать с использованием другой технологии. Поскольку в рамках статьи нас интересует клиентская часть, то для упрощения задачи воспользуемся локальным хранилищем (localStorage). Браузеры его поддерживают давно, так что проблем с совместимостью возникнуть не должно.

Для нового проекта я приготовил структуру:

/lib  - различные библиотеки, в примере только angular.js
/css – стили оформления;
/js – JavaScript сценарии
	/controllers – контроллеры;
	/services – сервисы;
	app.js – точка входа в приложения;
 index.html – здесь будет представление и подключение всех сценариев;

Разработку начнем с app.js. В нем определим единственный модуль для нашего приложения. Назовем его "todomvc". Функционал всегда крайне желательно выносить в отдельные модули. Это упрощает поддержку, уменьшает связь между компонентами и верный путь к повторному использованию кода.

Следующим шагом рассмотрим создание сервиса для работы с локальным хранилищем. Для этого создадим файл todoStorage.js в директории сервисов и пропишем в него код из листинга 1. Создание новой службы в angular.js должно выполняться с помощью метода factory(). Он должен вернуть объект службы.

Служба todoStorage обладает двумя методами – get и put. Первый позволяет считать данные локального хранилища, второй наоборот, записывает их. Хранить данные будем в привычном формате JSON. Для этого перед записью в хранилище выполняем преобразование с помощью метода stringify(), а при извлечении конвертируем обратно в объект методом parse().

Листинг 1. Код службы todoStorage

todomvc.factory('todoStorage', function () {
	var STORAGE_ID = 'todos-angularjs';
	return {
		get: function () {
			return JSON.parse(localStorage.getItem(STORAGE_ID) || '[]');
		},
		put: function (todos) {
			localStorage.setItem(STORAGE_ID, JSON.stringify(todos));
		}
	};
});

Теперь приступим к кодированию единственного контроллера. Он будет содержать бизнес-логику приложения. Его код приведен в листинге 2. Листинг начинается с определения нового контроллера в модуле todomvc. Новый контроллер создается с помощью одноименной службы (controller). В качестве параметров она принимает: имя контроллера и функцию-фабрику. В ней требуется описывать весь код и в параметрах определить сервисы, которые будут доступны в пределах контроллера.

Особое внимание стоит обратить на внедрение сервисов. В параметры мы передаем $scope и наш самописный сервис todoStorage. Все эти службы будут доступны в контроллере благодаря технологии внедрении зависимостей (dependency injection).

В самой первой строке контроллера выполняется инициализация модели (var todos). В нее мы помещаем данные из $scope или из локального хранилища. Доступ к хранилищу получаем через созданную нами службу todoStorage.

Модель готова и нужно начинать за ней наблюдение с помощью метода $watch. При любом изменении данные будут сразу же отправляться в локальное хранилище. Таким образом, можно быть уверенным, что при случайном закрытии страницы с приложением ничего не потеряется.

Далее остается реализовать методы для взаимодействия моделью. Метод addTodo() служит для добавления новой задачи, editTodo() для редактирования и т.д. Рассказывать тут особо нечего, т.к. показана банальная работа с массивами в JavaScript. Важно лишь, что все методы описаны в контексте $scope, чтобы потом мы смогли к ним обратиться из представления.

Листинг 2. Код контроллера

todomvc.controller('TodoCtrl', function TodoCtrl($scope,  todoStorage) {
	var todos = $scope.todos = todoStorage.get();
	$scope.$watch('todos', function () { 
			todoStorage.put(todos);
	}, true);
	$scope.addTodo = function () {
		if (!$scope.newTodo.length) {
			return;
		}
		todos.push({
			title: $scope.newTodo,
			completed: false
		});
		$scope.newTodo = '';
	};
	$scope.editTodo = function (todo) {
		$scope.editedTodo = todo;
	};
	$scope.doneEditing = function (todo) {
		$scope.editedTodo = null;
		if (!todo.title) {
			$scope.removeTodo(todo);
		}
	};
	$scope.removeTodo = function (todo) {
		todos.splice(todos.indexOf(todo), 1);
	};

Контроллер полностью готов и остается только подготовить представление - сделать разметку (html) списка задач и поставить в нужные места соответствующие директивы. Полный листинг представления я приводить не стал, т.к. большая его часть обычный html документ. Вместо этого в третьем листинге я выделил кусок, описывающий формирование списка добавленных задач.

helloworldangular.png

Листинг 3. Пример использования директив в представлении

<ul id="todo-list">
<li ng-repeat="todo in todos " ng-class="{completed: todo.completed, editing: todo == editedTodo}">
<div class="view">
<input class="toggle" type="checkbox" ng-model="todo.completed">
<label ng-dblclick="editTodo(todo)">{{todo.title}}</label>
<button class="destroy" ng-click="removeTodo(todo)"></button>
</div>
<form ng-submit="doneEditing(todo)">
<input class="edit" ng-model="todo.title" todo-blur="doneEditing(todo)" todo-focus="todo == editedTodo">
</form>
</li>
</ul>

Самая интересная часть представления начинается в теге li. К нему добавлены две директивы: ngRepeat и ngClass. Первая позволяет организовать типичный цикл. Нам лишь нужно указать переменную, в которую будет попадать очередной элемент коллекции, и саму коллекцию. Наша коллекция – это модель todos.

Директива переберет всю коллекцию и сформирует элементы списка. В теле тега мы определяем дополнительные элементы и выводим соответствующие данные из модели. ngClass позволяет динамически определять css-класс для html элемента на основании выражения.

В теле тега li я определяю дополнительные элементы управления, в которых тоже применяются директивы. Например, для кнопки, по нажатию которой будет выполняться удаление задачи из списка, применяется директива ngClick.

Приложение «Список задач»

Создание собственных директив

Разработчики angular.js снабдили фреймворк лишь базовыми директивами, без которых невозможно создать простейшее приложение. Как только требуется выйти за установленные рамки и решить нестандартную задачу, разработчик сталкивается с необходимостью написания собственной директивы. К счастью, в разработке директив нет ничего сложного. Достаточно ознакомиться с документацией и немного потренироваться.

Попробуем рассмотреть процесс создания собственных директив на практике. Любому angular.js-разработчику рано или поздно предстоит столкнуться с необходимостью интеграции jQuery-плагинов в свое приложение. Вот так просто подключить соответствующий JS файл нельзя. Точней можно, но подобный подход идет в разрез с идеологией angular и не факт, что кустарное подключение плагина позволит нормально работать с ним из контроллера.

Представим, что нам требуется воспользоваться в своем проекте возможностями библиотеки jQuery UI. Она предоставляет кучу готовых виджетов элементов управления, которые стали уже стандартом при разработке web-приложений. Возьмем компонент DatePicker (выпадающий календарь) и попробуем воспользоваться им в angular.js-проекте.

Чтобы сэкономить место в статье, я воспользуюсь готовым каркасом angular.js-приложения (см. врезку) «angular-seed». Первым делом откроем файл «index.html» и напишем в нем подключение необходимых файлов – библиотека jQuery/jQuery UI и стили оформления. В коде это выглядит так:

<link rel="stylesheet" href="css/jquery-ui-1.9.2.custom.css" />
<script src="js/jqury-1.8.2.js"></script>
<script src="js/jquery-ui.js"></script>

Код директивы будем описывать в файле directives.js. В нем напишем код из листинга 4. Метод «directive» регистрирует новую директиву. В нем нам требуется описать функцию фабрику, которая вернет объект с информацией, необходимую сервису $compile для компиляции HTML/DOM в шаблон.

Свойств у возвращаемого объекта много, рассмотрим самые важные:

  • require – перечисление директив, которые будут передаваться в контроллер. Их может быть несколько, но для примера необходима лишь "ngModel", обеспечивающая двухсторонний биндинг.
  • link – связующая функция. В функции описывается инициализация плагина DatePicker и процесс передачи данных из представления (в рамках примера это выбранная дата) в контроллер. Передача осуществляется при возникновении события onSelect виджета DatePicker. Что касается процесса передачи значения, то он выполняется при помощи метода $setViewValue. Оповещение $scope о изменениях выполняется с помощью метода apply().

Листинг 4. Код директивы myDatepicker

directive('myDatepicker', function() {
   return {
        require: 'ngModel',                        
        link: function (scope, element, attrs, ngModelCtrl) {                                                                                                                  
        $(function() {                                                                                                  
            element.datepicker({
            showOn: 'both',
            changeYear: true,
            changeMonth: true,
            dateFormat: 'dd.mm.yy',
            maxDate: '+1Y',
            yearRange: '1920:2015',
            onSelect:function (dateText, inst) {
              ngModelCtrl.$setViewValue(dateText);
              scope.$apply();
            }
         });
     });                                          
  }
 }
});

Директива-обертка для плагина DatePicker полностью готова и подключить ее к элементу управления можно следующим образом:

<input ng-model="myDateOfBirth" my-datepicker />

Полезные материалы о angular.js

Перед тем как приступать к изучению фреймворка, советую определиться со своим уровнем в JavaScript. При полном отсутствии опыта разработки на этом языке рекомендую отодвинуть angular.js в сторонку и проштудировать хорошую книгу по JS. Фреймворк упрощает разработку, но без знания фундамента, прочувствовать это не удастся. Скорей наоборот, легко погрязнуть в новых проблемах.

Первым шагом на пути к освоению angular.js должен стать официальный краткий туториал. Профессионалом после прохождения гайда стать не получится, но познакомиться с фреймворком на примере разработки реального приложения удастся.

Официальный сайт angular.js

Дальше я советую прослушать хороший бесплатный курс «Shaping up with angular.js» на CodeSchool. Все опять же на английском, но материал стоит того, чтобы пополнить свой словарный запас. Автор разбирает angular.js с самого начала и помимо тривиальных вещей рассматривает более сложные вещи.

Если по каким-то причинам «Shaping up with angular.js» породил больше вопросов, чем ответов, то стоит его отложить и посмотреть уроки на EggHead.io. Здесь angular.js разбирается с самых основ, поэтому они идеально подходят для новичков.

Проект EggHead

Перечисленных выше источников хватит, чтобы начать разрабатывать приложения. Вопросы возникать будут и ответы на них можно получать на StackOverflow и официальной группе в Google Groups. Что касается свежих материалов об angular.js, то их находить поможет официальный твиттер аккаунт и хэш тэг «#angularjs». Кстати, я тоже частенько публикую заметки о angular.js на своем блоге.

Вместо заключения

Angular.js – прекрасный инструмент, позволяющий немного по другому взглянуть на создание web-приложений. В некоторых случаях он сильно упрощает разработку благодаря своим уникальным функциям вроде директив или биндинга, но иногда приходится мириться с его ограничениями. Так или иначе, будущее у этого инструмента есть. Стремительный рост сообщества и приложений – лишнее тому подтверждение. Получился из angular.js идеальный фреймворк? Нет, он стал одним из лучших современных инструментов для разработки web-приложений.

Исходники примера к статье Введение в angular.js

Статья опубликована в журнале "Системный администратор" (http://samag.ru/). Сентябрь 2014 г.

Ссылка на журнал: http://goo.gl/rB6GgQ

Оставьте комментарий!
comments powered by HyperComments