На настоящий момент AngularJS — один из самых популярных javascript фреймворков. Его использование упрощает процесс разработки, делая AngularJS великолепным инструментом для создания небольших веб-приложений, но возможности фреймворка не ограничиваются этим и позволяют разрабатывать большие, наполненные разнообразным функционалом приложения. Комбинация легкости в разработке и большое количество возможностей привели к широкому распространению, а вместе с распространением появились типичные, часто встречающиеся ошибки. В этом топике описаны наиболее распространенные ошибки, встречающиеся при разработке на AngularJS больших проектов.
1. Структура папок, соответствующая MVC приложениям
AngularJS является MVC фреймворком. Несмотря на то, что модели в нем определяются не настолько явно, как в случае с backbone.js, общий архитектурный стиль остается тем же. Часто используемой практикой при использовании MVC фреймворков является группировка файлов по следующему шаблону:
templates/
_login.html
_feed.html
app/
app.js
controllers/
LoginController.js
FeedController.js
directives/
FeedEntryDirective.js
services/
LoginService.js
FeedService.js
filters/
CapatalizeFilter.js
Подобный подход встречается часто, особенно у разработчиков, имеющих опыт разработки на RoR. Тем не менее при росте приложения использование подобной структуры папок приводит к тому, что в каждый момент времени приходится держать открытыми несколько папок. Чем бы вы не пользовались — Sublime, Visual Studio или Vim с NerdTree — при перемещении по дереву каталогов вы постоянно будете тратить время на скроллинг. Чтобы избежать этого, вы можете группировать файлы по функционалу, а не по типу:
app/
app.js
Feed/
_feed.html
FeedController.js
FeedEntryDirective.js
FeedService.js
Login/
_login.html
LoginController.js
LoginService.js
Shared/
CapatalizeFilter.js
Подобная структура папок делает гораздо более простым поиск связанных файлов, относящихся к одной и той же фиче, что способно ускорить процесс разработки. Да, это может показаться спорным — хранить в одной папке html файлы вместе с js, но эффект от экономии времени может оказаться более важным.
2. Модули (или их отсутствие)
Часто, в начале разработки проекта весь функционал складывается в единый модуль. До какого-то момента подобный подход работает, но по мере развития проекта код становится неуправляемым.
var app = angular.module('app',[]);
app.service('MyService', function(){
//service code
});
app.controller('MyCtrl', function($scope, MyService){
//controller code
});
Следующим по распространенности подходом является группировать объекты по их типу:
var services = angular.module('services',[]);
services.service('MyService', function(){
//service code
});
var controllers = angular.module('controllers',['services']);
controllers.controller('MyCtrl', function($scope, MyService){
//controller code
});
var app = angular.module('app',['controllers', 'services']);
Подобный подход масштабируется тоже не лучшим образом, подобно структуре каталогов из пункта 1. Чтобы добиться лучшей масштабируемости, мы последуем той же самой концепции разбиения кода по фичам:
var sharedServicesModule = angular.module('sharedServices',[]);
sharedServices.service('NetworkService', function($http){});
var loginModule = angular.module('login',['sharedServices']);
loginModule.service('loginService', function(NetworkService){});
loginModule.controller('loginCtrl', function($scope, loginService){});
var app = angular.module('app', ['sharedServices', 'login']);
Разнесение функционала по различным модулям также дает возможность повторного использования кода в различных проектах.
3. Внедрение зависимостей
Внедрение зависимостей (dependency injection) одна из лучших возможностей, предоставляемых AngularJS. DI облегчает процесс тестирования и делает код чище. AngularJS очень гибок в вопросе того, как зависимости могут быть внедрены. Самый простой способ — это передать зависимость в функцию в качестве параметра:
var app = angular.module('app',[]);
app.controller('MainCtrl', function($scope, $timeout){
$timeout(function(){
console.log($scope);
}, 1000);
});
Из кода ясно, что MainCtrl зависит от $scope and $timeout. Это прекрасно работает до того момента, как проект пойдет в продакшн и вы захотите минифицировать ваш код. Использование UglifyJS к вышеуказанному коду приведет к следующему:
var app=angular.module("app",[]);app.controller("MainCtrl",function(e,t){t(function(){console.log(e)},1e3)})
Теперь AngularJS никак не узнает от чего в реальности зависит MainCtrl. Чтобы этого не происходило, есть очень простое решение — передавать зависимости как массив строк, с последним элементом в виде функции, принимающей все перечисленные зависимости в виде параметров:
app.controller('MainCtrl', ['$scope', '$timeout', function($scope, $timeout){
$timeout(function(){
console.log($scope);
}, 1000);
}]);
Код выше будет преобразован минификатором в код, который AngularJS уже сможет корректно интерпретировать:
app.controller("MainCtrl",["$scope","$timeout",function(e,t){t(function(){console.log(e)},1e3)}])
3.1. Глобальные зависимости
Часто, при разработке AngularJS приложения, появляется необходимость в использовании объектов, доступных в любой точке приложения. Это ломает стройную модель, основанную на внедрении зависимостей, и приводит к возникновению багов и усложнению процесса тестирования. AngularJS позволяет оборачивать подобные объекты в модули так, что они могут быть внедрены подобно обычным AngularJS модулям. К примеру, великолепная библиотека Underscore.js может быть завернута в модуль следующим образом:
var underscore = angular.module('underscore', []);
underscore.factory('_', function() {
return window._; //Underscore must already be loaded on the page
});
var app = angular.module('app', ['underscore']);
app.controller('MainCtrl', ['$scope', '_', function($scope, _) {
init = function() {
_.keys($scope);
}
init();
}]);
Это позволяет приложению использовать единый стиль с обязательным внедрением зависимостей и оставляет возможность тестировать модули в отрыве от функционала их зависимостей.
4. Раздувание контроллеров
Контроллеры — это основа AngularJS. И часто, особенно новички, пишут в контроллерах слишком много логики. Контроллеры не должны осуществлять манипуляции с DOM или содержать DOM селекторы, для это существуют директивы. Точно также и бизнес-логика должна находиться в сервисах. Данные также должны храниться в сервисах (за исключением случаев когда данные привязаны к $scope), поскольку сервисы, в отличие от контроллеров, синглтоны, чье время жизни совпадает со временем жизни самого приложения.При разработке контроллеров лучше всего следовать принципу единственной ответственности (SRP) и считать контроллер координатором между представлением и моделью, в этом случае логики в нем будет минимум.
5. Service vs Factory
Эти именования приводят в конфуз каждого новичка в AngularJS, хотя в реальности они почти одинаковы.Посмотрим исходные коды AngularJS:
function factory(name, factoryFn) {
return provider(name, { $get: factoryFn });
}
function service(name, constructor) {
return factory(name, ['$injector', function($injector) {
return $injector.instantiate(constructor);
}]);
}
Функция service просто вызывает функцию factory, в которую обернут вызов функции provider. Если service просто вызывает функцию factory, в чем разница между ними? Смысл в $injector.instantiate, внутри которой $injector создает новый экземпляр функции конструктора сервиса. Пример сервиса и фабрики, выполняющих одинаковые действия:
var app = angular.module('app',[]);
app.service('helloWorldService', function(){
this.hello = function() {
return "Hello World";
};
});
app.factory('helloWorldFactory', function(){
return {
hello: function() {
return "Hello World";
}
}
});
В момент, когда helloWorldService или helloWorldFactory будут инжектированы в контроллер, они обе будут иметь единственный метод, возвращающий «Hello World». Поскольку все провайдеры синглтоны, у нас всегда будет только один экземпляр сервиса и один экземпляр фабрики. Так почему же существуют одновременно и фабрики и сервисы, если они выполняют одну и ту же функцию? Фабрики предоставляют больше гибкости, поскольку они могут возвращать функцию, которая может создавать новые объекты. В ООП фабрика представляет собой объект, создающий другие объекты:
app.factory('helloFactory', function() {
return function(name) {
this.name = name;
this.hello = function() {
return "Hello " + this.name;
};
};
});
Вот пример контроллера, использующего сервис и две фабрики:
app.controller('helloCtrl', function($scope, helloWorldService, helloWorldFactory, helloFactory) {
init = function() {
helloWorldService.hello(); //'Hello World'
helloWorldFactory.hello(); //'Hello World'
new helloFactory('Readers').hello() //'Hello Readers'
}
init();
});
Фабрики также могут быть полезны при разработке классов с приватными методами:
app.factory('privateFactory', function(){
var privateFunc = function(name) {
return name.split("").reverse().join(""); //reverses the name
};
return {
hello: function(name){
return "Hello " + privateFunc(name);
}
};
});
6. Неиспользование Batarang
Batarang — это расширение браузера Chrome для разработки и дебага AngularJS приложений. Batarang позволяет:
- просматривать модели привязанные к скоупам
- строить граф зависимостей в приложении
- производить анализ производительности приложения
Несмотря на то, что производительность AngularJS неплохая «из коробки», при росте приложения, с добавлением кастомных директив и сложной логики, приложение может начать подтормаживать. Используя Batarang легко разобраться, какая из функций тратит много времени при вызове. Batarang также отображает дерево наблюдателей (watch tree), которое может быть полезным при использовании большого числа наблюдателей (watchers).
7. Слишком много наблюдателей
Как было отмечено выше, AngularJS довольно производителен из коробки. Но, когда количество наблюдателей достигнет числа 2000, $digest цикл, в котором происходит проверка изменения данных, может начать замедлять работу приложения. Хотя достижение числа 2000 не гарантирует замедления, это хорошая стартовая точка, с которой уже можно начинать беспокоиться. С помощью следующего кода можно узнать количество наблюдателей на странице:
(function () {
var root = $(document.getElementsByTagName('body'));
var watchers = [];
var f = function (element) {
if (element.data().hasOwnProperty('$scope')) {
angular.forEach(element.data().$scope.$$watchers, function (watcher) {
watchers.push(watcher);
});
}
angular.forEach(element.children(), function (childElement) {
f($(childElement));
});
};
f(root);
console.log(watchers.length);
})();
Используя код выше и дерево наблюдателей батаранга, вы можете посмотреть, есть ли у вас наблюдатели-дубликаты или наблюдатели над неизменяемыми данными. В случае с неизменяемыми данными вы можете использовать директиву bindonce, чтобы не увеличивать количество наблюдателей на странице.
8. Наследование скоупов ($scope's)
Наследование в JS основанное на прототипах отличается от классического наследования на классах. Обычно это не является проблемой, но эти нюансы могут проявляться при работе со скоупами. В AngularJS обычный (не изолированный) $scope наследован от родительского до самого старшего предка $rootScope. Общая модель данных, разделяемая родительским скоупом с дочерним, организуется легко благодаря наследованию на прототипах. В следующем примере мы хотим, чтобы имя пользователя одновременно отображалось в двух элементах span, после того как пользователь введет свое имя.
<div ng-controller="navCtrl">
<span>{{user}}</span>
<div ng-controller="loginCtrl">
<span>{{user}}</span>
<input ng-model="user"></input>
</div>
</div>
Теперь вопрос: когда пользователь введет свое имя в текстовое поле, в каких элементах оно будет отображаться:navCtrl, loginCtrl, или в обоих? Если ваш ответ — loginCtrl, вы понимаете, как работает наследование основанное на прототипах. В поиске по строковым полям цепочка прототипов не используется. Чтобы добиться желаемого поведения, нам желательно использовать объект для корректного обновления имени пользователя в дочернем и родительском $scope. (Напоминаю, что в JS функции и массивы также являются объектами.)
<div ng-controller="navCtrl">
<span>{{user.name}}</span>
<div ng-controller="loginCtrl">
<span>{{user.name}}</span>
<input ng-model="user.name"></input>
</div>
</div>
Теперь, поскольку переменная user — объект, цепочка прототипов будет работать и элемент span в navCtrl будет корректно обновлен вместе с loginCtrl. Это может выглядеть неестественным примером, но при работе с директивами, создающими дочерние скоупы (подобно ngRepeat), подобные моменты будут возникать.
9. Использование ручного тестирования
До тех пока вы не начнете использовать TDD в своей работе, вам придется каждый раз запускать проект и проводить ручное тестирование, чтобы удостовериться, что ваш код работает. Нет оправданий для использования подобного подхода в случае с AngularJS. AngularJS был изначально спректирован таким образом, чтобы разработанный на нем код был тестируемым. DI, ngMock — ваши лучшие помощники в этом. Также есть несколько инструментов, способных вас перенести на следующий уровень.
9.1 Protractor
Юнит-тесты — это основа для построения полноценно покрытого тестами приложения, но с ростом проекта использование интеграционных тестов может быть более эффективным для проверки, насколько жизнеспособен код в приложении. К счастью, команда AngularJS разработала замечательный инструмент — Protractor, способный имитировать взаимодействие с пользователем. Protractor использует фреймворк Jasmine для написания тестов и обладает хорошим API для описания различных сценариев взаимодействия. Среди множества различных инструментов для тестирования у Protractor есть преимущество в понимании внутреннего устройства AngularJS, что особенно полезно, когда вы имеет дело с чем-то наподобие $digest циклов.
9.2. Karma
Команда проекта AngularJS не ограничилась написанием инструментария для разработки тестов. Также был разработан исполнитель тестов (test runner) Karma. Karma позволяет выполнять тесты каждый раз при изменении файлов с исходниками. Karma способна выполнять тесты параллельно в нескольких браузерах. Различные устройства также могут быть нацелены на сервер кармы для более полного покрытия реальных сценариев использования.
10. Использование jQuery
jQuery замечательная библиотека. Она стандартизовала кросс-платформенную разработку и стала стандартом в современной веб-разработке. Несмотря на то, что jQuery обладает большим количеством фич, её философия далека от философии AngularJS. AngularJS — это фреймворк для построения приложений, в то время как jQuery — это просто библиотека, упрощающая процесс взаимодействия JavaScript и HTML и предоставляющая удобный API для работы с AJAX. В этом заключается фундаментальное различие между ними. Ангуляр — это подход к построению приложений, а не способ управления разметкой документа. Чтобы действительно осознать принципы построения AngularJS приложений, вам стоит перестать использовать JQuery. jQuery заставляет вас соответствовать существующему HTML стандарту, в то время как ангуляр позволяет вам расширять стандарт HTML под нужды вашего приложения. Манипуляции с DOM в AngularJS должны производиться в директивах, но при этом вполне допустимо размещать в директивах обертки над существующими jQuery компонентами, если вы не смогли найти аналог на angular.
Заключение
AngularJS — это великолепный, постоянно совершенствующийся фреймворк с отличным комьюнити. Надеюсь, мой список популярных ошибок пригодится вам в вашей работе.
Автор: steamru