Как я перестал любить Angular

из песочницы
MooooM 10 сентября в 13:17 55,2k

Вступление


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


На дворе 2017ый год и для каждого нового продукта/проекта встает вопрос выбора фреймворка для разработки. Долгое время я был уверен, что новый Angular 2/4 (далее просто Angular) станет главным трендом enterprise разработки еще на несколько лет вперед и даже не сомневался что буду работать только с ним.


Сегодня я сам отказываюсь использовать его в своем следующем проекте.


Дисклеймер: данная статья строго субъективна, но таков мой личный взгляд на происходящее и касается разработки enterprise-level приложений.


AngularJS



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


Несмотря на колоссальные труды и общее ускорение фреймворка (особенно после 1.5),
основным его недостатком я бы все же назвал скорость работы. Конечно это простительно, учитывая что спустя столько лет, худо-бедно до сих пор поддерживается обратная совместимость.


Angular



И вот наконец Angular был переписан с нуля дабы стать основной для многих будущих веб-приложений.


Конечно путь к этому был долог и полон Breaking Changes, но на сегодняшний день Angular 4 стабилен и позиционируется как полностью production-ready.


Одна из наиболее крутых вещей, которую дал нам новый Angular — популяризация TypeScript.
Лично я был с ним знаком и работал еще до того, как он стал основным для моего любимого фреймворка, но многие узнали о нем именно благодаря Angular.


TypeScript



Не буду подробно останавливаться на TypeScript, т.к. это тема для отдельной статьи,
да и написано о нем уже больше чем нужно. Но для enterprise разработки TypeScript дает огромное количество преимуществ. Начиная с самой статической типизации и областями видимости и заканчивая поддержкой ES7/8 даже для IE9.


Главное преимущество работы с TypeScript — богатый инструментарий и прекрасная поддержка IDE. По нашему опыту, юнит тестов с TS приходится писать существенно меньше.


Vue



Если вы читаете данную статью, то с вероятностью 95% вы уже знаете что это такое.


Но для тех 5% кто еще не знает — Vue.js это крайне легковесный (но очень богатый по функционалу) фреймворк, вобравший в себя многое хорошее, как из AngularJS, так и из React.


Фактически больше он похож все же на React, но шаблоны практически идентичны AngularJS (HTML + Mustache).


В реальности от AngularJS, он отличается очень даже разительно, но в поверхносном смысле,
понять как он работает, если у вас уже был опыт работы с React или AngularJS, будет несложно.


Предыстория


Было — большой проект на AngularJS


Последний мой проект, который совсем недавно вышел в production, мы писали на AngularJS 1.5-1.6.


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


Вот пример нашего компонента из данного проекта:


import {Component} from "shared-front/app/decorators";
import FileService, {ContentType, IFile} from "../file.service";
import AlertService from "shared/alert.service";

@Component({
    template: require("./file.component.html"),
    bindings: {
        item: "<",
    },
})
export default class FileComponent {
    public static $inject = ["fileService"];
    public item: IFile;

    constructor(private fileService: FileService, private alertService: AlertService) {
    }

    public isVideo() {
        return this.item.contentKeyType === ContentType.VIDEO;
    }

    public downloadFile() {
        this.fileService.download(this.getFileDownloadUrl()).then(() => {
            this.alertService.success();
        });
    }

    private getFileDownloadUrl() {
        return `url-for-download${this.item.text}`;
    }
}

На мой взгляд выглядит очень даже приятно, не слишком многословно, даже если вы не фанат TS.
К тому же все это замечательно тестируется как Unit-тестами, так и Е2Е.


Продолжай AngularJS развиваться, как тот же React, и будь он побыстрее, можно было бы и по сей день продолжать писать крупные проекты на нем.


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


Стало — средний проект на Angular


Так мы и поступили, рационально выбрав Angular 2 (позже 4) для нашего нового проекта несколько месяцев назад.


Выбор казался довольно очевидным, учитывая, что вся наша команда имеет большой опыт работы с первой версией. К тому же, лично я ранее работал с alpha-RC версиями и тогда проблемы фреймворка списывались на 0.х номер версии.


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


А вот пример компонента из проекта на Angular:


import {Component} from '@angular/core';

import FileService, {ContentType, IFile} from "../file.service";
import AlertService from "shared/alert.service";

@Component({
  selector: 'app-file',
  templateUrl: './file.component.html',
  styleUrls: ['./file.component.scss']
})
export class FileComponent {

    Input() item: IFile;

    constructor(private fileService: FileService, private alertService: AlertService) {
    }

    public isVideo() {
        return this.item.contentKeyType === ContentType.VIDEO;
    }

    public downloadFile() {
        this.fileService.download(this.getFileDownloadUrl()).subscribe(() => {
            this.alertService.success();
        });
    }

    private getFileDownloadUrl() {
        return `url-for-download${this.item.text}`;
    }
}

Возможно чуть чуть более многословно, но гораздо чище.


Плюсы


Angular CLI — единственное реальное преимущество перед AngularJS



Первое, что вы установите при разработке нового Angular 4 приложения это Angular CLI


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


Это то, чего так сильно не хватало в AngularJS, и все решали проблему отсутствия данной тулзы по-своему. Множество различных сидов (стартеров), сотни разных подходов к одному и тому же, в общем анархия. Теперь с этим покончено.


Конечно CLI тоже имеет ряд недостатков в части настроек и конфигурации "под себя", но все же он на голову выше аналогичных утилит для React (create-react-app) или Vue (vue-cli). Хотя второй, благодаря своей гибкости, становится лучше с каждым днем.


Минусы или "За что я перестал любить Angular"


Изначально я не хотел писать очередную хейтерскую статью вроде Angular 2 is terrible (нашелся даже перевод).


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


В целом не совсем разделяю взгяд автора на RxJS, т.к. библиотека невероятно мощная.


An Ajax request is singular, and running methods like Observable.prototype.map when there will only ever be one value in the pipe makes no semantic sense. Promises on the other hand represent a value that has yet to be fulfilled, which is exactly what a HTTP request gives you. I spent hours forcing Observables to behave before giving up using Observable.prototype.toPromise to transform the Observable back to a Promise and simply using Promise.all, which works much better than anything Rx.js offers.

В реальности, благодаря RxJS, со временем становится очень приятно воспринимать любые данные, пусть даже не совсем подходящие для Observable, как единую шину.


Но суровая правда в том, что Object.observe нативно мы все же не увидим:


After much discussion with the parties involved, I plan to withdraw the Object.observe proposal from TC39 (where it currently sits at stage 2 in the ES spec process), and hope to remove support from V8 by the end of the year (the feature is used on 0.0169% of Chrome pageviews, according to chromestatus.com).

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


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


Статья крайне рекомендуется к ознакомлению, если вы уже используете или планируете использовать Angular


Однако все же изложу несколько собственных мыслей (к сожалению, все больше негативных), не упомянутых в статье выше.


TypeScript в Angular


Пожалуй самое болезненное разочарование для меня — это то, во что превратили работу с TypeScript'ом в Angular.


Ниже несколько примеров наиболее неудачных решений.


Ужасные API


Одной из основных проблем использования TypeScript в Angular я считаю крайне спорные API.
Сам по себе TypeScript идеально подходит для написания максимально строгого кода, без возможностей случайно сделать шаг не в ту сторону. Фактически он просто создан для того, чтобы писать публичный API, но команда Angular сделала все, чтобы данное преимущество превратилось в недостаток.


Примеры:


HttpParams

По какой-то причине команда Angular решила сделать класс HttpParams иммутабельным.
Иммутабельность это здорово, но если вы думаете, что большинство классов в Angular являются таковыми, то это вовсе не так.


Например код вида:


let params = new HttpParams();
params.set('param', param);
params.set('anotherParam', anotherParam);
...
this.http.get('test', {params: params});

Не будет добавлять параметры к запросу. Казалось бы почему?
Ведь никаких ошибок, ни TypeScript ни сам Angular не отображает.


Только открыв сам класс в TypeScript можно найти комментарий


This class is immuatable — all mutation operations return a new instance.

Конечно, это совершенно неочевидно.


В вот и вся документация про них:


http
  .post('/api/items/add', body, {
    params: new HttpParams().set('id', '3'),
  })
  .subscribe();

RxJS operator import

Начнем с того, что документация по Angular вообще не имеет толкового разбора и описания Observable и того, как с ними работать.


Нет даже толковых ссылок на документацию по RxJS. И это при том, что Rx является ключевой частью фреймворка, а само создание Observable уже отличается:


// rx.js
Rx.Observable.create();
vs
// Angular
new Observable()

Ну да и черт с ним, здесь я хотел рассказать о Rx + TypeScript + Angular.


Допустим вы хотите использовать некий RxJS оператор, вроде do:


observable.do(event => {...})

В коде никакой ошибки не произойдет, это сработает и замечательно запустится.


Вот только, во время выполнения возникнет такая ошибка:


ERROR TypeError: observable.do is not a function

Потому что вы очевидно (потратили кучу времени на поиск проблемы) забыли заимпортировать сам оператор:


import 'rxjs/add/operator/do';

Почему это ломается в рантайме, если у нас есть TypeScript? Не знаю. Но это так.


Router API

Претензий к новому ангуляровскому роутеру у меня накопилось уже великое множество, но его API — одна из основных.


Events

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


this.router.events.subscribe(event => {
  if(event instanceof NavigationStart) {
    ...
  }
}


В очередной раз странным решением было сделать всю работу с роутами командами — причем массивом из них. Простые и наиболее распространенные переходы будут выглядеть как-то так:


this.router.navigate(['/some']);
...
this.router.navigate(['/other']);

Почему это плохо?


Потому что команды в данном случае имеют сигнатуру any[]. Для незнакомых с TypeScript — это фактически отключение его фич.


Это при том, что роутинг — наиболее слабо связанная часть в Angular.


Например мы в нашем AngularJS приложении наоборот старались типизировать роуты, чтобы вместо простых строк они были хотя бы enum. Это позволяет находить те или иные места, где используется данный роут без глобального поиска по строке 'some'.


Но нет, в Angular это преимущество TypeScript не используется никак.


Lazy Load

Этот раздел также мог бы пойти на отдельную статью, но скажу лишь, что в данном случае
игнорируются любые фичи TypeScript и название модуля пишется как строка, в конце через #


{
  path: 'admin',
  loadChildren: 'app/admin/admin.module#AdminModule',
},

Forms API

Для начала — в Angular есть два типа форм: обычные и реактивные.


Само собой, работать с ними нужно по-разному.


Однако лично меня раздражает именно API reactive forms:


// Зачем нужен первый пустой параметр?
// Почему name это массив c валидатором??
this.heroForm = this.fb.group({
  name: ['', Validators.required ],
});

или из документации


// Почему пустое поле это имя??
this.heroForm = this.fb.group({
  name: '', // <--- the FormControl called "name"
});

и так далее


this.complexForm = fb.group({   
  // Почему понадобился compose ?
  // Неужели нельзя без null ??
  'lastName': [null, Validators.compose([Validators.required, Validators.minLength(5), Validators.maxLength(10)])],
  'gender' : [null, Validators.required],
})

А еще — нельзя просто использовать атрибуты типа [disabled] с реактивными формами...


Это далеко не все примеры подобных откровенно спорных решений, но думаю, что для раздела про API этого хватит


__metadata


К сожалению использование горячо любимого мною TypeScript'а в Angular слишком сильно завязано на декораторы.


Декораторы — прекрасная вещь, но к сожалению в рантайме нет самой нужной их части, а именно __metadata.


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


Без метаданных декораторы тоже можно использовать — во время компиляции, но толку в таком случае от них не очень много.


Впрочем, мы в нашем AngularJS приложении использовали такие декораторы, например @Component:


export const Component = (options: ng.IComponentOptions = {}) => controller => angular.extend(options, {controller});

Он фактически просто оборачивает наши TypeScript классы в компоненты AngularJS и делает их контроллерами.


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


Абстракции


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


Самый яркий пример подобных проблем — это Dependency Injection в Angular.


Сама по себе концепция замечательная, особенно для unit тестирования. Но практика показывает, что большой нужды делать из фронтенда нечто Java-подобное нет. Да, в нашем AngularJS приложении мы очень активно это использовали, но поработав с тестированием Vue компонентов, я серьезно начал сомневаться в пользе DI.


В Angular для большинства обычных зависимостей, вроде сервисов, DI будет выглядеть очень просто, с получением через конструктор:


constructor(heroService: HeroService) {
  this.heroes = heroService.getHeroes();
}

Но так работает только для TypeScript классов, и если вы хотите добавить константу, необходимо будет использовать @Inject:


constructor(@Inject(APP_CONFIG) config: AppConfig) {
  this.title = config.title;
}

Ах да, сервисы которые вы будете инжектить должны быть проанотированы как @Injectable().


Но не все, а только те, у которых есть свои зависимости, если их нет — можно этот декоратор не указывать.


Consider adding @Injectable() to every service class, even those that don't have dependencies and, therefore, do not technically require it.
Here's why:

Future proofing: No need to remember @Injectable() when you add a dependency later.

Consistency: All services follow the same rules, and you don't have to wonder why a decorator is missing.

Почему бы не сделать это обязательным сразу, если потом это рекомендуется делать всегда?


Еще прекрасная цитата из официальной документации по поводу скобочек:


Always write @Injectable(), not just @Injectable. The application will fail mysteriously if you forget the parentheses.

Короче говоря, создается впечатление, что TypeScript в Angular явно используется не по назначению.


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


Синтаксис шаблонов


Синтаксис шаблонов — основная претензия к Angular. И по вполне объективным причинам.


Пример не только множества разных директив, но еще и разных вариантов их использования:


<div [ngStyle]="{'color': color, 'font-size': size, 'font-weight': 'bold'}">
  style using ngStyle
</div>

<input [(ngModel)]="color" />

<button (click)="size = size + 1">+</button>

<div [class.required]="isReq">Hello Wordl!</div>  
<div [className]="'blue'">CSS class using property syntax, this text is blue</div>
<div [ngClass]="{'small-text': true, 'red': true}">object of classes</div>
<div [ngClass]="['bold-text', 'green']">array of classes</div>
<div [ngClass]="'italic-text blue'">string of classes</div>

Изначально разработчики позиционировали новый синтакисис шаблонов, как спасение от множества директив.


Обещали, что будет достаточно только [] и ().


Binding Example
Properties <input [value]="firstName">
Events <button (click)="buy($event)">
Two-way <input [(ng-model)]="userName">

К сожалению в реальности директив едва ли не больше чем в AngularJS.



И да, простое правило запоминания синтаксиса two-way binding про банан в коробке
из официальной документации:


Visualize a banana in a box to remember that the parentheses go inside the brackets.

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


Вообще писать про документацию Angular даже нет смысла, она настолько неудачная,
что достаточно просто попытаться ее почитать, чтобы все стало понятно.


Контрпример — доки Vue. Мало того, что написаны подробно и доходчиво, так еще и на 6 языках, в т.ч. русском.


View encapsulation


Angular позволяет использовать так называемый View encapsulation.


Суть сводится к эмуляции Shadow DOM или использовании нативной его поддержки.


Сам по себе Shadow DOM — прекрасная вещь и действительно потенциально позволяет использовать даже разные CSS фреймворки для разных копмонентов без проблем.
Однако нативная поддержка на сегодняшний день совершенно печальна.


По умолчанию включена эмуляция Shadow DOM.


Вот пример простого CSS для компонента:


.first {
  background-color: red;
}
.first .second {
  background-color: green;
}
.first .second .third {
  background-color: blue;
}

Angular преобразует это в:


.first[_ngcontent-c1] {
  background-color: red;
}
.first[_ngcontent-c1]   .second[_ngcontent-c1] {
  background-color: green;
}
.first[_ngcontent-c1]   .second[_ngcontent-c1]   .third[_ngcontent-c1] {
  background-color: blue;
}

Совершенно не ясно зачем делать именно так.


Например Vue делает то же самое, но гораздо чище:


.first[data-v-50646cd8] {
  background-color: red;
}
.first .second[data-v-50646cd8] {
  background-color: green;
}
.first .second .third[data-v-50646cd8] {
  background-color: blue;
}

Не говоря уже о том, что в Vue это не дефолтное поведение и включается добавлением простого scoped к стилю.


Так же хотелось бы отметить, что Vue (vue-cli webpack) подобным же образом позволяет указывать SASS/SCSS, тогда как для Angular CLI нужны команды типа ng set defaults.styleExt scss. Не очень понятно зачем все это, если внутри такой же webpack.


Но это все ерунда, реальные проблемы у нас начались, когда нам потребовалось использовать сторонние компоненты.


В частности мы использовали один из наиболее популярных UI фреймворков — PrimeNG, а он иногда использует подобные селекторы:


body .ui-tree .ui-treenode .ui-treenode-content .ui-tree-toggler {
    font-size: 1.1em;
}

Они по определению имеют больший приоритет нежели стили компонента, который использует в себе тот или иной сторонний элемент.


Что в итоге приводит к необходимости писать нечто подобное:


body :host >>> .ui-tree .ui-treenode .ui-treenode-content .ui-tree-toggler {
  font-size: 2em;
}

Иногда и вовсе приходилось вспомнить великий и ужасный !important.


Безусловно все это связано конкретно с PrimeNG и не является как таковой проблемой фреймворка, но это именно та проблема, которая скорее всего возникнет и у вас при реальной работе с Angular.


К слову о стабильности


В примере выше мы использовали >>> — как и /deep/ это алиас для так называемого shadow-piercing селектора.


Он позволяет как бы "игнорировать" Shadow DOM и для некоторых сторонних компонентов порой просто незаменим.


В одном из относительно свежих релизов Angular создатели фреймворка решили,
в соответствии со стандартом, задепрекейтить /deep/ и >>>.


Никаких ошибок или ворнингов их использование не принесло, они просто перестали работать.
Как выяснилось позже, теперь работает только ::ng-deep — аналог shadow-piercing селектора в Angular вселенной.


Обновление это было отнюдь не мажорной версии (4.2.6 -> 4.3.0), просто в один прекрасный момент наша верстка во многих местах поползла (спасибо и NPM за версии с шапочкой ^).


Конечно, не все наши разработчики ежедневно читают ChangeLog Angular 4, да и за трендами веб разработки не всегда можно уследить. Само собой сначала грешили на собственные стили — пришлось потратить немало времени и нервов для обнаружения столь неприятной особенности.


К тому же скоро и ::ng-deep перестанет работать. Как в таком случае править стили кривых сторонних компонентов, вроде тех же PrimeNG, ума не приложу.


Наш личный вывод: дефолтная настройка — эмуляция Shadow DOM порождает больше проблем чем решает.


Свой HTML парсер


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


Нет смысла холиварить на тему ухода Angular от стандартов, но по мнению многих это довольно странная идея, ведь в том же AngularJS обычного HTML (регистронезависимого) вполне хватало.


С AngularJS нередко бывало такое: добавили вы некий <my-component/> а тест не написали.
Прошло некоторое время и модуль который содержит логику данного компонента был удален/отрефакторен/итд.


Так или иначе — теперь ваш компонент не отображается.


Теперь он сразу определяет неизвестные теги и выдает ошибку если их не знает.


Правда теперь любой сторонний компонент требует либо полного отключения этой проверки,
либо включения CUSTOM_ELEMENTS_SCHEMA которая пропускает любые тэги с -


Сорцы можете оценить самостоятельно


...
const TAG_DEFINITIONS: {[key: string]: HtmlTagDefinition} = {
  'base': new HtmlTagDefinition({isVoid: true}),
  'meta': new HtmlTagDefinition({isVoid: true}),
  'area': new HtmlTagDefinition({isVoid: true}),
  'embed': new HtmlTagDefinition({isVoid: true}),
  'link': new HtmlTagDefinition({isVoid: true}),
  'img': new HtmlTagDefinition({isVoid: true}),
  'input': new HtmlTagDefinition({isVoid: true}),
  'param': new HtmlTagDefinition({isVoid: true}),
  'hr': new HtmlTagDefinition({isVoid: true}),
  'br': new HtmlTagDefinition({isVoid: true}),
  'source': new HtmlTagDefinition({isVoid: true}),
  'track': new HtmlTagDefinition({isVoid: true}),
  'wbr': new HtmlTagDefinition({isVoid: true}),
  'p': new HtmlTagDefinition({
    closedByChildren: [
      'address', 'article', 'aside', 'blockquote', 'div', 'dl',      'fieldset', 'footer', 'form',
      'h1',      'h2',      'h3',    'h4',         'h5',  'h6',      'header',   'hgroup', 'hr',
      'main',    'nav',     'ol',    'p',          'pre', 'section', 'table',    'ul'
    ],
    closedByParent: true
  }),
...
  'td': new HtmlTagDefinition({closedByChildren: ['td', 'th'], closedByParent: true}),
  'th': new HtmlTagDefinition({closedByChildren: ['td', 'th'], closedByParent: true}),
  'col': new HtmlTagDefinition({requiredParents: ['colgroup'], isVoid: true}),
  'svg': new HtmlTagDefinition({implicitNamespacePrefix: 'svg'}),
  'math': new HtmlTagDefinition({implicitNamespacePrefix: 'math'}),
  'li': new HtmlTagDefinition({closedByChildren: ['li'], closedByParent: true}),
  'dt': new HtmlTagDefinition({closedByChildren: ['dt', 'dd']}),
  'dd': new HtmlTagDefinition({closedByChildren: ['dt', 'dd'], closedByParent: true}),
  'rb': new HtmlTagDefinition({closedByChildren: ['rb', 'rt', 'rtc'
  ...

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


Однако это решается прекомпиляцией шаблонов благодаря другому, AOT компилятору.
Нужно всего лишь собирать с флагом --aot, но и здесь не без ложки дегтя, это плохо работает с ng serve и еще больше тормозит и без того небыструю сборку. Видимо поэтому он и не включен по умолчанию (а стоило бы).


Наличие двух различно работающих компиляторов само по себе довольно опасно звучит,
и на деле постоянно приводит к проблемам.


У нас с AOT было множество разнообразных ошибок, в том числе весьма неприятные и до сих пор открытые, например нельзя использовать экспорты по умолчанию


Обратите внимание на элегантные предлагаемые решения проблемы:


don't use default exports :)

Just place both export types and it works

Или нечто подобное описанному здесь (AOT не всегда разбирает замыкания)


Код подобного вида вызывает очень странные ошибки компилятора AOT:


@NgModule({
  providers: [
    {provide: SomeSymbol, useFactor: (i) => i.get('someSymbol'), deps: ['$injector']}
  ]
})
export class MyModule {}

Приходится подстраиваться под компилятор и переписывать код в более примитивном виде:


export factoryForSomeSymbol = (i) => i.get('someSymbol');

@NgModule({
  providers: [
    {provide: SomeSymbol, useFactor: factoryForSomeSymbol, deps: ['$injector']}
  ]
})
export class MyModule {}

Также хотелось бы отметить, что текст ошибок в шаблонах зачастую совершенно неинформативен.


Zone.js


Одна из концептуально очень крутых вещей которые появились в Angular это Zone.js.
Он позволяет отслеживать контекст выполнения для асинхронных задач, но новичков отпугивают огромной длины стактрейсы. Например:


core.es5.js:1020 ERROR Error: Uncaught (in promise): Error: No clusteredNodeId supplied to updateClusteredNode.
Error: No clusteredNodeId supplied to updateClusteredNode.
    at ClusterEngine.updateClusteredNode (vis.js:47364)
    at VisGraphDataService.webpackJsonp.../../../../../src/app/services/vis-graph-data.service.ts.VisGraphDataService.updateNetwork (vis-graph-data.service.ts:84)
    at vis-graph-display.service.ts:63
    at ZoneDelegate.webpackJsonp.../../../../zone.js/dist/zone.js.ZoneDelegate.invoke (zone.js:391)
    at Object.onInvoke (core.es5.js:3890)
    at ZoneDelegate.webpackJsonp.../../../../zone.js/dist/zone.js.ZoneDelegate.invoke (zone.js:390)
    at Zone.webpackJsonp.../../../../zone.js/dist/zone.js.Zone.run (zone.js:141)
    at zone.js:818
    at ZoneDelegate.webpackJsonp.../../../../zone.js/dist/zone.js.ZoneDelegate.invokeTask (zone.js:424)
    at Object.onInvokeTask (core.es5.js:3881)
    at ClusterEngine.updateClusteredNode (vis.js:47364)
    at VisGraphDataService.webpackJsonp.../../../../../src/app/services/vis-graph-data.service.ts.VisGraphDataService.updateNetwork (vis-graph-data.service.ts:84)
    at vis-graph-display.service.ts:63
    at ZoneDelegate.webpackJsonp.../../../../zone.js/dist/zone.js.ZoneDelegate.invoke (zone.js:391)
    at Object.onInvoke (core.es5.js:3890)
    at ZoneDelegate.webpackJsonp.../../../../zone.js/dist/zone.js.ZoneDelegate.invoke (zone.js:390)
    at Zone.webpackJsonp.../../../../zone.js/dist/zone.js.Zone.run (zone.js:141)
    at zone.js:818
    at ZoneDelegate.webpackJsonp.../../../../zone.js/dist/zone.js.ZoneDelegate.invokeTask (zone.js:424)
    at Object.onInvokeTask (core.es5.js:3881)
    at resolvePromise (zone.js:770)
    at zone.js:696
    at zone.js:712
    at ZoneDelegate.webpackJsonp.../../../../zone.js/dist/zone.js.ZoneDelegate.invoke (zone.js:391)
    at Object.onInvoke (core.es5.js:3890)
    at ZoneDelegate.webpackJsonp.../../../../zone.js/dist/zone.js.ZoneDelegate.invoke (zone.js:390)
    at Zone.webpackJsonp.../../../../zone.js/dist/zone.js.Zone.run (zone.js:141)
    at zone.js:818
    at ZoneDelegate.webpackJsonp.../../../../zone.js/dist/zone.js.ZoneDelegate.invokeTask (zone.js:424)
    at Object.onInvokeTask (core.es5.js:3881)

Некоторым Zone.js нравится и наверняка в продакшене нам это еще не раз поможет, но в нашем проекте большой пользы мы пока не извлекли.


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


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


И сама суть проблемы остается где-то далеко наверху, если ошибка происходит слишком много раз.



UI frameworks


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


Да, я понимаю, что выбирать фреймворк по наличиую UI компонентов в корне неверно,
но при реальной разработке это необходимо.


Вот список основных UI фреймворков для Angular: https://angular.io/resources (раздел UI components).


Рассмотрим наиболее популярные бесплатные варианты.


Angular Material 2



Безусловно, наибольшие надежды я возлагал на Angular Material 2 ввиду того, что разрабатывается он командой Angular и наверняка будет соответствовать всем гайдлайнам.


К сожалению, несмотря на его возраст, набор компонентов крайне мал.


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


Я считаю, что Angular Material 2 подойдет лишь небольшим или, в лучшем случае, средним проектам, т.к. до сих пор нет, например, деревьев. Часто очень нужны компоненты вроде multiple-select, коих тоже нет.


Отдельно стоит сказать про очень скупую документацию и малое количество примеров.


Перспективы развития тоже довольно печальные.


Feature Status
tree In-progress
stepper In-progress, planned Q3 2017
sticky-header In-progress, planned Q3 2017
virtual-repeat Not started, planned Q4 2017
fab speed-dial Not started, not planned
fab toolbar Not started, not planned
bottom-sheet Not started, not planned
bottom-nav Not started, not planned

Bootstrap



По тем же причинам, что и выше не буду останавливаться на Bootstrap фреймворках типа
ng2-bootstrap (получше) и ngx-bootstrap. Они очень даже неплохи, но простейшие вещи можно сделать и обычным CSS, а сложных компонентов тут нет (хотя наверняка многим будет достаточно modal, datepicker и typeahead).


Prime Faces



Это на сегодняшний день наиболее популярый фреймворк содержащий множество сложных компонентов. В том числе гриды и деревья (и даже Tree Table!).


Изначально я вообще довольно скептически относился к PrimeFaces т.к. у меня был давний опыт работы с JSF, и много неприятных воспоминаний. Да и выглядят PrimeNG визуально так же (не очень современно). Но на момент начала нашего проекта достойных альтернатив не было, и все же хочется сказать спасибо разработчикам за то, что в короткие сроки был написан дейтсивтельно широчайший инструментарий.


Однако проблем с этим набором компонентов у нас возникало очень много.


Часто документация совершенно не помогает. Забыли добавить некий модуль — что-то просто молча не работает и никаких ошибок не возникает. Приходится дебажить и выясняется, что сорцы совершенно без комментариев.


В общем, несмотря на наличие большого количества готовых компонентов,
по возможности, я бы не рекомендовал выбирать PrimeNG в качестве базы.


Clarity



Луч света в темном царстве — это относительно молодая (меньше года от роду) библиотека Clarity от vmware.


Набор компонентов впечатляет, документация крайне приятная, да и выглядят здорово.


Фреймворк не просто предоставляет набор UI компонентов, но и CSS гайдлайны.
Эдакий свой bootstrap. Благодаря этому достигается консистентный и крайне приятный/минималистичный вид компонентов.


Гриды очень функциональные и стабильные, а сорцы говорят сами за себя (о боже, комментарии и юнит тесты, а так можно было?).


Однако пока что очень слабые формы, нет datepicker'а и select2-подобного компонента.
Работа над ними идет в данный момент: DatePicker, Select 2.0 (как всегда дизайн на высоте, и хотя с разработкой не торопятся я могу быть уверен, что делают на совесть).


Пожалуй, "Clarity Design System" — единственная причина почему я еще верю в жизнь Angular
(и вообще единственный фреймворк который не стыдно использовать для enterprise разработки). Как никак VMware серьезнейший мейнтейнер и есть надежда на светлое будущее.


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


Но он лишь один


Да, я считаю что для Angular на сегодняшний день есть лишь один достойный UI фреймворк.
О чем это говорит?


Полноценно разрабатывать такие фреймворки для Angular могут лишь серьезнейшие компании вроде той же VMware. Нужен ли вам такой суровый enterprise? Каждый решает сам.


А теперь давайте посмотрим, что происходит с одним из свежих конкурентов.


Vue UI frameworks



Для сравнения мощные уже существующие фреймворки для Vue.js с теми же гридами:


Element (~15k stars), Vue Material (существенно младше Angular Material 2 но уже содержит в разы больше), Vuetify (снова Material и снова множество компонентов), Quasar, также надо отметить популярные чисто китайские фреймворки типа iView и Muse-UI (iView выглядит очень приятно, но документация хромает).


Вот и простой, но очевидный, пример того, что писать компоненты на Vue гораздо проще.
Это позволяет даже выбирать наиболее приглянувшийся из множества наборов компонентов,
нежели надяться на один, который поддерживает огромная команда.


Какой вывод мы сделали?


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


В реальности все это лишь увеличивает время разработки, не снижая стоимость поддержки и рефакторинга. Поэтому для нашего нового проекта мы выбрали Vue.js.


Достаточно просто развернуть базовый webpack шаблон для vue-cli и оценить скорость работы библиотеки. Несмотря на то, что лично я всегда был сторонником фреймворков all-in-one,
Vue без особых проблем делает почти все то же, что и Angular.


Ну и конечно, множество тех же UI framework'ов также играет свою роль.


Единственное чего немного не хватает — чуть более полноценной поддержки TypeScript,
уж очень много головной боли он нам сэкономил за эти годы.


Но ребята из Microsoft уже вот вот вмержает ее. А потом она появится и в webpack шаблоне.


Почему мы не выбрали React? После AngularJS наша команда гораздо проще вошла в Vue,
ведь все эти v-if, v-model и v-for уже были очень знакомы.


Лично мне во многом, но не во всем, нравится Aurelia но уж больно она малопопулярна,
а в сравнении с взрывным ростом популярности Vue она кажется совсем неизвестной.


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


И поверьте, спустя 4 года работы с Angular, вот так бросить его было очень нелегко.
Но достаточно один раз попробовать Vue...


английская версия поста: https://medium.com/@igogrek/how-i-stopped-loving-angular-c2935f7378c4

Проголосовать:
+106
Сохранить: