Angular 2 просто ужасен

mangohost

Мы используем Angular 2 в наших веб-интерфейсах. Я не принимал участия в этом решении, так как пришёл работать в этот проект относительно поздно. Статья не претендует на исчерпывающий обзор фреймворка, скорее это совокупность наблюдений после использования его в течении двух недель. Я не утверждаю, что использование его в течении двух недель делает меня экспертом, и приветствую любые корректировки, но считаю что использование Angular 2 это одна из самых больших ошибок в нашем проекте.

Ненадёжная основа

Стабильная версия Angular 2 основана на экспериментальных особенностях языка (TypeScript decorators, based on a Stage 1 TC39 draft proposal) и библиотеках находящихся в бете версии (Rx.js 5.0). Позвольте повториться, что от фреймворка требуется больше стабильности, чем от его зависимостей и языка с помощью которого он разработан. Это абсолютное бемзумство. Angular 2 не должен называть себя стабильным, пока его основные зависимости не будут стабильными. Создание приложений с помощью него, это как строительство карточного домика. Что случиться, если семантика декораторов измениться или вообще пропадёт из языка?

Фатальный недостаток (Not Invented Here)

Angular 2 - это фреймворк в прямом смысле  этого слова. Он использует собственную систему загрузки модулей (потому что возможностей JavaScript не хватает, не так ли?), пытается абстрагироваться от всей браузерной платформы, и даже поставляется с полноценным HTML-парсером и санитайзером. Он даже разговаривает на собственном языке - структурные директивы, трубы (pipes), декларации, модули, инжекторы, сервисы, инкапсуляция представлений, декораторы.

Архитектура Angular

Становиться сложнее изучать фреймворк. Даже те кто уже знаком с API браузера и реактивными фреймворками, должен переучиваться чтобы понять как это делается в  Angular. Трубы (pipes), например, практически не отличаются от концепции Unix pipes или фильтров которые вы могли видеть традиционных шаблонизаторах таких как Twig. Другой пример: HTTP клиент в Angular возвращает Observables вместо Promises, заставляя нас выучить не только другую асинхронную библиотеку, но ещё и другую парадигму.

Это усугубляется тем, что несмотря на утверждения в документации, Observables не даёт ощутимых улучшений в сравнении с Promises, при использовании AJAX запросов.

Rx.js потоки

Многие методы Rx.js проиллюстрированы с помощью потоков, как этот. По смыслу это подходит для данных, которые на самом деле являются потоками, но избыточно для запросов состоящих из одного значения.[/caption]

AJAX запросы являются сингулярными, и запущенные методы такие как Observable.prototype.map не имеют никакого смысла, когда есть только одно значение. Promises с другой стороны представляют собой значение, которые ещё предстоит выполнить, это именно то что даёт нам HTTP запрос. Я потратил несколько часов чтобы заставить Observables работать, перед чем начал использовать Observable.prototype.toPromise чтобы преобразовать Observable назад к Promises и просто использовать Promise.all, который работает намного лучше чем то что предлагает Rx.js.

Поспешная абстракция

Angular выглядит как платформа агностик, платформа с помощью которой можно разрабатывать приложения (мобильные или десктопные).

Изучите один способ чтобы разрабатывать приложения с помощью Angular и повторно использовать ваш код для любых целей. Для WEB, мобильного WEB, для нативных мобильных приложений и нативных десктопных приложений.

Он  делает это путём абстрагирования от различных частей API браузера. Angular заменяет DOM с помощью собственного нелепо модифицированного HTML, историю браузера и Location API с помощью собственной маршрутизации и сервиса местоположения, а XHR и веб-сокеты с помощью собственного HTTP клиента. К сожалению, это смехотворно маленькое подмножество API, которое может понадобиться современному приложению. В любое время вам может понадобиться что-то простое, например LocalStorage, геолокация, push-уведомления, или даже простой полифилл по разному поддерживаемый браузерами для input type="date" и вы тут же нарушите своё обещание о том, что ваше приложение кросс-платформенное.

Что ещё более важно, это то что в большинстве случаев эта абстракция просто не нужна. Некоторые вещи из Angular platform API - это просто тонкие обёртки вокруг API браузера, которые не делают ничего полезного, а только усложняют его использование. Геолокация и HTTP клиент - два хороших этому примера.

Цитата выше намекает на изучение одного фреймворка для разработки приложений под все платформы, но обратная сторона изучения этого фреймворка заключается в том, что вы не сможете использовать полученные знания в другом месте. Изучите основы Fetch API и вы сможете использовать его в любое время при разработке веб-приложений. Изучите HTTP клиент из Angular и эти знания будут бесполезны за пределами вселенной Angular.

HTML minus

Angular утверждает, что HTML Plus - это как HTML, но только лучше. Это неправда.

Во-первых, Angular HTML это не HTML. HTML атрибуты не чувствительны к регистру, а у Angular'а чувствительны. Это может показаться небольшой разницей, но это означает что любой инструмент разработанный с поддержкой HTML кода, не может поддерживать его Angular версию. Например в WebStorm автодополнение, ожидает что вы напишете ngif, но не как не ngIf. Они эквивалентны в HTML, но не в Angular HTML.

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

<div *ngIf="condition">...</div>
<div template="ngIf condition">...</div>
<template [ngIf]="condition"><div>...</div></template>

Примеры приведённые выше эквивалентны, но если вы смешаете синтаксис,  следующий пример просто не будет работать, без всяких предупреждений:

<template *ngIf="condition">...</template>

Причина заключается в звёздочке * , которая по существу является синтаксическим сахаром и просто оборачивает элемент в <template>. Проблема в том, что когда мы используем звёздочку в теге <template> он преобразовывается в:

<template [ngIf]="condition"><template>...</template></template>

который вообще ничего не отображает. Vue использует для этого одну директиву v-if и обычные HTML-теги работают так как  они должны работать.

ngFor в Angular использует синтаксис микроразметки. Я никогда не видел шаблонизаторов которым нужен DSL для циклов.

<li *ngFor="let item of items; let i = index; trackBy: trackByFn">...</li>

let вводит в заблуждение - потому что на самом деле вы не можете делать присваивание в этих выражениях, что собственно заставляет задумываться, а зачем вообще всё это нужно? Синтаксис Vue, для примера, намного проще и разумнее:

<li v-for="(item, index) in items" :key="item.id">...</li>

Pipe'ы в Angular (все остальные фреймворки называют их "фильтры") могут принимать параметры. Но для нескольких значений в качестве разделителя используется двоеточие, а не запятая как это сделано почти в каждом шаблонизаторе, который я использовал. Зачем? Понятия не имею.

<p>{{ name | slice:1:5 }}</p>

Все приведённые выше проблемы раздражают, но терпимы. Заключительная проблема, хотя это проблема проектирования, лежит в основе философии Angular'а. Angular пытался переместить большую часть прикладной логики в шаблоны - логический поток, событие и связывание свойств, директивы, ссылки в язык разметки HTML. Я покажу вам кусок нашего кода, который говорит о том, что это было плохой идеей.

<ul class="session-container-list">
  <li class="session-container"
    *ngFor="let container of containers"
    [class.empty-container]="container.sessions.length === 0" (click)="container.sessions.length === 0&&!isPublic&&!isAnalytics&&addNewSession(container)" [style.cursor]="(!isPublic&&!isAnalytics)?'pointer':'auto'">
    <ul *ngIf="!isPublic" class="session-list"
        [dragula]='"column"' [dragulaModel]='container.sessions'
        data-column-type="absolute"
        [attr.data-container]="getContainerData(container) | json">
      <li class="session-wrapper"
        *ngFor="let session of container.sessions"
        [attr.data-session-id]="session.id"
        [attr.data-session-placeholder]="session.placeholder"
        [attr.data-session-start]="session.start?session.start.toISOString():''"
        [attr.data-session-duration]="session.duration"
        [class.placeholder]="session.placeholder">
        <my-session [session]="session" [offsetDate]="offsetDate" [agenda]="agenda" [isPublic]="isPublic" [isAnalytics]="isAnalytics"
          (onSessionEdited)="onSessionEdited($event)"
          (onSpeakerEdited)="onSpeakerEdited($event)"
          (onSpeakerAdded2)="onSpeakerAdded($event)"
          (onVenueEdited)="onVenueEdited($event)"
          (onVenueAdded2)="onVenueAdded($event)"
          (onSessionDeleted)="onSessionDeleted($event)"
          ></my-session>
      </li>
    </ul>
    <ul *ngIf="isPublic && container.sessions" class="session-list">
      <li class="session-wrapper"
        *ngFor="let session of container.sessions"
        [class.placeholder]="session.placeholder">
        <my-session [session]="session" 
                    [offsetDate]="offsetDate" 
                    [agenda]="agenda" 
                    [isPublic]="isPublic" 
                    [isAnalytics]="isAnalytics" 
                    *ngIf="session.toggle"
                    [token]="token"
                    [interested]="isInterestedInSession(session)"
                    (onSessionInterestEdited)="onSessionInterestEdited($event)"
                    [analyticsData]="getAnalyticsDataForSession(session)">
        </my-session>
      </li>
    </ul>
  </li>
</ul>

Чтобы быть справедливым, иметь логику в шаблонах - это не обязательно плохо. Я не защищаю пуританский подход как в Mustache и нахожу логику в большинстве шаблонизаторов приемлемой. Проблема связана с шаблонами в Angular, которые позволяют и даже в некоторой степени поощряют связывание всего и вся с компонентами. Это способствует медленному наращиванию слоёв из излучателей событий и входных данных, пока вы не получите результат, который только что увидели выше, вместо спланированной модели данных для приложения. Сейчас в большинстве наших компонентов бардак из обработчиков событий, излучателей и свойств с крайне плохим разделением. И всякий раз когда нам нужно повторно использовать компонент, как правило мы копируем и вставляем код.

Ненужное многословность

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

Для создания компонента в Angular, нужно создать отдельные JS, CSS и HTML файлы. В зависимости от настроек, JavaScript может быть скомпилирован из TypeScript, а CSS может быть скомпилирован из SCSS или LESS, так что это 5 отдельных файлов для одного компонента. В связи с большим количеством файлов, вам нужно создавать отдельную папку для компонента. Сам компонент представляет собой класс с декоратором @Component, который объявляет метаданные компонентов такие как стили, шаблон и селектор, чтобы компонент можно было повторно использовать в других местах. После этого происходит внедрение зависимостей через конструктор, и не забудьте объявить интерфейсы жизненного цикла компонента. В заключении вам нужно зарегистрировать компонент в файле модуля приложения. Очень много работы, ради одного компонента.

Сравните этот подход с единственным файлом компонента в Vue. Для Vue, разметка, стили и логика объявляются в одном единственном файле. Компонент выражается как простой JavaScript объект. Есть очень маленький ритуал, который позволяет легко создавать новые компоненты.

Одиночные файлы компонентов возможны и в Angular, но вам нужно объявлять разметку и стили в TypeScript, а он (насколько я знаю) не совместим с препроцессорами, такими как SCSS.

Существенная разница? Я думаю, да. В нашем Angular приложении 21 компонент, в тоже время у нашего Vue приложения их более 30, хотя последнее намного проще. Vue компоненты маленькие и простые, в то время как Angular компоненты неудержимо растут, потому что проще добавлять некоторые вещи в один компонент, чем дробить их на отдельные.

Низкая производительность и раздутие

Это может быть связано с конкретно нашей настройкой, но Angular 2 кажется очень тяжёлым. Приложение построенное на Vue, которое я разрабатывал для себя, перестраивается в одно мгновение. Во время разработки, пока я переключаю взгляд с рабочего пространства на браузер, страница уже перезагружена и отображена. Нашему Angular 2 приложению требуется несколько секунд чтобы перестроиться и отобразиться. Это может показаться недолго, но не забывайте что любые изменения в исходном коде вызывают перезагрузку, поэтому это происходит сотни раз в день.

В Vue из коробки есть модуль горячей перезагрузки стилей в режиме разработки, в то время как в Angular ничего этого нет, что тоже замедляет работу. Хоть и есть возможность настроить модуль горячей перезагрузки в Angular 2, Vue уже настроен по умолчанию.

Точно также, если Vue приложение поддерживает отображение шаблона перед компиляцией (ahead-of-time (AoT)) из коробки, то в Angular 2  всё это нужно настраивать и подключать сторонние зависимости. Подключение зависимостей наиболее важно потому что, для настройки в приложении поддержки AoT компиляции, всё тоже самое требуется и для зависимостей. Также это способствует увеличению размера. Изначальный размер нашего приложения 1MB.

$ ls -sh *.js 
8.0K app.4fda0a42cad7fc187df1.js  
100K polyfills.4fda0a42cad7fc187df1.js  
908K vendor.4fda0a42cad7fc187df1.js

Один мегабайт. Это размер приложения с единственным вендором и файлами полифиллов. Кроме того это размер всего JS использованного в нашем приложении. Он включает в себя 600kb использования Three.js и OSM Building библиотеку. 0.js содержит Three.js, а 1.js содержит OSMB.js. Это очень большие JS библиотеки спроектированные для 3D отображения в браузере, и даже когда они обе включены в наше приложение, оно меньше чем при использовании Angular.

$ ls -sh *.js 
496K 0.932d135e27de51cdc9e7.js  116K app.f5d63e42fa964e3f9b04.js
128K 1.09b1205dc52803a6ab90.js  8.0K manifest.e0de65d72b1053e2c121.js
8.0K 2.71a0679fdbcabd1595e7.js  236K vendor.ab341f6d4eec7db5a1a3.js

Vue на собственной странице сравнения фреймворков отмечает, что "Hello World" в Angular 2 приложении может быть таким же лёгким, и весить всего 50 килобайт после встряхивания дерева (tree shaking). Тем не менее помните, что такой номер проходит только после удаление всех компонентов, которые не нужны для сборки, в то время как Vue 2.0 весит 23 килобайта из коробки. Другими словами, 23 килобайта это максимальный размер для Vue, а 50 килобайт это лишь минимальный размер для Angular.

Проталкиваем Java назад в JavaScript

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

Профайлинг Angular

... и тогда я наткнулся на статью в которой обсуждалась стратегия выявления изменений в Angular.

По умолчанию, даже если нам придётся проверять каждый компонент всякий раз когда вызывается событие, Angular очень быстрый. Он может выполнить сотни тысяч проверок в течении пары миллисекунд. В основном это объясняется тем, что Angular генерирует код для VM.

Разумеется я вас не знаю, но это смелое заявление, звучит дико. Angular обрабатывается браузерным JavaScript движком в качестве VM, но это не совсем так, как делает это Java. Что это означает в дополнении к трассировки стека для трёх длинных страниц и бесполезному профайлеру, дак это то что производительность полностью во власти движка. Есть только одна JVM, а у JavaScript есть полтора десятка различных движков, с разными профилями производительности. Я могу только предполагать, что код сгенерированный Angular будет "VM friendly" для всех остальных. Многообещающе.

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

private static extractAgenda(agenda: any): Agenda {
  return agenda;
}

Ужасная документация

У Angular 2 наихудшая документация из всех крупных фреймворков, которые я когда либо видел. Она не аккуратная, неполная и плохо написанная. Для начинающих, Angular - это JavaScript фреймворк, у которого нет документации на JavaScript. На странице "Quickstart" говориться следующее:

Не смотря на то, что в разделе "Getting started" приводятся примеры на TypeScript, вы можете писать Angular приложения на JavaScript или Dart. Используйте переключатель языка программирования в левой навигационной панели, для того чтобы переключить язык используемый в этом руководстве. 

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

Большая часть документации была написана для TypeScript разработчиков и ещё не была переведена на JavaScript. Пожалуйста оставайтесь с нами. Между тем мы будем предоставлять ссылки на TypeScript главы, где документация по JavaScript не доступна.

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

Документация по Angular

Документация плохо организована. Авторы разделили её на три секции "Guide", "Advanced" и "Cookbook", но разграничения довольно таки произвольны. Например, руководства по формам и синтаксису шаблонов находится в секции Guide, а руководства по маршрутизации и навигации в секции Advanced. Ahead-of-Time компиляция почему то является разделов в Cookbook, также как и валидация форм. Ни один из приведённых только что примеров не имеет никакого смысла, а что ещё хуже, открытие второго уровня навигации полностью перезагружает страницу, что делает процесс поиска информации мучительно долгим.

Страницы в руководстве пронумерованы, но эти цифры ничего не значат. "Forms" находится перед "Template Syntax", даже при том что синтаксис двустороннего связывания данных, используемый в формах, описывается только в синтаксисе шаблонов.

Отдельные руководства сами по себе плохо написаны. Вот абзац из руководства по Pipe'ам:

Давайте напишем ещё один грязный pipe, который делает HTTP запрос к серверу. Как правило это ужасная идея. Вероятно, это ужасная идея независимо от того что мы делаем. Мы всё равно продвигаемся вперёд чтобы поставить точку. Помните что грязные pipe'ы вызываются каждые несколько микросекунд. Если мы не будем осторожны, то этот pipe покарает сервер запросами.

Стиль написания совершенно невыносим. Только первое и последнее предложение могут рассказать нам что-то полезно, но и они содержат большие технические ошибки - автор, вероятно имел в виду миллисекунды, а не микросекунды, величины которых различаются на три порядка. Опечатки есть по всюду (Прим. переводчика: Не стал переводить, чтобы было видно в каких местах опечатки):

The component doesn’t have to subscribe to the async data source, it doesn’t extract the resolved values and expose them for binding, and the component doesn’t have to unsubscribe when it is destroyed (a potentsource of memory leaks).

Despite the two bindings and what we know to be frequent pipe calls, the nework tab in the browser developer tools confirms that there is only one request for the file.

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

Будущее веб-разработки!?

Когда я знакомился с Angular 2, я надеялся что он будет элегантным фреймворком, который находится на одном уровне с такими проектами как Laravel или Django, и сделает разработку веб-интерфейсов лёгкой и приятной. Но всё что я увидел, это то что он является нестабильных, раздутым, слишком усложнённым фреймворком, который много что обещает, но мало что делает.

Пожалуйста не используйте Angular. Меньше чем за одну десятую его размера, Vue.js предоставит вам более широкие возможности для развития.

Комментарии

7 декабря 2016 17:07
SocialChooozy

кто бы сомневался

16 декабря 2016 17:01
Maksym Kondratenko

Автор не ной! Нормальный ангуляр, поработй больше и поймешь его прелесть. А нюансы есть во всех либах и фреймворках!

16 декабря 2016 17:35
Администратор

Если обращение к автору, то лучше было бы оставить коммент к оригиналу статьи. Хотя коммент без аргументов, лучше нигде не оставлять.

13 января 2017 14:09
Купстас Николай

Не знаю, у меня уже несколько крупных проектов в продакшене на втором ангуляре. Я безумно рад. Жалко, что там нет встроенного аналага redux, но это не беда. Он построен для крупных интерпрайз-приложений и позволяет делать и менять внутри по сути всё. Хочешь, напиши свой более удобный шаблонизатор, хочешь роуты обнови. Человек не разобрался в нём ни на грамм и просто написал пост гнева. Да, порог вхождения у него достаточно высокий, не каждый новичок сможет осилить, но он по своему прекрасен =)

13 января 2017 14:50
Администратор

Все фреймворки по своему прекрасны =) И у всех фреймворков есть свои недостатки. Мне кажется, что автор как раз разобрался в этой теме, и его доводы достаточно убедительны. Если вы говорите, что он не разобрался, напишите где именно он неправ, приведите примеры. Тем более, что у вас есть опыт в этой сфере)

3 сентября 2017 15:28
Евген

Хотите пруфы? Получите)"Другой пример: HTTP клиент в Angular возвращает Observables вместо Promises, заставляя нас выучить не только другую асинхронную библиотеку, но ещё и другую парадигму."- это же детский сад какой-то, помимо того, что он не понимает эти преимущества, так он тупо не знает про toPromise() у обсервера и такой банальной безграмотности здесь на каждом шагу, имхо писал какой-то двоечник.

24 января 2017 05:32
Смотрящий

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

17 мая 2017 20:29
XaH

На счет документации - полностью согласен. Весь остальной негатив только от "скорее это совокупность наблюдений после использования его в течении двух недель"

6 июня 2017 01:20
Дмитрий

У китайца, писавшего статью, явно какой-то ПМС. Ну или перевод такой импульсивный.

11 сентября 2017 01:20

А у человека, писавшего перевод, явно выше двойки по русскому языку никогда не было. Боль от чтения этой статьи больше, чем от ангуляровских проблем.

7 августа 2017 01:24
Сергей

Я начал учить жаваскрипт программирование с Ангуляра2. После жавы переход прошел легко и просто. Очень нравится. Пишу сервера на жаве, клиенты на Ангуляре2 и Ионик. Сказка

mangohost