Angular.JS: введение и основные концепции

разбираем Angular.JS на пальцах

| Категории: AngularJS, Для новичков
Дмитрий Горбунов

Подготовка

Различия версий

На момент написания (начало июля 2013) готовится к выходу AngularJS 2.0, который будет фактически представлять собой 1.1.5 с минимальными изменениями.

Отличия между 1.1.5 и 1.0.7 уже достаточно велики, и рассматривать технически устаревшую 1.0.7 нет смысла.

Установка

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

Подключение

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

Для подключения AngularJS к веб-странице достаточно подключить основной файл:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!doctype html>
<html ng-app>
<head>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.1.5/angular.js"></script>
</head>
<body>
<div>
<label>Имя:</label>
<input type="text" ng-model="yourName">
<hr>
<h1>Привет, { { yourName } }!</h1>
</div>
</body>
</html>

В примере выше используются сразу две основных концепции AngularJS: область видимости и двойное связывание данных. Рассмотрим их подробнее ниже.

Основные концепции

Область видимости

Область видимости (scope) является наиважнейшим элементом AngularJS и ведёт себя идентично области видимости переменных в классических языках программирования. Так же, как и, к примеру, в C, существует глобальная область видимости и существуют вложенные в неё дочерние. Однако, в отличие от C, области видимости в AngularJS играют намного более важную роль.

Во-первых, область видимости — не более, чем обычный объект JavaScript. Это значит, что работа с ней может осуществляться простым добавлением/изменением свойств объектов.

Во-вторых, область видимости позволяет следить за изменениями собственных свойств, а также свойств дочерних/родительских областей видимости.

В-третьих, AngularJS не работает за пределами какой-либо области видимости.

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

Применение к области видимости

Важно: если вы изменяете какой-либо объект, лежащий внутри области видимости, из-за пределов AngularJS, необходимо всегда вызывать метод $scope.$apply. Мы вернёмся к этому чуть позже.

Связывание данных

AngularJS предлагает концепцию связывания данных (data binding), работающую в обе стороны. Однако следует обратить внимание на то, что центровым объектом связывания данных всегда является область видимости. Только объекты, находящиеся внутри области видимости, могут участвовать в связывании данных.

Как это работает

Обратимся к примеру выше. В нём имеется поле ввода, к которому привязана переменная, лежащая внутри области видимости, автоматически созданной AngularJS (это не глобальная область видимости). ngModel является директивой, которая меняет поведение того или иного элемента DOM. В данном случае, эта директива связывает содержимое input и переменную в области видимости. Это не магия, это не более чем директива, которую вы легко можете реализовать самостоятельно, однако разработчики уже реализовали её, потому что такое поведение требуется очень часто.

Чисто технически, директива добавляет обработчик события изменения введённого в input значения, в котором производит вызов $scope.$apply для изменения переменной в области видимости, а также применяет обработчик $scope.$watch на области видимости, в котором меняется значение в поле ввода.

Попробуйте ввести в поле ввода любой текст и понаблюдать, как изменяется выводимый текст. Попробуйте открыть консоль и ввести $scope.yourName = "test", обратите внимание, что текст и значение в поле ввода не изменились, так как AngularJS ничего не знает про консоль и не может отследить изменения, произведённые из неё. Теперь введите $scope.$apply() и изменения обработаются AngularJS.

Важно: любые события браузера вызываются вне области видимости AngularJS, поэтому внутри ваших обработчиков таких событий необходимо вызывать $scope.$apply.

Внедрение зависимостей

Внедрение зависимостей (dependency injection, DI) является третьим краеугольным камнем AngularJS, однако он не относится непосредственно к логике работы библиотеки, а скорее к организации кода.

Грубо говоря, DI позволяет любой функции сказать: «мне нужны эти сервисы, эти и эти», после чего AngularJS автоматически предоставит эти сервисы, где бы они ни были реализованы и где бы ни находились. Разумеется, это работает только в пределах AngularJS.

Сервис

Сервис в DI — не более, чем некоторый объект, который предоставляет некоторую функциональность. Это может быть что угодно, от простой константы до фабрики классов.

Приложение

Теперь можно поговорить о том, как описанные выше концепции работают вместе для реализации приложений.

Каркас и ng-app

Вся библиотека AngularJS разбита на независимые модули, соответственно от вашего приложения ожидается такая же организация и для этого предоставлено всё необходимое.

ngApp это директива, сообщающая AngularJS: «здесь находится приложение». DOM, окружённый элементом с этой директивой будет работать под управлением AngularJS.

Создание модуля

Для начала сообщим AngularJS, что мы хотим не просто создать приложение, а подключить к нему собственную функциональность:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!doctype html>
<html ng-app="tutorial">
<head>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.1.5/angular.js"></script>
</head>
<body>
<div>
<label>Имя:</label>
<input type="text" ng-model="yourName">
<hr>
<h1>Привет, { {yourName } }!</h1>
</div>
</body>
</html>

Само приложение является просто модулем, давайте создадим его (важно: имя модуля должно совпадать со значением, переданным директиве ngApp):

var app = angular.module("tutorial", [])

Обратите внимание на эту запись, поскольку она является стандартной для всех элементов AngularJS. В особенности обратите внимание на второй параметр-массив. Это — зависимости для DI, и везде, где возможно внедрение зависимостей такая запись является предпочтительной, хотя возможны и другие варианты.

Общая форма внедрения зависимостей выглядит так:

["dependency1", "dependency2", ..., function(dependency1, dependency2, ...) {}]

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

Второй вариант, который можно использовать для обучения и в тех случаях, когда код не сжимается, выглядит так:

function(dependency1, dependency2, ...) {}

В данном случае наоборот, важны имена параметров, а не их порядок. В первом случае мы передаём имена в виде строк, которые не будут изменены при сжатии кода, во втором случае имена извлекаются из параметров функции, поэтому изменение имени параметра будет иметь плачевные последствия.

Настройка провайдеров

Провайдер — это функция, которая отвечает за создание сервисов. Если требуется перед внедрением сервиса в функцию настроить его функциональность, следует обращаться к провайдеру. Наличие/отсутствие провайдера у сервиса определяется целиком и полностью его разработчиком.

1
2
3
4
5
6
app.config(["$locationProvider", function($locationProvider) {
// обратите внимание, что я сохранил название параметра
// это полезно для читаемости кода
$locationProvider.html5Mode(true)
}])

В примере выше мы настроили работу сервиса $location, включив режим html5. Теперь приложение будет работать с полноценными URL, без использования хэшей, если браузер поддерживает HTML5 History API.

Аналогичным образом настраиваются другие сервисы.

MVC

MVC в AngularJS реализовано отдельным модулем и не играет важной роли в самой библиотеке. Вы можете пользоваться любой сторонней реализацией, или разработать собственную.

Для включения MVC в 1.1.5 не требуется дополнительных действий. В 1.1.6 (и 2.0) потребуется подключить angular-router.js и в зависимости приложения добавить ngRoute:

var app = angular.module("tutorial", ["ngRoute"])

ngRoute настраивается с помощью $routeProvider, например, так:

1
2
3
4
5
6
app.config(["$routeProvider", function($routeProvider) {
$routeProvider.when("/", {
templateUrl: "/partials/index.html",
controller: "defaultCtrl"
})
}])

Подробнее о том, что здесь происходит, вы можете прочитать в документации к модулю ngRoute или в справке по $route на сайте AngularJS.

Модели

Как таковых, моделей в AngularJS нет. Использованная выше ngModel — не более чем директива, меняющая поведение DOM, а model лишь удобное имя. Вы можете использовать к примеру модели Backbone.js, а можете вообще запрашивать данные с сервера вручную или пользоваться модулем ngResource (читайте документацию $http и $resource).

Важно помнить, что всё связывание данных происходит исключительно на области видимости. Поместили модель в область видимости? Всё отлично работает.

Контроллеры

В отличие от моделей, концепция контроллеров в AngularJS есть, однако относятся контроллеры здесь непосредственно к DOM. Рассмотрим пример:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!doctype html>
<html ng-app="tutorial">
<head>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.1.5/angular.js"></script>
</head>
<body>
<div ng-controller="defaultCtrl">
<label>Имя:</label>
<input type="text" ng-model="yourName">
<hr>
<h1>Привет, { { yourName } }!</h1>
<button ng-click="sayHello(yourName)">Сказать привет</button>
</div>
</body>
</html>

И код контроллера (заодно узнаем, как они создаются):

1
2
3
4
5
app.controller("defaultCtrl", ["$scope", function($scope) {
$scope.sayHello = function (name) {
alert("Привет, " + name);
}
}])

В контроллер необходимо внедрить хотя бы $scope, в противном случае он будет вещью в себе и не сможет ничего сделать с областью видимости, а именно для этого предназначен контроллер. Однако в 2.0 предлагается новый, упрощённый вариант создания контроллеров:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!doctype html>
<html ng-app="tutorial">
<head>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.1.5/angular.js"></script>
</head>
<body>
<div ng-controller="defaultCtlr as ctrl">
<label>Имя:</label>
<input type="text" ng-model="yourName">
<hr>
<h1>Привет, { { yourName } }!</h1>
<button ng-click="ctrl.sayHello(yourName)">Сказать привет</button>
</div>
</body>
</html>
1
2
3
4
5
app.controller("defaultCtrl", function() {
this.sayHello = function (name) {
alert("Привет, " + name);
}
})

Обратите внимание, способы не равнозначны. И this.yourName не будет равен $scope.yourName. Однако второй способ позволяет вообще не внедрять $scope в контроллер и очистить код от ненужных зависимостей. Кроме того функции контроллера не будут зависеть от $scope и их можно будет использовать в других частях приложения, а не только в одной конкретной области видимости (по этой же причине рекомендуется передавать в функции контроллера параметры, а не полагаться на $scope).

Важно: контроллер всегда создаёт внутреннюю область видимости, поэтому в примере ниже anotherName видно лишь внутри anotherCtrl, и мы всегда будем видеть «Не привет,», какой бы текст во второе поле ввода мы ни вводили. Проверьте, что будет выведено внутри anotherCtrl для yourName. Почему?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!doctype html>
<html ng-app="tutorial">
<head>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.1.5/angular.js"></script>
</head>
<body>
<div ng-controller="defaultCtrl as ctrl">
<label>Имя:</label>
<input type="text" ng-model="yourName">
<hr>
<h1>Привет, { { yourName } }!</h1>
<h1>Не привет, { { anotherName } }!</h1>
<button ng-click="ctrl.sayHello(yourName)">Сказать привет</button>
<div ng-controller="anotherCtrl">
<input type="text" ng-model="anotherName">
<h1>Ещё раз привет, { { yourName } }!</h1>
</div>
</div>
</body>
</html>

Внутри anotherCtrl существует своя область видимости, однако родительская по отношению к нему область видимости также доступна из него. В данном случае { { yourName } } правильнее было бы записать как { { $parent.yourName } }. При этом выражение с yourName будет верно вычислено в обоих случаях и мы увидим «Ещё раз привет, Вася», если ввести «Вася» в поле ввода, относящееся к контроллеру defaultCtrl.

У каждой области видимости есть свойства $parent (указывает на родительскую область видимости) и $root (указывает на глобальную область видимости).

Важно: контроллер должен существовать, иначе AngularJS выдаст ошибку.

Не рекомендуемый способ создавать контроллеры:

1
2
3
function anotherCtrl($scope) {
}

В данном случае вы не только засоряете глобальную область видимости JS (не область видимости AngularJS), но ещё и лишаетесь возможности грамотно организовать DI.

ngView

Директива ngView позволяет динамически подгружать часть HTML в зависимости от значения templateUrl в настройках $routeProvider. Это делается $route автоматически, равно как и подключение указанного контроллера.

1
<div ng-view></div>

Важно: стандартная реализация поддерживает лишь один ngView на приложение. Вложенные ngView также не поддерживаются.

Для расширенных возможностей MVC рекомендуется использовать AngularUI-Router, поддерживающий вышеописанный функционал.

Шаблоны и выражения

Здесь мы поговорим о V из MVC применительно к AngularJS. Раздел вынесен отдельно ввиду большой важности.

Выражения

Выражением в AngularJS считается практически любое выражение JavaScript. Вы можете выполнять математические операции, вызывать функции и т.д. Существуют ограничения, с которыми вы можете ознакомиться в документации, мы же рассмотрим для чего используются выражения.

Выражение в AngularJS заключается в двойные фигурные скобки и может быть использовано непосредственно в DOM. AngularJS вычисляет выражение и подставляет его значение прямо в DOM. При этом используются все преимущества связывания данных: значение выражения обновляется с изменением входящих в него переменных (или результатов вычисления функции).

1
2
3
4
5
6
7
8
<p>{ { somefunc() } }</p>
<input type="text" ng-model="counter">
$scope.somefunc = function () {
return parseInt($scope.counter) + 1
}
$scope.counter = 5

Здесь я использовал не рекомендованный стиль кода с привязкой к $scope внутри обработчика, поскольку это нагляднее показывает, что связывание данных в AngularJS работает даже в том случае, если выражение не содержит прямой ссылки на данные.

Шаблоны

Шаблоны в AngularJS это не более, чем обычный HTML, расширенный с помощью директив и выражений. Выражения используются для вывода данных, директивы — для расширения функциональности HTML, превращая его в DSL (Domain Specific Language) для вашего конкретного приложения. Вы можете добавить директиву, создающую интерфейс с вкладками, можете добавить директиву, которая выводит в цикле содержимое массива с данными (такая директива встроена в AngularJS), можете выводить HTML в зависимости от значения той или иной переменной и так далее. Всё ограничено лишь вашей фантазией. Подлинная мощь AngularJS заключена именно в директивах, а обычный <div>, расширенный директивой может превратиться во что угодно.

Создание директив — расширенная тема, к которой мы обратимся позже, равно как и к созданию сервисов.

Читайте так же статьи по теме: