Как правильно готовить Angular

Пост составлен на основе развернутого ответа John David Miller о том, как человеку, который писал сайты с jQuery, переключится на AngularJS – мышление.

1. Проектируйте страницу в момент написания HTML

В jQuery-ориентированном подходе вы сначала верстаете страницу, а потом – делаете ее “динамической”.

В AngularJS вы должны начинать с архитектуры в самом начале. Вместо того, чтобы думать “У меня есть этот кусок DOM и я хочу заставить его делать что-то такое …”, сразу думать о том, что хотите достичь в целом(от приложения), и только затем переходить к структуре, и в самую последнюю очередь – к дизайну конкретного отображения.

2. Не пытайтесь “разогнать” jQuery с помощью AngularJS

Проще говоря: не начинайте с мысли “у меня есть jQuery, оно делает делает X, Y и Z, и я просто добавлю AngularJS для создания моделей и контроллеров”. Это очень заманчиво, особенно тогда, когда вы только начали работать с AngularJS и еще не понимаете всех концептов. Именно поэтому я всегда рекомендую начинающим AngularJS разработчикам не использовать jQuery вообще. До тек пор, пока они не почувствуют как делать “Angular-way“.

Я видел много разработчиков создающих эти запутанные решения с jQuery плагинами на 150-200 строчек кода, которые они пытаются прикрутить к AngularJS с помощью набора колбеков совместно с использованием $apply, что крайне запутано и сбивает с толку, но они таки заставляют “Это” работать. Проблема состоит в том, что в большинстве случаев jQuery плагин может быть переписан в несколько строчек AngularJS, где все бы стало на свои места и было просто и понятно.

Суть заключается в следующем: когда вы стараетесь думать “Angular-way“, даже если вы не можете сразу решить задачу – спросите Angular-сообщество, если и после этого решение не было найдено – используйте jQuery. Но не позволяйте jQuery стать костылем для всех решений, иначе вы никогда не освоите AngularJS.

3. Всегда думаете в терминах архитектуры

Single page applications – это приложение, но не в коем случае не набор веб-страниц. Поэтому мы должны думать не только как разработчики клиентской части, но так же как сервер-сайд разработчики(в плане взаимодействия данных). Мы должны думать о том, как разбить наше приложение на индивидуальные, расширяемые компоненты, которые легко будет потом покрыть тестами.

И как это сделать? Как думать Angular-way? Далее представлены основные принципы, контрастирующие с jQuery:

Место действия View

В jQuery мы программно изменяем представление(view). Рассмотрим пример выпадающего меню:

[html]
<ul class="main-menu">
<li class="active"><a href="#/home">Home</a></li>
<li><a href="#/menu1">Menu 1</a>
<ul>
<li><a href="#/sm1">Submenu 1</a></li>
<li><a href="#/sm2">Submenu 2</a></li>
<li><a href="#/sm3">Submenu 3</a></li>
</ul>
</li>
<li><a href="#/home">Menu 2</a></li>
</ul>
[/html]

В jQuery мы активируем логику нашего приложения как-то так:

[javascript]$(‘.main-menu’).dropdownMenu();[/javascript]

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

В AngularJS представление определяет логику представления. Наше объявление View будет следующим:

[html]
<ul class="main-menu" dropdown-menu>…</ul>
[/html]

Обе версии(jQuery & AngularJS) делают одно и тоже, но в случае с AngularJS – любой смотря в шаблон может понять суть. Когда новый разработчик изучает проект, он может посмотреть на этот код  и сделать вывод что тут используется директива dropdownMenu. Представление раскажет все, что ожидается в плане функциональности намного яснее, чем какой-то сторонний код, прикрепленный непонятно где.

Разработчики начиная использовать AngularJS часто задают вопрос типа: как я могу выбрать все нужные элементы и добавить к ним директиву. И они застынут от изумления, когда услышат ответ “вам не нужно искать элементы”. Но причина, по которой вы не дожны так делать: это полу-jQuery полу-AngularJS – что к добру не приведет. Проблема тут заключается в том, что разработчик пытается “делать jQuery” в контексте AngularJS. Снаружи директивы вы никогда не изменяете DOM, а директивы применяются во View. Замысел должен быть ясен.

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

Связывание данных (Data binding)

Это одна из самых крутых фишек AngularJS. Она отбрасывает кучу не нужной работы с DOM. AngularJS автоматически обновит ваше представление и вам не нужно будет этого делать! В jQuery, мы реагируем на события и обновляем контент:

[javascript]
$.ajax({
url: ‘/myEndpoint.json’,
success: function ( data, status ) {
$(‘ul#log’).append(‘<li>Data Received!</li>’);
}
});
[/javascript]

а представление выглядит так:

[html]
<ul id="log" class="messages">
</ul>
[/html]

Как видим, у нас теже проблемы. Но что более важно – мы должны в ручную обновлять DOM. А если мы захотим удалить один элемент из списка – мы тоже должны писать для этого дополнительный код. Как мы можем протестировать логику отдельно от DOM? И что, если я хочу изменить представление?

Немного хардкорно, но в AngularJS мы можем сделать так:

[javascript]
$http( ‘/myEndpoint.json’ ).then( function ( response ) {
$scope.log.push( { msg: ‘Data Received!’ } );
});
[/javascript]

И представление будет выглядеть следующим образом:

[html]
<ul class="messages">
<li class="alert" ng-repeat="entry in log">{{ entry.msg }}</li>
</ul>
[/html]

Но наше представление так же может выглядеть так:

[html]
<div class="messages">
<div class="alert" ng-repeat="entry in log">{{ entry.msg }}</div>
</div>
[/html]

Теперь мы используем бустраповские алерты вместо списка. И мы даже не прикасались к коду контроллера! Но еще важнее, что каким бы способом не обновлялся log – представление тоже обновится. Автоматически!

Связывание данных двух стороннее(two-way). Лог-сообщения могут быть редактируемыми, просто вот так:

[html]
<input type="text" ng-model="entry.msg"/>
[/html]

Разве не повод для радости?

Выделение уровня модели

В jQuery DOM является моделью, но в AngularJS у нас отдельный уровень модели, которым мы можем управлять как захотим, и полностью независимо от представления.

Разделение концептов

Ваше представление (View) определяет поведение, ваша модель представляет данные, ваш уровень сервисов служит для различных задач, которые будут выполнятся не один раз. Вы работаете с DOM и расширяете представление в директивах, и соединяете все вместе с помощью контроллеров.

Внедрение зависимости (Dependency injection)

В разделении концептов нам помогает DI. Если вы пришли с серверных языков (от Java до PHP), вы вероятно уже знакомы с данным термином, но если вы разрабатывали только клиент на jQuery, этот концепт может показаться глупым либо хипстерским. Но это далеко не так. :)

В общем случае DI дает возможность объявлять компоненты очень свободно и из разных других компонент, просто запросите экземпляр в нужном месте и он будет предоставлен. Вам не нужно задумываться о порядке загрузки, или размещении файлов или еще о чем-то таком. Мощь механизма может быть не сразу ощутима, но я приведу лишь один общий пример – тестирование.

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

4. Разработка через тестирование (Test-driven development) – всегда

На самом деле это тоже часть секции 3, но это довольно важно, поэтому выношу в отдельный пункт.

Из множества jQuery плагинов, которые вы видели/использовали, сколько из них идут покрытые тестами? – Не так много. Это потому что jQuery не очень поддается тестированию, а вот AngularJS в тестировании очень хорош.

В jQuery единственный способ протестировать – создавать отдельный от приложения компонент на демо- страничке, на которой будет произведены DOM-манипуляции. То есть, мы должны разрабатывать компонент отдельно, а потом интегрировать его в приложение. Как неудобно!

Но, так как в AngularJS мы используем разделение концептов, то мы можем легко делать TDD! Например: мы хотим простую директиву для индикации роли в меню. Мы можем объявить что мы хотим в нашем представлении:

[html]
<a href="/hello" when-active>Hello</a>
[/html]

Теперь мы можем написать тест:

[javascript]
it( ‘should add "active" when the route changes’, inject(function() {
var elm = $compile( ‘<a href="/hello" when-active>Hello</a>’ )( $scope );

$location.path(‘/not-matching’);
expect( elm.hasClass(‘active’) ).toBeFalsey();

$location.path( ‘/hello’ );
expect( elm.hasClass(‘active’) ).toBeTruthy();
}));
[/javascript]

Запускаем тест, убеждаемся, что он не проходит, и пишем директиву:

[javascript]
.directive( ‘whenActive’, function ( $location ) {
return {
scope: true,
link: function ( scope, element, attrs ) {
scope.$on( ‘$routeChangeSuccess’, function () {
if ( $location.path() == element.attr( ‘href’ ) ) {
element.addClass( ‘active’ );
}
else {
element.removeClass( ‘active’ );
}
});
}
};
});
[/javascript]

Теперь тест проходит и меню рабоает. TDD.

5. Директивы – это не запакованый jQuery код

Вы часто слышите “работа с DOM только в директиве”. Это необходимо. Отнеситесь к этому серьезно!

Но давайте нырнем глубже…

Некоторые директивы просто являются декораторами того, что уже есть в представлении (ngClass) и поэтому иногда работают с DOM напрямую(без нашего кода). Но, если директива типа “виджет” и имеет шаблон, она также должна следовать принципу разделения концептов. То есть, шаблон также должен быть независим от области его применения в link-методе и контроллере.

В AngularJS есть целый набор готовых директив, что серьезно упрощает нашу работу: с ngClass мы можем динамически обновлять классы, ngBind позволяет делать двусторонне связывание данных, ngShow и ngHide показывают и прячут элемент. Другими словами: мы можем делать “конфетку” даже без манипуляций с DOM. Чем меньше работы с DOM – тем проще тестировать директиву, а также изменять в будущем.

Я вижу многие начинающие AngularJS используют директивы как место, куда можно вкинуть jQuery код. Другими словами: они думают “если я не могу работать с DOM в контроллере, то я напишу этот код в директиве”. Это намного лучше, но все еще не angualr-way.

Обратите внимание на систему логирования, которую мы описали в разделе 3. Даже, если мы поместим jQuery код в директиву, мы по прежнему хотим делат это angular-way. Есть много случаев, когда работа с DOM необходима, но этих случаев намного меньше, чем вы думаете! До того как сделать какие-либо операции с DOM, спросите себя нужны ли они вам. Возможно есть лучший способ, который позволит обойтись без них использовав встроенные инструменты фреймворка.

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

[javascript]
.directive( ‘myDirective’, function () {
return {
template: ‘<a class="btn">Toggle me!</a>’,
link: function ( scope, element, attrs ) {
var on = false;
$(element).click( function () {
if ( on ) {
$(element).removeClass( ‘active’ );
} else {
$(element).addClass( ‘active’ );
}

on = !on;
});
}
};
});
[/javascript]

Но что-то с ним не так.

  • jQuery тут совсем не нужно.
  • даже если у нас где-то есть jQuery код на странице, то совсем не обязательно использовать его тут для совместимости.  Мы можем просто использовать angular.element и все зависимости продолжат работать в проекте без jQuery
  • даже если jQuery нам необходим для использования конкретно в этой директиве, то мы опять же всегда можем использовать angular.element, который представляет из себя jqLite, а если jQuery подключена к проекту, то ссылку на jQuery. То есть нет необходимости использовать $ вобще – вместо него лучше использовать angular.element.
  • довольно тесно идет с предыдущим пунктом: element, который получает функция link как параметр уже является экземпляром jqLite и его не нужно дополнительно оборачивать в jQuery.
  • что мы говорили о смешении шаблона и представления с логикой в предыдущей секции?

Эта директива может быть переписана следующим образом:

[javascript]
.directive( ‘myDirective’, function () {
return {
scope: true,
template: ‘<a class="btn" ng-class="{active:on}" ng-click="toggle()">Toggle me!</a>’,
link: function ( scope, element, attrs ) {
scope.on = false;
scope.toggle = function () {
scope.on = !scope.on;
};
}
};
});
[/javascript]

Опять же таки: логика и шаблон идут раздельно, тестирование делать просто, изменнеия вносить просто: как шаблона, так и логики.

Итак, директивы – это не просто коллекции jQuery-подобных функций. Но что же они? Директивы – это расширение HTML. Если HTML не позволяет сделать то, что вам нужно сделать – вы пишете директиву, и используете ее так, как будто это часть HTML.

Или по другому: если AngularJS что-то не делает “из коробки”, то подумайте, как бы вы могли это правильно сделать с ngClick, ngClass и так далее.

Итого

Даже не пытайтесь использовать jQuery. Даже не подключайте ее. И каждый раз, при желании воспользоваться $, думайте как бы вы могли сделать это в рамках AngularJS. Если не знаете – спросите! В 19ти случаях из 20ти лучший способ решения задачи не требует использования jQuery.