http://habrahabr.ru/post/246905/
Много времени прошло с момента появления AngularJs (в масштабах веб-технологий конечно). Сейчас в интернетах есть огромное количество постов восхваляющих этот фреймворк до небес, что это манна небесная не иначе, а критики не так уж и много как он того заслуживает. Сразу хочу сказать, что фреймворк я знаю хорошо, даже больше чем мне хотелось бы его знать, я программировал на нем в течении 2 лет. И для следующего проекта я его точно не выберу. Так что же не так с ним? Тут нет однозначного ответа, слишком много разных недостатков, которые создают такой облик фреймворку. Если одним словом – непродуманная архитектура.
Распространенная практика в мире ангуляра — решать проблемы, которые сам себе же и создал. Хотя на самом деле этой проблемы можно было бы и избежать, если хорошо спроектировать архитектуру.
И ЭТОТ ПАТТЕРН ЕСТЬ ПРАКТИЧЕСКИ В КАЖДОМ АСПЕКТЕ ФРЕЙМВОРКА. Под катом я приведу много конкретных примеров, так что устраивайтесь поудобнее.
Двусторонний дата-биндинг
Существует фундаментальное правило, что явное лучше неявного. Все эти watcher'ы это неявный вызов обработчиков событий. В реальности события происходят при клике по элементу, а не при изменении данных, поэтому когда вы пишите на ангулярке вам необходимо думать следующим образом: «ага, тут кликнули по кнопке и изменилась модель, теперь нужно слушать изменения на этой модели и когда она меняется вызывается хэндлер», а не «кликнули на кнопку, вызвался хэндлер»,
ДВУСТОРОННИЙ БИНДИНГ — ЭТО НЕ ТО КАК ПРОИСХОДИТ ОБРАБОТКА СОБЫТИЙ В ПРИНЦИПЕ. На малых масштабах это не так заметно, но когда начинаешь писать что-то побольше Hello World, то отсутствие такой последовательности очень сильно мешает. И к тому же это усложняет дебагинг, подробнее об это в главе про дебаггинг.
Еще двусторонний биндинг означает, что изменив что-либо в своем приложении, это тригерит сотни функций, которые наблюдают за изменениями. И это чудовищно медленная операция, особенно все становится плохо на мобильных платформах. И это фундаментальная часть фреймворка. Ангуляр даже накладывает ограничения на то насколько богатый UI можно писать. И что самое интересное, это не какое-то эфемерное и далекое ограничение в которое вы никогда не упретесь. Это всего лишь 2000 биндингов, и если вы разрабатываете более-менее большое приложение,
ТО ВЫ ТОЧНО УПРЕТЕСЬ В ЭТО ОГРАНИЧЕНИЕ. И тут вам придется драться с фреймворком. Ангуляр вводит возможность одностороннего дата-биндинга, что бы избежать проблем с производительностью. Создаем себе проблему и решаем ее костылем (
непродуманная-архитектура#1).
Тут есть еще один момент, иногда все-таки двух сторонний дата-биндинг нужен, но UI все равно тормозит. Например при первой загрузке пользователь видит {{ выражения в скобочках }}. Хм… так это ж не баг, это фича! Нужно ввести директиву, которая будет прятать UI, что бы пользователь не видел мерцания, и не мешал фреймворку решать его несуществующие проблемы. И для этого вводится директива
ngCloak, которая собственно этим и занимается. Опять создаем себе проблемы и решаем костылями
(непродуманная-архитектура#2).
И если у фреймворка появляются такие проблемы с масштабируемостью, не значит ли это, что что-то на фундаментальном уровне не так? Лаконичные и продуманные технологии, как правило масштабируются очень хорошо.
Dependency Injection
Следующий пример того, как ангуляр сначала ставит грабли, а потом заставляет вас плясать с бубном, что бы их обойти — это Dependency Injection. В ангуляре переменные внедряются по имени аргумента:
function MyController($scope, $window) {
// ...
}
Здесь ангуляр вызывает у функции .toString(), берет названия аргументов и потом ищет их в списке со всеми существующими зависимостями. И проблема тут в том что,
ЭТО ПЕРЕСТАЕТ РАБОТАТЬ ПРИ МИНИФИКАЦИИ КОДА.
Когда вы минифицируете свой код, то он перестает работать, потому что переменные инжектятся по имени. Вам либо нужно использовать такой синтаксис:
someModule.controller('MyController', ['$scope', 'dep1', 'dep2', function($scope, dep1, dep2) {
}]);
или такой
MyController.$inject = ['$scope', '$window'];
и в том и в том случае синтаксис совершенно уродливый. Для того, что бы решить эту проблему существуют специальные плагины, которые сделают из вашего кода пригодный для минифицирования код:
ng-min и
ng-annotate (непродуманная-архитектура#3). Существует три способа указать зависимости (совершенно не понятно зачем нужно было столько способов сделать одно и то же), один из которых не работает и требует костылей, а остальные уродливые. Примечательно, что документация начинается со способа,
который не работает.
Следующая важная часть — это то, как зависимости объявляются. Прежде чем внедрять зависимость ее нужно как-то объявить. Как это можно было сделать, если вы психически здоровый человек:
injector.register(name, factoryFn);
Где name — это название зависимости, и factoryFn — функция, которая будет инициализировать зависимость. Вот и все. Буквально в одном предложении уместили очень лаконичную идею. Теперь смотрим на то что предлагает ангуляр
docs.angularjs.org/guide/providers. Там вводятся 5 новых сущностей:
provider,
service,
factory,
value,
constant (непродуманная-архитектура#4). И каждая из них чем-то отличается друг от друга, но по сути это все одно и то же. Но самое главное, что все они могут быть легко заменены одним методом, который я привел выше. Но это слишком просто и не интересно, уж
лучше давайте заставим людей мучится.
И еще на засыпку, любая сущность которая инжектится называется «сервис» и один из типов сервиса, называется «сервис». И еще у них есть концепция фильтров и один объект из этой концепции называется… как, как вы думаете он называется (барабанная дробь) — ФИЛЬТР! ЭТО ГЕНИАЛЬНО!!! Это тоже самое что назвать своего ребенка «ребенок».
Дебаггинг
Дебаггинг сам по себе сложный, но этого мало, нужно это усложнить! Ангуляр питается вашей болью и страданьями!
ОШИБКИ В БИНДИНГАХ ВООБЩЕ НЕ ВЫЛЕТАЮТ, например в вот этом коде:
<div ng-repeat="phone in user.phones">
{{ phone }}
</div>
если user не определен, то никакой ошибки не будет
(непродуманная-архитектура#5). Ангуляр молчит как партизан. Более того вы не можете поставить брейкпойнт внутри {{ такого идиотского выражения }}, потому что это не JavaScript.
Ошибки, которые произошли в JavaScript'е ловятся внутренним ангуляровским перехватчиком, и
ВОСПРИНИМАЮТСЯ БРАУЗЕРОМ КАК ПОЙМАННЫЕ ОШИБКИ (все что произошло в ангуляре, остается в ангуляре). Из-за этого приходитья ставить в хроме флаг «Pause on caught exeptions» (что бы дебаггер останавливался на всех ошибках, а не только на непойманных (обычно интересуют именно непойманные ошибки, которые выкидываются в консоль)), поэтому приходится в дебаггере проходить по всем внутренним пойманным исключениям ангулярки пока она инициализируется
(непродуманная-архитектура#6) и только потом вы попадаете именно в ваше исключение. И тут-то вы увидите вот такой стактрейс:
Ошибка вылетает из digest цикла и это означает,
ЧТО ОНА МОГЛА БЫТЬ ВЫЗВАНА ИЗМЕНЕНИЕМ ЛЮБОЙ ПЕРЕМЕННОЙ ВО ВСЕМ ПРИЛОЖЕНИИ. Вы хрен когда проследите в дебаггере откуда растут ноги у ошибок.
(непродуманная-архитектура#7)
Наследование скопов
Это без сомнения самая распространенная ошибка с которой сталкивался абсолютно каждый разработчик на angular. Например если вы напишите вот такой код
<div ng-init="phone = 02"> {{ phone }} </div>
<div ng-if="true">
<button ng-click="phone = 911">change phone</button>
</div>
то при нажатии на кнопку переменная phone меняется не будет, в то время как в этом коде
<div ng-init="phone = {value: 02}"> {{ phone.value }} </div>
<div ng-if="true">
<button ng-click="phone.value = 911">change phone</button>
</div>
все работает корректно, и номер телефона меняется при нажатии на кнопку. Либо например вот этот код, который даже опытных разработчиков поставит может поставить в тупик
jsfiddle.net/1op3L9yo/. Даже введен
новый синтаксис для объявления переменных в контроллере через this, который решает эту проблему.
(непродуманная-архитектура#8)
Я не буду объяснять почему так происходит, на эту тему столько статей написано в интернете. И что самое интересное — этой проблемы можно было бы легко избежать просто
УБРАВ ВОЗМОЖНОСТЬ НАСЛЕДОВАНИЯ СКОПОВ. Это был бы правильный дизайн. К тому же когда вы используете переменные из родительского скопа, это становится крайне сложно тестировать, странно, что фреймворк, который ставит одной из своих самых сильных сторон легкость тестирования, вводит такую логику.
Директивы
Мы подошли к самому интересному, директивы — это святая святых ангуляра. Им воспевают песни и хвалят все, кому не лень. Но я буду прагматичным, вот полный синтаксис объявления директивы:
var myModule = angular.module(...);
myModule.directive('directiveName', function factory(injectables) {
var directiveDefinitionObject = {
priority: 0,
template: '<div></div>', // or // function(tElement, tAttrs) { ... },
// or
// templateUrl: 'directive.html', // or // function(tElement, tAttrs) { ... },
transclude: false,
restrict: 'A',
templateNamespace: 'html',
scope: false,
controller: function($scope, $element, $attrs, $transclude, otherInjectables) { ... },
controllerAs: 'stringAlias',
require: 'siblingDirectiveName', // or // ['^parentDirectiveName', '?optionalDirectiveName', '?^optionalParent'],
compile: function compile(tElement, tAttrs, transclude) {
return {
pre: function preLink(scope, iElement, iAttrs, controller) { ... },
post: function postLink(scope, iElement, iAttrs, controller) { ... }
}
// or
// return function postLink( ... ) { ... }
},
// or
// link: {
// pre: function preLink(scope, iElement, iAttrs, controller) { ... },
// post: function postLink(scope, iElement, iAttrs, controller) { ... }
// }
// or
// link: function postLink( ... ) { ... }
};
return directiveDefinitionObject;
});
Что бы понять этот синтаксис и зачем это нужно, надо действительно заморочиться. Вместо того, чтобы описывать ЧТО нужно сделать, вы больше заморачиваетесь на том КАК это сделать. И что самое обидное —
ЭТА СЛОЖНОСТЬ НЕ НЕСЕТ НИКАКОЙ ПОЛЬЗЫ.
(непродуманная-архитектура#9)
Далее, чтобы интегрировать какой-то код в мир ангуляра, например какой-нибудь jQuery плагинчик, вам нужно обернуть его обязательно в директиву. Потому что это было бы слишком просто, если бы разработчики могли сразу использовать готовые решения!
Далее, в директивах есть controller, compile, link функции, которые делают примерно тоже самое, но хрен пойми почему из них не сделать одну единственную функцию. И плюс ко всему имеется приоритет на директивах, в итоге какой код из какого метода (compile, link, controller) в какой последовательности применяется, если на элементе указано несколько директив, да еще какой-нибудь трансклюд вообще хрен пойми.
(непродуманная-архитектура#10)
Проблемы с людьми
Во первых из-за того, что фрейворк чрезмерно сложный, мало разработчиков его понимают на действительно хорошем уровне и найти таких людей сложно. Во вторых серверные разработчики вообще не будут понимать, что творится на front-end'е и не смогут читать код. А это очень большой недостаток, создается черный ящик, в котором может разобраться только один человек в команде, и если этот человек уйдет, то никто не сможет его заменить. Это было бы нормально, если бы разработка на front-end'е действительно бы требовала таких сложных инструментов, но нет, она сложная потому что сложности создаются целенаправленно. Если вы используете ReactJs или jQuery, то любой серверный разработчик, да даже любой программист, сможет разобраться в коде.
Невозможность серверной шаблонизации
Если вы попытаетесь использовать серверную шаблонизацию, например что бы ускорить прорисовку страницы, либо что бы поисковики индексировали (либо и то и другое), то вас постигнет разочарование. Т.к. серверная шаблонизация добавляет логики в HTML и AngularJs тоже пишет логику в HTML, то не происходит четкого разделения ответственности и как результат очень запутанный спагетти-код. Ангуляр просто не предполагает того, что разработчики захотят ускорить загрузку страницы, либо захотят индексацию поисковиками, он не создан для этого.
(непродуманная-архитектура#11)
Важные вещи россыпью
- Ужасная документация. Приходится дополнительно гуглить много информации.
- До сих пор отсутствуют директивы для Drag and Drop событий (на момент написания этой статьи). Само по себе это не очень большая проблема, но это сигнализирует о продуманности продукта и внимании к деталям.
- Когда вы пишите на ангуляре вы помещаете логику в ваш HTML. ng-repeat, ng-show, ng-class, ng-model, ng-init, ng-click, ng-switch, ng-if — что это как не помещение логики в представление. Само наличие такой логики — это не настолько страшно, как то что — это невозможно тестировать юнит-тестами, невозможно продебажить и оттуда не кидаются ошибки (непродуманная-архитектура#12).
- Гугл не использует AngularJs для своих основных продуктов (gmail, google+). И это вызывает подозрения, если это такой хороший фрейворк и они сами его создали, что тогда сами и не используют? Хотя например изобретенный в фейсбуке ReactJs они используют в facebook.com и в инстаграмме.
- Гугл может передумать и закрыть этот проект. Все мы знаем как гугл любит закрывать даже очень популярные проекты.
НЕ ИСПОЛЬЗУЙТЕ АНГУЛЯР. Вместо ангуляра лучше использовать React (предпочтительно) либо jQuery (для тех кто боится использовать реакт по какой-то причине). Я не знаю почему ангуляр популярный, но это точно не потому что это хороший фреймворк. Надеюсь мой опыт поможет кому-нибудь принять правильное решение при выборе стека технологий для следующего проекта.