Сейчас во фронтенде среди фреймворков есть три явных лидера: Angular, React и Vue. Думаю, мы можем судить о любви разработчиков к проекту по количеству звезд на GitHub. На момент написания данной статьи у Vue уже 161 тысяча звезд, на втором месте находится React с 146 тысячами, а на третьем месте — Angular со своими скромными 59.6 тысячами.


С первого взгляда может показаться, что Angular не настолько популярный, как другие фреймворки, но если обратиться к результатам исследования статистики с портала Tecla, то мы увидим, что Angular занимает довольно большую долю рынка. Например, по данным исследования Angular работает на более чем 400 тысячах сайтов, в то время как Vue — на 100 тысячах. Предлагаю в этом разобраться. Рассмотрим, за что разработчики любят Vue, почему много приложений написаны на Angular и какие выгоды может получить разработчик при использовании фреймворка от Google конкретно для себя.



Про любовь


Чаще всего выделяют следующие пункты особенного удобства Vue (они перечислены не в порядке приоритета):


  • Низкий порог вхождения
  • Размер бандла
  • Производительность
  • Однофайловые компоненты
  • Простой синтаксис шаблонов
  • Простота расширения плагинами и модулями
  • Виртуальный DOM
  • Vue CLI
  • Вам не нужно задумываться о процессе рендеринга
  • Отличная документация на множестве языков

Хотел бы обратить ваше внимание, что достаточно большая часть — это Developer Experience, то есть комфорт работы с фреймворком. Думаю, что теперь причина любви стала вполне очевидной. Если обобщить, то вы можете сесть, запустить vue-cli, сконфигурировать проект как вам нужно и сразу же начать делать классные вещи, не думая о рендеринге, оптимизации, сборке и многом другом.


Но если работа с Vue — это сплошное удовольствие, то почему в больших компаниях Angular используется гораздо чаще? Когда одно из самых важных в разработке — это простота, комфорт и скорость, то тогда вам нужен Vue. Но когда речь заходит уже о крупном проекте с большим количеством разработчиков, сложной работой с данными и разнообразным API, то ситуация меняется. И давайте подробнее разберемся почему.


Все уже решено


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


В каждом SPA-приложении требуется работа с API. Как будет реализована работа в каждом из фреймворков?


  • Vue: собираемся командой и решаем что будем использовать. Нативный fetch или axios или что-то новое? Как будем обрабатывать ошибки? В каком месте будем отправлять запросы? Как будут называться классы? После того как вы решили и договорились — начали работать. Пришел новый сотрудник — ему нужно все это рассказать или составить документацию. Иногда новым разработчикам приходится доказывать, что ваше решение было лучшим на момент договоренности.
  • Angular: вы используете встроенный в Angular HttpClient, запросы отправляете из сервисов, которые работают с определенными сущностями. Классы будут носить стандартные названия, генерируемые Angular CLI. Обработку ошибок можно сделать в одном месте с помощью встроенного Interceptor. Приходит новый разработчик, и он ожидает, что все работает именно так.

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


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


Готовые паттерны


Мы уже поговорили о сервисах, интерсепторах и клиенте для работы с API. Предлагаю рассмотреть что вы можете получить еще.


Формы


"Из коробки" во Vue работать с формами очень сложно. Вам нужно сформировать начальные данные, передать их в инпуты, ловить изменения и отражать их в данных. А если нужна валидация? Куда вы будете выводить ошибки? Как будете понимать, что форма еще не была "потрогана" пользователем? Да, есть классные сторонние библиотеки вроде Vuelidate. Но, вам как минимум нужно о них знать.


В Angular “из коробки” реализована потрясающая работа с формами. Вы просто создаете FormGroup, передаете начальные значения, ставите нужные валидаторы и… Все. Далее вы можете управлять формой как угодно. Сбрасывать значения, ставить состояние dirty или принудительно указывать состояние валидности. То есть вам нужно думать исключительно о бизнес-логике.


Dependency Injection


DI — это механизм управления зависимостями, который выносит логику управления экземплярами из класса вовне. Можно сказать, что это самое важное отличие Angular от других фреймворков.


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


Во Vue:


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

В Angular:


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


На самом деле, это довольно поверхностное описание работы механизма DI. Он позволяет очень удобно работать с зависимостями. Есть множество статей, которые описывают данный механизм, в том числе и моя. Если интересно, то можно ознакомиться.


CLI


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


В мире Angular же CLI позволяет не только сконфигурировать и создать проект, но и сгенерировать сервисы, модули, компоненты, директивы и даже целые библиотеки. При создании компонента не только формируются все необходимые файлы, но и импортируются в нужные модули.


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


Angular CLI избавляет вас от массы рутинных операций, позволяя не думать о процессе создания файлов и прописывания импортов. Лучше потратьте сэкономленное время на архитектуру, отдых или на чтение Хабра.


Базовая архитектура


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


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


В чем они похожи


TypeScript


TypeScript — это статически типизированный язык, "JavaScript that scales", как называют его создатели. Он привносит в JavaScript типы, позволяет писать красивые классы, как в "больших" языках программирования. О его возможностях и преимуществах в сравнении с JavaScript написано много статей и книг.


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


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


Router


На сегодняшний день сложно представить себе SPA-приложение без Lazy Loading'а. Если раньше мы реализовывали это с помощью Webpack'а, копались в его настройках и устанавливали плагины, то теперь фреймворки делают это за нас. Нам достаточно только написать одну конфигурацию.


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


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


Observable


Во Vue при обновлении значения в поле data автоматически происходит обновление связанных данных и перерисовка шаблона. Вам не нужно принудительно вызывать метод render() компонента или вообще как-то думать об этом процессе. Но как же это происходит?


Здесь все просто — значения обновляются реактивно. Любое поле в данных — это Observer, то есть реализация паттерна "Наблюдатель". Давайте представим, что есть объект, у которого есть какое-то значение. И есть другие объекты, которые "наблюдают" за первым объектом. При изменении значения первого объекта наблюдатели узнают об этом и производят какие-то действия.


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


Небольшой итог


Давайте подведем итог и посмотрим что вы, как разработчик Vue, можете получить при работе с Angular:


  • Функционал "из коробки"
  • Готовые паттерны решения проблем
  • Быстрое включение других разработчиков в проект в контексте Angular
  • Экономию времени на обсуждениях за счет готовых решений и подходов
  • Мощный CLI

Примеры


А теперь я предлагаю реализовать компонент с одинаковым функционалом на Vue и Angular.


Vue


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


Код компонента


<script>
export default {
  name: 'HelloHabr',
  props: {
    name: {
      type: String,
      required: true,
      validator: value => value.trim().length !== 0,
    },
  },
  data() {
    return {
      clickCount: 0,
      secondsCount: 0,
      intervalId: null,
    };
  },
  computed: {
    clicksPerSecond() {
      if (this.secondsCount === 0) {
        return 0;
      }

      const DECIMAL = 1000;
      const ratio = this.clickCount / this.secondsCount;

      return Math.round(ratio * DECIMAL) / DECIMAL;
    },
  },
  created() {
    this.intervalId = setInterval(() => {
      this.secondsCount++;
    }, 1000);
  },
  beforeDestroy() {
    clearInterval(this.intervalId);
  },
  methods: {
    countUp() {
      this.$emit('updateClick', ++this.clickCount);
    },
  },
};
</script>

Шаблон компонента


<template>
  <div class="hello">
    <p>Привет, Хабр! Это {{ name }}.</p>

    <div class="click-place" :class="{ clicked: clickCount > 0 }">
      <button @click="countUp()">Кликни меня!</button>
      <p v-if="clickCount > 0">Общее количество кликов: {{ clickCount }}</p>
      <p v-else>Пока вы ни разу не кликнули</p>
    </div>

    <div class="statistic">
      <p>С момента открытия прошло {{ secondsCount }} секунд.</p>
      <p>Кликов в секунду: {{ clicksPerSecond }}</p>
    </div>
  </div>
</template>

Здесь мы также видим данные, обработку событий, динамические свойства и директивы.


Использование компонента


Чтобы использовать данный компонент, достаточно просто импортировать его в родителя:


<script>
  import HelloHabr from './components/HelloHabr.vue';

  export default {
    name: 'App',
    components: { HelloHabr }
    ...
  };
</script>

Шаблон родителя будет выглядеть так:


<template>
  <div id="app">
    <HelloHabr :name="name" @updateClick="updateClickCount" />
  </div>
</template>

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


Angular


Код компонента


Код класса будет выглядеть так:


import {
  Component,
  EventEmitter,
  Input,
  OnDestroy,
  OnInit,
  Output,
} from '@angular/core';

@Component({
  selector: 'hello-habr',
  templateUrl: './hello-habr.component.html',
  styleUrls: ['./hello-habr.component.less'],
})
// Указываем название класса и говорим какие интерфейсы он реализует.
// В данном случае это необходимо, чтобы "напомнить" разработчику,
// что компонент реализует указанные хуки жизненного цикла.
export class HelloHabrComponent implements OnInit, OnDestroy {
  // Объявляем параметр компонента
  @Input() name: string;

  // Таким образом мы сообщаем наверх о событии
  @Output() updateClick = new EventEmitter<number>();

  // Состояние компонента
  clickCount = 0;
  secondsCount = 0;
  intervalId = null;

  // Хук компонента на момент инициализации
  ngOnInit() {
    this.intervalId = setInterval(() => {
      this.secondsCount++;
    }, 1000);
  }

  // Хук компонента на момент удаления
  ngOnDestroy() {
    clearInterval(this.intervalId);
  }

  // Геттер, который заменяет вычисляемое значение
  // Его логика работы отличается от Vue, но в текущем
  // примере данный вариант подходит
  get clicksPerSecond(): number {
    if (this.secondsCount === 0) {
      return 0;
    }

    const DECIMAL = 1000;
    const ratio = this.clickCount / this.secondsCount;

    return Math.round(ratio * DECIMAL) / DECIMAL;
  }

  // Метод компонента
  countUp() {
    this.updateClick.emit(++this.clickCount);
  }
}

Шаблон компонента


Данному компоненту будет соответствовать следующий шаблон:


<p>Привет, Хабр! Это {{ name }}.</p>

<!-- В Angular можно указать едичный динамический класс -->
<div class="click-place" [class.clicked]="clickCount > 0">
  <!-- Обработка событий примерно такая же, но без модификаторов  -->
  <button (click)="countUp()">Кликни меня!</button>

  <!-- Директивы, управляющие наличием DOM-элементов начинаются со звездочки -->
  <!-- else-блок нужно указать прямо в условии -->
  <p *ngIf="clickCount > 0; else elseBlock">
    Общее количество кликов: {{ clickCount }}
  </p>
  <!-- Сам else-блок выглядит немного иначе -->
  <ng-template #elseBlock><p>Пока вы ни разу не кликнули</p></ng-template>
</div>

<div class="statistic">
  <p>С момента открытия прошло {{ secondsCount }} секунд.</p>
  <p>Кликов в секунду: {{ clicksPerSecond }}</p>
</div>

Что касается компонента, то это все. В Angular есть целое множество возможностей переписать этот компонент по-другому. Например, используя потоки данных. Если интересно, то код компонента на потоках я представил ниже. На данном же этапе я хочу продемонстрировать схожее в двух фреймворках, поэтому реализовал все таким же образом, как и во Vue.


Использование компонента


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


import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
import { HelloHabrComponent } from './hello-habr/hello-habr.component';

@NgModule({
  // Здесь мы указываем, что компонент используется в модуле
  declarations: [AppComponent, HelloHabrComponent],
  imports: [BrowserModule],
  providers: [],
  bootstrap: [AppComponent],
})
export class AppModule {}

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


Но есть и другой способ. Например, наш компонент использует другие модули. Чтобы не импортировать все в AppModule, мы создаем специальный модуль и импортируем компонент уже в него. Чтобы компонент стал доступен вне модуля, нужно его экспортировать. Выглядит это так:


import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { HelloHabrComponent } from './hello-habr.component';

@NgModule({
  // Так же указываем, что компонент используется в модуле
  declarations: [HelloHabrComponent],
  // CommonModule позволяет использовать директивы вроде *ngIf
  imports: [CommonModule],
  // Делаем его доступным вне модуля HelloHabrModule
  exports: [HelloHabrComponent],
})
export class HelloHabrModule {}

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


import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppComponent } from './app.component';
import { HelloHabrModule } from './hello-habr/hello-habr.module';

@NgModule({
  declarations: [AppComponent],
  // Импортируем модуль и можем использовать "HelloHabrComponent"
  imports: [BrowserModule, HelloHabrModule],
  providers: [],
  bootstrap: [AppComponent],
})
export class AppModule {}

Такой способ работы позволяет инкапсулировать всю специальную логику в одном месте и при импорте не думать об этом. Плюс, разделение на модули позволяет удобно управлять Lazy-Loading'ом.


В шаблоне родителя использование компонента вне зависимости от реализации декларации будет выглядеть одинаково:


<hello-habr [name]="name" (updateClick)="updateClickCount($event)"></hello-habr>

Код компонента на потоках


Вот как можно реализовать тот же самый компонент, но уже на потоках:


import { Component, EventEmitter, Input, Output } from '@angular/core';
import { BehaviorSubject, combineLatest, interval } from 'rxjs';
import { map, startWith } from 'rxjs/operators';

@Component({
  selector: 'hello-habr',
  templateUrl: './hello-habr.component.html',
  styleUrls: ['./hello-habr.component.less'],
})
export class HelloHabrComponent {
  @Input() name: string;
  @Output() updateClick = new EventEmitter<number>();

  clicksCount$ = new BehaviorSubject<number>(0);

  secondsCount$ = interval(1000).pipe(
    map(second => second++),
    startWith(0)
  );

  clicksPerSecond$ = combineLatest(this.clicksCount$, this.secondsCount$).pipe(
    map(([clicks, seconds]) => this.getClicksPerSecond(clicks, seconds))
  );

  countUp() {
    const newValue = this.clicksCount$.value + 1;

    this.updateClick.emit(newValue);
    this.clicksCount$.next(newValue);
  }

  private getClicksPerSecond(
    clicksCount: number,
    secondsCount: number
  ): number {
    if (secondsCount === 0) {
      return 0;
    }

    const DECIMAL = 1000;
    const ratio = clicksCount / secondsCount;

    return Math.round(ratio * DECIMAL) / DECIMAL;
  }
}

В данном примере все считается декларативно. Что это нам дает:


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

Итог


Итак, давайте подведем итог. Почему большие проекты, скорее всего, будет удобнее писать на Angular:


  • Готовые паттерны решения проблем.
  • Множество функционала доступно из коробки: роутер, формы и их валидация, перехватчики (interceptors), клиент для работы с запросами и многое другое.
  • Мощный CLI, который позволяет не только создавать и запускать проект, но и создавать файлы, корректно выставлять зависимости и так далее.
  • Dependency Injection — механизм управления зависимостями.
  • RxJS и TypeScript по умолчанию.

Что вы можете получить как разработчик:


  • Экономию времени на обсуждениях и спорах.
  • Экономию времени на изобретении велосипедов.
  • Возможность быстро включиться в процесс разработки на Angular практически в любой команде.

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