Разработка → Почему вам НЕ стоит использовать AngularJs

mnemosha 27 декабря 2014 в 16:07 220k
Много времени прошло с момента появления AngularJs (в масштабах веб-технологий конечно). Сейчас в интернетах есть огромное количество постов восхваляющих этот фреймворк до небес, что это манна небесная не иначе, а критики не так уж и много как он того заслуживает. Но такие статьи уже потихоньку начинают появляться, и меня это радует, надеюсь индустрия переболеет ангуляром так же, как переболела MooTools, Prototype, %какой-нибудь новый язык под JVM%, %другая-супер-революционная-технология%. Не знаю почему, но в IT-области такие революционные технологии, которые поднимают шум, а потом пропадают, появляются довольно часто. Хороший разработчик должен уметь отличать очередную модную технологию, от работающего инструмента. И для этого очень важно критически смотреть на вещи. Моя статья — это компиляция самых весомых выводов из других статей, и моих личных умозаключений. Ангуляр создает хороший вау-эффект, когда видишь его впервые: «ух ты, я написал ng-repeat, и реализовал эту логику одними тегами и все само обновляется!», но как только приходится реализовывать реальные приложения, а не очередной TODO-лист, то все становиться очень печально. Сразу хочу сказать, что фреймворк я знаю хорошо, даже больше чем мне хотелось бы его знать, я программировал на нем в течении 2 лет. И для следующего проекта я его точно не выберу, и это хорошо, все мы учимся на ошибках. Так что же не так с ангуляром? Тут нет однозначного ответа, слишком много разных недостатков, которые создают такой облик фреймворку. Если одним словом – непродуманная архитектура. Под катом я привожу конкретику, так что устраивайтесь поудобнее. ДА НАЧНЕТСЯ ХОЛЛИ ВАР!

Двусторонний дата-биндинг


Существует фундаментальное правило программирования, оно относиться к совершенно любому языку или технологии, это правило гласит — явное лучше неявного. Все эти watcher'ы это неявный вызов обработчиков событий. В реальности события происходят при клике по элементу, а не при изменении данных, поэтому когда вы пишите на ангулярке вам необходимо думать следующим образом: «ага, тут кликнули по кнопке и изменилась модель, теперь нужно слушать изменения на этой модели и когда она меняется вызывается хэндлер», а не «кликнули на кнопку, вызвался хэндлер», ДВУСТОРОННИЙ БИНДИНГ — ЭТО НЕ ТО КАК ПРОИСХОДИТ ОБРАБОТКА СОБЫТИЙ В ПРИНЦИПЕ. Это тоже самое, что вы достаете данные из базы данных и записываете их в модель, и только на изменениях модели вызываются колбэки, да так вообще нигде не реализовано, даже на Java, язык который вообще не имеет анонимных функций (когда я на нем еще писал), реализовывал колбэки на классах. Потому что это явно, потому что такая концепция описывает реальное положение вещей. Даже будущая версия Ember.js не будет использовать двусторонний дата-биндинг.

Еще двусторонний биндинг означает, что изменив что-либо в своем приложении, это тригерит сотни функций, которые наблюдают за изменениями. И это чудовищно медленная операция, особенно все становится плохо на мобильных платформах. И это фундаментальная часть фреймворка. Ангуляр даже накладывает ограничения на то насколько богатый UI можно писать. И что самое интересное, это не какое-то эфемерное и далекое ограничение в которое вы никогда не упретесь. Это всего лишь 2000 биндингов, и если вы разрабатываете более-менее большое приложение, ТО ВЫ ТОЧНО УПРЕТЕСЬ В ЭТО ОГРАНИЧЕНИЕ. И тут вам придется драться с фреймворком. Ангуляр вводит возможность одностороннего дата-биндинга, что бы избежать проблем с производительностью. Создаем себе проблему и решаем ее костылем (непродуманная-архитектура#1). Сами глянте, какое количество людей, которые борются с производительностью ангуляра. Да если вы в гугле вобьете angular performance, то вы ужаснетесь количеству статей, описывающих как его ускорить, не является ли это достаточным аргументом, в пользу того, что ангуляр медленный?

Тут есть еще один момент, иногда все-таки двух сторонний дата-биндинг нужен, но UI все равно тормозит. Например при первой загрузке пользователь видит {{ выражения в скобочках }}. Хм… так это ж не баг, это фича! Нужно ввести директиву, которая будет прятать UI, что бы пользователь не видел мерцания, и не мешал фреймворку решать его несуществующие проблемы. И для этого вводится директивы ngCloak, ng-bind, ng-bind-template, которые собственно этим и занимается. Этих проблем бы не было, если бы использовался более шустрый фреймворк, который сразу показывает данные, как только они появились, ангуляр же после появления данных, требует времени что бы их отобразить. Опять создаем себе проблемы и решаем костылями (непродуманная-архитектура#2).

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

Dependency Injection


Следующий пример того, как ангуляр сначала ставит грабли, а потом заставляет вас плясать с бубном, что бы их обойти — это Dependency Injection. В ангуляре переменные внедряются по имени аргумента:

function MyController($scope, $window) {
    // ...
}

Здесь ангуляр вызывает у функции .toString(), берет названия аргументов и потом ищет их в списке со всеми существующими зависимостями. Поиск переменной по имени — это еще терпимо, учитывая типизацию JavaScript, а проблема тут в том что, ЭТО ПЕРЕСТАЕТ РАБОТАТЬ ПРИ МИНИФИКАЦИИ КОДА. Когда вы минифицируете свой код, то он перестает работать, потому что переменные инжектятся по имени. Вам либо нужно использовать такой синтаксис:

someModule.controller('MyController', ['$scope', 'dep1', 'dep2', function($scope, dep1, dep2) {
}]);

Или такой:

MyController.$inject = ['$scope', '$window'];

Но непонятно, зачем нужно было вводить несколько способов сделать одно и тоже, но еще больше непонятно, зачем нужно было вводить заведомо неработающий способ (непродуманная-архитектура#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).

Очевидно, что это поведение абсолютно бесполезное и очень вредное:
  • Если вы опираетесь в своей логике на наследование скопов, то такую логику становиться крайне сложно тестировать (нужно инициализировать еще состояние родительского контроллера).
  • Логика становиться более запутанной и неявной (вы используете переменную, которая в текущем модуле не объявлена)
  • Унаследованные переменные чем-то похожи на глобальные переменные (из дочернего скопа вы имеете доступ к абсолютно любой переменной из любого родительского скопа). А все знают, что глобальные переменные — это зло.
  • Увеличивает входной порог и количество теории, которую нужно знать: вам нужно знать прототипное наследование, вам нужно знать как работает tranclude и как можно поменять скоп в котором он работает, вам нужно знать что ng-repeat, ng-if, ng-switch работают со скопами по другому, вам нужно знать, что в директиве есть 3 разных параметра что бы указать какой скоп она создает внутри себя, да перечислять можно еще долго.

Да по количеству вопросов на stackoverflow.com и статей в интернете можно уже понять, насколько это запутанная тема, вот такие вот дела, а кстати, что вы делаете сегодня вечером, я вот хочу какой-нибудь фильм глянуть, может посоветуете что-нибудь ? Даже в продвинутых стайл-гайдах советуют избегать использования $scope. И за 2 года работы на ангулярке я не встретил ни одного места, где обращение к переменным объявленным в родительском скопе было бы хорошей идеей (за исключением конечно стандартных директив типа ng-repeat, на реализацию которых мне, как пользователю библиотеки, вообще пофиг). Самое интересно, что такое поведение можно было бы избежать УБРАВ ВОЗМОЖНОСТЬ НАСЛЕДОВАНИЯ СКОПОВ (Например в ExtJs создается обертка над классами для имитации OOП, уверен, что и в каких-нибудь других фреймворках есть похожее). Обычно фреймворки должны прятать недостатки платформы и предоставлять в своем API логичное и максимально прозрачное поведение, но ангуляр наоборот бросает этот недостаток JavaScript вам в лицо и заставляет с ним драться. Из-за этого косяка был потрачен не один человеко-месяц.

Директивы


Мы подошли к самому интересному, директивы — это святая святых ангуляра. Им воспевают песни и хвалят все, кому не лень. Но я буду прагматичным, вот полный синтаксис объявления директивы:

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;
});

Что бы понять этот синтаксис и зачем это нужно, надо действительно заморочиться. Вместо того, чтобы описывать ЧТО нужно сделать, вы больше заморачиваетесь на том КАК это сделать. И что самое обидное — ЭТА СЛОЖНОСТЬ НЕ НЕСЕТ НИКАКОЙ ПОЛЬЗЫ. Нету логических причин разделять логику на 3 метода (compile, link, controller), все это можно легко реализовать в одном методе. Опять таки смотрим какое количество проблем это породило. И учтите еще что в какие-то функции scope передается, в какие-то нет, какие-то функции выполняются один раз, какие-то каждый раз при $digest цикле, у каждой директивы так же имеется приоритет выполнения. Да даже, чтобы интегрировать какой-то код в мир ангуляра, например какой-нибудь jQuery плагинчик, вам нужно обернуть его обязательно в директиву. Потому что это было бы слишком просто, если бы разработчики могли сразу использовать готовые решения! И еще непонятно почему controller в директиве и реальный контроллер имеют одно название, они используются в совершенно разных местах и для разных целей, у них должно быть разное имя! (непродуманная-архитектура#9).


Проблемы с людьми


Во первых из-за того, что фрейворк чрезмерно сложный, мало разработчиков его понимают на действительно хорошем уровне и найти таких людей сложно. Во вторых серверные разработчики вообще не будут понимать, что творится на front-end'е и не смогут читать код. А это очень большой недостаток, создается черный ящик, в котором может разобраться только один человек в команде, и если этот человек уйдет, то никто не сможет его заменить. Это было бы нормально, если бы разработка на front-end'е действительно бы требовала таких сложных инструментов (например разработка GUI на ExtJs намного сложнее, чем на Angular, но там эта сложность оправданна), но нет, она сложная потому что сложности создаются целенаправленно. Если вы используете ReactJs или jQuery, то любой серверный разработчик, да даже любой программист, сможет разобраться в коде.

Невозможность серверной шаблонизации


Если вы попытаетесь использовать серверную шаблонизацию, например что бы ускорить прорисовку страницы, либо что бы поисковики индексировали (либо и то и другое), то вас постигнет разочарование. Т.к. серверная шаблонизация добавляет логики в HTML и AngularJs тоже пишет логику в HTML, то не происходит четкого разделения ответственности и как результат очень запутанный спагетти-код. Ангуляр просто не предполагает того, что разработчики захотят ускорить загрузку страницы, либо захотят индексацию поисковиками, он не создан для этого. (непродуманная-архитектура#11). Да, вы можете обойти эту проблему, используя prerender.io (это сервис, который парсит ваш SAP и выдает вам HTML файлы, которые вы должны скармливать поисковикам). Но опять таки — это костыль, а не коренное решение проблемы.

AngularJs 2.0


В скором времени выдет следующая мажорная версия фреймворка, которая ломает обратную совместимость. Судя по всему даже разработчики фреймворка поняли, что дальше так продолжаться не может и решили переписать фреймворк практически с нуля. Они не добавляют новый функционал, немного ломая обратную совместимость, а прям на корню переделывают. И это означает, что если вы сейчас начнете проект на первой версии, то в скором времени вы точно окажетесь за бортом. Ну и сами подумайте, разве они бы стали переписывать фреймворк, если бы он уже был качественный?

Важные вещи россыпью


  • Ужасная документация. Приходится дополнительно гуглить много информации.
  • До сих пор отсутствуют директивы для Drag and Drop событий (на момент написания этой статьи). Само по себе это не очень большая проблема, но это сигнализирует о продуманности продукта и внимании к деталям. Эти события уже поддерживаются подавляющим большинством десктопных браузеров и являются таким же базовыми, как и onclick или mousemove.
  • Когда вы пишите на ангуляре вы помещаете логику в ваш 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 (для тех кто боится использовать реакт по какой-то причине), либо что-то еще, но уже на свой страх и риск потому что негативных отзывов о backbone, knockout и тому подобных тоже много. И я прошелся только по самым важным и заметным косякам, обычно когда программист говнокодит, он говнокодит везде, так что если вы собираетесь использовать этот фреймворк, приготовьтесь встретится не только с описанными проблемами, но и с кучей других, более мелких, но все равно проблем. Я не знаю почему ангуляр популярный, но это точно не потому что это хороший фреймворк. Поэтому я категорически не советую использовать текущую версию ангуляра. Надеюсь мой опыт поможет кому-нибудь принять правильное решение при выборе стека технологий для следующего проекта.

Источники


Why you should not use AngularJS
What I would recommend instead of Angular.js?
React vs AngularJS – How the two Compare
From AngularJS to React: The Isomorphic Way
The reason Angular JS will fail
Pros and Cons of Facebook's React vs. Web Components (Polymer)
AngularJS: The Bad Parts
What’s wrong with Angular.js
Things I Wish I Were Told About Angular.js

P.S. Я хочу думать о бизнесс-логике и о том какие задачи мне нужно решить по факту, но я не хочу думать о бессмысленных суррогатных коцептах: наследовании скопов, когда нужно использовать односторонний или двусторонний дата-биндинг, когда нужно использовать compile, link, controller, когда мне нужно использовать value или constant, provider или factory, я не хочу думать о том, что делать когда я хочу интегировать натинвые события события в мир ангуляр, я не хочу думать должна ли директива видеть родительский скоп или нет, я не хочу думать о наследовании скопов в стандартных директивах и всех обходных путях, я не хочу думать о том, использовать мне глубокий $watch, обычный или $watchCollection, я не хочу думать о том, зачем нужны Provider для каждой внедряемой сущности и не хочу знать ничего о run и config фазах, я не хочу думать о том увидел ли пользователь {{ скобочки }}, я не хочу решать проблемы производительности, там где их нет, я знать не хочу о том, что такое трасклюд и как он работает со скопами. ГОСПОДИ, АНГУЛЯР ОТПУСТИ, ВЕДЬ Я НЕ В ЧЕМ НЕ ВИНОВАТ, ДАЙ МНЕ ПОЖАЛУЙСТА ПО ПРОГРАММИРОВАТЬ, МНЕ ЭТО НРАВИТСЯ.
Проголосовать:
+179
Сохранить: