Angular vs. React: что лучше для веб-разработки?

Существует огромное множество статей, в которых ведется обсуждение того, что лучше для веб-разработки React или Angular? Нужна ли нам еще одна?

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

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

Ошибка Angular: Несвоевременное объявление

Два года назад я написал статью об экосистеме React. Среди множества пунктов в статье утверждалось, что Angular стал жертвой «предварительных объявлений (обещаний)». Тогда выбор между Angular и практические любым другим фреймворком был очевиден для всех, кто не хотел, чтобы их проект работал на устаревшем фреймворке. Первая версия Angular была устаревшей, а вторая версия даже не была доступна в альфа-версии.

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

Два года спустя мы имеем четвертую версию Angular вместе с обещаниями разработчиков об её стабильности.

Что же теперь?

Angular vs. React: Сравнение яблок и апельсинов

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

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

Самая большая разница заключается в области управления созданием программного обеспечения. Angular включает в себя работу с данными, в то время как React, на сегодняшний день, обычно дополняется библиотекой Redux для обеспечения однонаправленного потока работы с данными, а также для работы с неизменяемыми данными. Сами по себе подходы полностью противоположны друг другу, и на данный момент ведется бесчисленное количество дискуссий о том, что лучше: работа с изменяемыми данными или с неизменяемыми/однонаправленными.

Игра на равных

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

Отметим, что в Angular есть некоторое количество определенных спец-возможностей, которые по умолчанию не указаны:

ВозможностьAngular пакетбиблиотека React

  • Связывание данных, внедрение зависимостей (DI) – @angular/core  - MobX
  • Обработанные свойства –  rxjsMobX
  • Компонентно-ориентированная маршрутизация – @angular/router – React Router v4
  • Компоненты material design – @angular/material  - React Toolbox
  • Атрибут ‘scoped’ CSS-компонентов – @angular/core – CSS modules
  • Проверка формы – @angular/forms – FormState
  • Генератор проектов – @angular/cli  –  React Scripts TS

Привязка данных

Подход, основанный на работе с привязкой данных, скорее всего, будет проще на начальных этапах, чем подход, основанный на работе с однонаправленными данными. Без сомнения, можно использовать абсолютно иной подход и использовать такие связки технологий:  Redux или  mobx-state-tree совместно с React, ngrx совместно с Angular. Но это уже будет темой для другой статьи.

Обработанные свойства

В то время как производительность напрямую связана с обычными геттерами в Angular, и это бесспорный факт, так как они вызываются при каждом рендере. Для выполнения этой работы можно использовать BehaviorSubject от  RsJS.

В случае с React возможно использование @computed от MobX, которая помогает решить туже задачу, но, возможно, с более удобным API.

Внедрение зависимостей

Внедрение зависимостей является предметом для споров, потому что данное понятие идет вразрез с нынешней парадигмой функционального программирования и работы с неизменяемыми данными в React. Как оказалось, некоторая часть зависимостей просто необходима в среде работы с привязанными данными, так как это помогает с развязкой (и соответственно с тестированием и работой с заглушками), где отсутствует раздельная многоуровневая структура данных.

Еще одним преимуществом DI (внедрением данных), которое имеет поддержку в Angular, является возможность иметь несколько жизненных циклов различных блоков данных. Большая часть нынешних React парадигм использует некоторые виды глобальных состояний приложения, которые сопоставляются с различными компонентами, но из своего опыта могу добавить, что здесь очень легко, при очистке глобальных состояний за счет размонтирования компонент, наделать ошибок.

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

Подобное решение в Angular есть «из коробки», но оно довольно легко воспроизводится с помощью MobX.

Маршрутизация

Компонентно-ориентированная маршрутизация позволяет компонентам управлять своим под-маршрутами вместо единой глобальной конфигурации маршрута. Этот подход был окончательно реализован в 4 версии React Router.

Material design

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

Я преднамеренно выбрал  React Toolbox вместо, обычно рекомендуемого Material UI, связано это с тем, что Material UI имеет, откровенно говоря, серьезные проблемы производительности за счет их подхода, основанного на встроенных CSS-стилях. В следующей версии они планируют решать эти проблемы.

Кроме того, PostCSS/cssnext, используемый в React Toolbox, в любом случае начинает постепенно заменять Sass/LESS.

Атрибут ‘scoped’ CSS-компонентов

CSS-классы чем-то напоминают глобальные переменные. Существует гигантское количество способов организации CSS, направленных на предотвращение конфликтов (включая методологию web-разработки - BEM), но сегодня явно просматривается тенденция использования различных библиотек, которые помогают обрабатывать CSS-свойства так, чтобы не возникало исключительных ситуаций. Подобные библиотеки дают возможность front-end разработчикам не создавать сложные системы именования CSS-классов.

Проверка формы

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

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

Наличие CLI(Command Line Interface) генератора в проекте – это просто более удобно, чем клонирование шаблонного кода из GitHub.

Построение одного и того же приложения дважды

Итак, мы собираемся создать два абсолютно одинаковых приложения. Одно будет создано с помощью React, другое с помощью Angular. Ничего сверхъестественного просто «Доска объявлений», которая даст любому желающему возможность размещать свои объявления в одном месте.

Здесь вы можете попробовать наши приложения:

  • Доска объявлений Angular;
  • Доска объявлений React.

Если вам необходим весь исходный код, то вы можете взять его из GitHub-репозиториев:

Вы, скорее всего, заметите, что мы использовали TypeScript для создания React-приложения. Преимущества строгой типизации в TypeScript очевидны. А во второй версии TypeScript была добавлена улучшенная обработка импорта, механизм async/await и операторы Spread/Rest, за счет данных обновлений TypeScript оставляет далеко позади такие технологии как: Babel/ES7/Flow.

Также давайте добавим Apollo-клиент для наших приложений, потому что мы хотим использовать  GraphQL. Я ничего не имею против REST, но спустя десяток лет или около того, он просто устареет.

Инициализация и Маршрутизация

Для начала, давайте взглянем на точки входа обоих приложений.

Angular

const appRoutes: Routes = [
{ path: 'home', component: HomeComponent },
{ path: 'posts', component: PostsComponent },
{ path: 'form', component: FormComponent },
{ path: '', redirectTo: '/home', pathMatch: 'full' }
]

@NgModule({
declarations: [
AppComponent,
PostsComponent,
HomeComponent,
FormComponent,
],
imports: [
BrowserModule,
RouterModule.forRoot(appRoutes),
ApolloModule.forRoot(provideClient),
FormsModule,
ReactiveFormsModule,
HttpModule,
BrowserAnimationsModule,
MdInputModule, MdSelectModule, MdButtonModule, MdCardModule, MdIconModule
],
providers: [
AppService
],
bootstrap: [AppComponent]
})

@Injectable()
export class AppService {
username = 'Mr. User'
}

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

React

const appStore = AppStore.getInstance()
const routerStore = RouterStore.getInstance()

const rootStores = {
appStore,
routerStore
}

ReactDOM.render(
<Provider {...rootStores} >
<Router history={routerStore.history} >
<App>
<Switch>
<Route exact path='/home' component={Home as any} />
<Route exact path='/posts' component={Posts as any} />
<Route exact path='/form' component={Form as any} />
<Redirect from='/' to='/home' />
</Switch>
</App>
</Router>
</Provider >,
document.getElementById('root')
)

Компонент <Provider/>, используется для внедрения зависимостей в MobX. Это позволяет сохранять контекст(React-свойство) в хранилищах, и в дальнейшем React-компоненты могут его извлекать для своих нужд. И да, React-контекст может (с неким сомнением) быть использован безопасно.

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

export class AppStore {
static instance: AppStore
static getInstance() {
return AppStore.instance || (AppStore.instance = new AppStore())
}
@observable username = 'Mr. User'
}

Маршрутизация в Angular является встроенным решением и может быть использована где угодно, не только компонентами. Для достижения аналогичного результата в React, мы будем использовать пакет mobx-react-router и встроим routerStore.

Итого: Процесс инициализирования приложений не вызвал никаких затруднений. React имеет преимущество в виде использования простого импорта вместо модулей, однако, как мы увидим далее, эти модули могут быть крайне удобными. Создание синглтонов вручную немного неудобно. Что касается синтаксиса объявления маршрутизации, JSON против JSX – это просто вопрос предпочтений.

Ссылки и Императивная навигация

Итак, есть два случая для того чтобы задействовать переключение маршрута. Декларативный, в котором используются <a href...> элементы, и императивный, который напрямую обращается к API маршрутизации (и, следовательно, к местоположению).

Angular

<h1> Shoutboard Application </h1>
<nav>
<a routerLink="/home" routerLinkActive="active">Home</a>
<a routerLink="/posts" routerLinkActive="active">Posts</a>
</nav>
<router-outlet></router-outlet>

Маршрутизатор в Angular автоматически определяет, которая из routerLink является активной, и помещает её в соответствующий для неё routerLinkActive класс, поэтому их можно стилизировать.

Маршрутизатор использует специальный элемент <router-outlet> для рендера того, что есть по данному пути (например, по yandex.ru будет отображена главная страница Яндекс). Также, возможно, наличие множества таких элементов, они будут нужны, по мере того как далеко мы будем углубляться в подкомпоненты нашего приложения.

@Injectable()
export class FormService {
constructor(private router: Router) { }
goBack() {
this.router.navigate(['/posts'])
}
}

Модуль, отвечающий за маршрутизатор, может быть внедрен в любой сервис (полу-волшебным образом за счет TypeScript), private декларация затем сохраняет его в экземпляре без необходимости явного присвоения. Для переключения URL используем метод navigate.

React

import * as style from './app.css'
// …
<h1>Shoutboard Application</h1>
<div>
<NavLink to='/home' activeClassName={style.active}>Home</NavLink>
<NavLink to='/posts' activeClassName={style.active}>Posts</NavLink>
</div>
<div>
{this.props.children}
</div>

Маршрутизатор в React также может помещать активные ссылки в соответствующий класс activeClassName.

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

Как уже было показано выше, маршрутизатор в React использует внутри элемента <App> элемент <Switch>. Поскольку элемент <Switch> просто обертывает и монтирует текущий маршрут то это означает, что под-маршруты текущего маршрута (обернутого) будут доступны следующим образом:  this.props.children. Что ж такой подход также вполне пригоден.

export class FormStore {
routerStore: RouterStore
constructor() {
this.routerStore = RouterStore.getInstance()
}
goBack = () => {
this.routerStore.history.push('/posts')
}
}

Используемый ранее пакет mobx-router-store позволяет легко организовывать навигацию и что-либо встраивать.

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

Внедрение зависимостей

Такой подход уже давно зарекомендовал себя как отличный способ отделения уровня данных от уровня представления. Основной целью использования DI(внедрения зависимостей) является составление компонентов слоев данных (model / store / service), при этом необходимо соблюдать жизненный цикл визуальных компонентов. Соблюдение этих условий позволит создавать один или несколько таких компонентов без необходимости затрагивать глобальное состояние. Кроме того, появляется возможность смешивать и сопоставлять совместимые между собой уровни данных и визуализации.

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

Angular

@Injectable()
export class HomeService {
message = 'Welcome to home page'
counter = 0
increment() {
this.counter++
}
}

Итак, любой класс можно сделать @injectable и сделать доступными для компонентов всего его свойства и методы.

@Component({
selector: 'app-home',
templateUrl: './home.component.html',
providers: [
HomeService
]
})
export class HomeComponent {
constructor(
public homeService: HomeService,
public appService: AppService,
) { }
}

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

Для сравнения, AppService был зарегистрирован в app.module (см. выше) получается, что это синглтон и остается таковым для всех компонентов на протяжении всего жизненного цикла приложения. Наличие возможности управлять жизненным циклом сервисов напрямую с помощью компонентов является крайне полезным, но недооценённым решением.

DI работает за счет назначения экземпляров службы конструктору компонента, которые идентифицированы типами TypeScript. Помимо этого, ключевое слово public автоматически связывает данные параметры с this, поэтому нам больше не нужно прописывать подобные, многим надоевшие, строчки: his.homeService = homeService.

<div>
<h3>Dashboard</h3>
<md-input-container>
<input mdInput placeholder='Edit your name' [(ngModel)]='appService.username' />
</md-input-container>
<br/>
<span>Clicks since last visit: {{homeService.counter}}</span>
<button (click)='homeService.increment()'>Click!</button>
</div>

Синтаксис шаблонов в Angular можно вполне считать элегантным. Мне нравятся подобные сокращения [()], которые работают как двунаправленная привязка данных, но на самом деле – это просто атрибут binding + event. При каждом уходе с /home сервис homeService.counter будет производить обновление, поскольку жизненный цикл наших сервисов предписывает это, однако сервис appService.username остается доступным откуда-угодно.

React

import { observable } from 'mobx'

export class HomeStore {
@observable counter = 0
increment = () => {
this.counter++
}
}

При помощи MobX нам необходимо добавить @observable класс-декоратор для любого свойства, которое мы хотим сделать наблюдаемыми.

@observer
export class Home extends React.Component<any, any> {

homeStore: HomeStore
componentWillMount() {
this.homeStore = new HomeStore()
}

render() {
return <Provider homeStore={this.homeStore}>
<HomeComponent />
</Provider>
}
}

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

interface HomeComponentProps {
appStore?: AppStore,
homeStore?: HomeStore
}

@inject('appStore', 'homeStore')
@observer
export class HomeComponent extends React.Component<HomeComponentProps, any> {
render() {
const { homeStore, appStore } = this.props
return <div>
<h3>Dashboard</h3>
<Input
type='text'
label='Edit your name'
name='username'
value={appStore.username}
onChange={appStore.onUsernameChange}
/>
<span>Clicks since last visit: {homeStore.counter}</span>
<button onClick={homeStore.increment}>Click!</button>
</div>
}
}

HomeComponent использует класс-декоратор @observer для того, чтобы отслеживать все изменения в свойствах @observable.

С точки зрения внутренней структуры механизма, то она крайне интересна. Итак, давайте кратко рассмотрим её. Класс-декоратор @observable заменяет свойство объекта на геттер и сеттер, которые дают возможность перехватывать обращения. При вызове функции рендера дополнительного компонента @observer идет обращение к данным геттерам, а они в свою очередь ссылаются на тот компонент, который к ним обратился.

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

Очень простой и крайне эффективный механизм. Более подробное объяснение здесь.

Класс-декоратор @inject используется для внедрения экземпляров хранилища внутрь свойств компонента  HomeComponent. На данном этапе оба хранилища имеют разный жизненный цикл. Жизненный цикл хранилища appStore аналогичен жизни приложения, но жизненный цикл хранилища homeStore каждый раз обновляется при переходе на “/home”.

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

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

Обработанные свойства

React

Теперь давайте начнем с React, решение здесь будет простым.

import { observable, computed, action } from 'mobx'

export class HomeStore {
import { observable, computed, action } from 'mobx'

export class HomeStore {
@observable counter = 0
increment = () => {
this.counter++
}
@computed get counterMessage() {
console.log('recompute counterMessage!')
return `${this.counter} ${this.counter === 1 ? 'click' : 'clicks'} since last visit`
}
}

Итак, мы имеем обработанное свойство, которое привязывается к counter и  возвращает корректно образованное сообщение. Результат counterMessage кэшируется и повторно обрабатывается только тогда, когда counter изменяется.

<Input
type='text'
label='Edit your name'
name='username'
value={appStore.username}
onChange={appStore.onUsernameChange}
/>
<span>{homeStore.counterMessage}</span>
<button onClick={homeStore.increment}>Click!</button>

Затем мы ссылаемся на данное свойство (и метод increment) из шаблона JSX. Поле ввода управляется привязкой к значению и позволяет методу из хранилища appStore обрабатывать событие пользователя.

Angular

Для достижения такого же эффекта в Angular нам необходимо быть более изобретательными.

import { Injectable } from '@angular/core'
import { BehaviorSubject } from 'rxjs/BehaviorSubject'

@Injectable()
export class HomeService {
message = 'Welcome to home page'
counterSubject = new BehaviorSubject(0)
// Computed property can serve as basis for further computed properties
counterMessage = new BehaviorSubject('')
constructor() {
// Manually subscribe to each subject that couterMessage depends on
this.counterSubject.subscribe(this.recomputeCounterMessage)
}

// Needs to have bound this
private recomputeCounterMessage = (x) => {
console.log('recompute counterMessage!')
this.counterMessage.next(`${x} ${x === 1 ? 'click' : 'clicks'} since last visit`)
}

increment() {
this.counterSubject.next(this.counterSubject.getValue() + 1)
}
}

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

Конечно же, RxJS может сделать куда больше чем это, но это уже тема для абсолютно другой статьи. Недостатком является то, что такое тривиальное использование RxJS только для обработанных свойств – это усложнение данного React примера, и вам придется вручную управлять подписками (как приведено ниже в конструкторе).

<md-input-container>
<input mdInput placeholder='Edit your name' [(ngModel)]='appService.username' />
</md-input-container>
<span>{{homeService.counterMessage | async}}</span>
<button (click)='homeService.increment()'>Click!</button>

Обратите внимание на то, как мы можем ссылаться на объект RxJS, используя | async канал. Это выглядит впечатляюще, намного короче, что позволяет более удобным способом проводить регистрацию элементов в ваших компонентах. Компонент imput управляется с помощью директивы [(ngModel)]. Несмотря на то, что выглядит это немного странно, на самом деле очень элегантно. Просто синтаксических сахар для привязки данных к значению appService.username, а также для автоматического присвоения значения из пользовательского ввода.

Итого: Обработанные свойства легче реализовывать с помощью React/MobX, чем с помощью          Angular/RxJS, но за счет RxJS можно реализовать некоторые функции FRP (functional reactive programming – функциональное реактивное программирование), которые позже могут быть оценены по достоинству.

Шаблоны и CSS

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

Angular

@Component({
selector: 'app-posts',
templateUrl: './posts.component.html',
styleUrls: ['./posts.component.css'],
providers: [
PostsService
]
})

export class PostsComponent implements OnInit {
constructor(
public postsService: PostsService,
public appService: AppService
) { }

ngOnInit() {
this.postsService.initializePosts()
}
}

Этот компонент просто связывается вместе  HTML, CSS и встроенные сервисы, а также, при инициализации вызывает, функцию для загрузки объявлений из API. Сервис AppService – это синглтон, который определен в одном из модулей приложения, где PostsService является промежуточным, в котором располагаются свежие экземпляры компонентов. CSS, на который ссылается этот компонент, имеет привязку к этому компоненту. Этот факт означает, что контекст не может влиять ни на что кроме самого компонента.

<a routerLink="/form" class="float-right">
<button md-fab>
<md-icon>add</md-icon>
</button>
</a>
<h3>Hello {{appService.username}}</h3>
<md-card *ngFor="let post of postsService.posts">
<md-card-title>{{post.title}}</md-card-title>
<md-card-subtitle>{{post.name}}</md-card-subtitle>
<md-card-content>
<p>
{{post.message}}
</p>
</md-card-content>
</md-card>

В HTML-шаблоне мы практически всегда ссылаемся на компоненты из Angular Material.

Для обеспечения к ним доступа, необходимо включить их в импорт модуля app.module (см. выше). Директива *ngFor необходима для повторного использования компонента md-card у каждого объявления.

Локальные CSS:

.mat-card {
margin-bottom: 1rem;
}

Приведенный выше, локальное CSS-свойство просто управляет  одним из классов, которые имеются у компонента md-card.

Глобальные CSS:

.float-right {
float: right;
}

Данный класс определен в глобальном файле style.css для того, чтобы он был доступен всем компонентам. На него можно ссылаться стандартным способом, class = "float-right".

Скомпилированный CSS:

.float-right {
float: right;
}
.mat-card[_ngcontent-c1] {
margin-bottom: 1rem;
}

В скомпилированном CSS мы можем просматривать к какой области действия и к какому компоненту, отвечающему за рендеринг, относится тот или иной локальный CSS-стиль. Для этого необходимо использовать селектор атрибутов [_ngcontent-c1]. Каждый компонент Angular, который отвечает за рендеринг, имеет сгенерированный класс для подобных целей.

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

React

import * as style from './posts.css'
import * as appStyle from '../app.css'

@observer
export class Posts extends React.Component<any, any> {

postsStore: PostsStore
componentWillMount() {
this.postsStore = new PostsStore()
this.postsStore.initializePosts()
}

render() {
return <Provider postsStore={this.postsStore}>
<PostsComponent />
</Provider>
}
}

Опять же таки, в React нам необходимо использовать подход Provider, чтобы создать «промежуточную» зависимость для PostsStore. Мы также импортируем стили такие, как style и appStyle, для того, чтобы иметь возможность использовать классы из этих CSS-классов в JSX.

interface PostsComponentProps {
appStore?: AppStore,
postsStore?: PostsStore
}

@inject('appStore', 'postsStore')
@observer
export class PostsComponent extends React.Component<PostsComponentProps, any> {
render() {
const { postsStore, appStore } = this.props
return <div>
<NavLink to='form'>
<Button icon='add' floating accent className={appStyle.floatRight} />
</NavLink>
<h3>Hello {appStore.username}</h3>
{postsStore.posts.map(post =>
<Card key={post.id} className={style.messageCard}>
<CardTitle
title={post.title}
subtitle={post.name}
/>
<CardText>{post.message}</CardText>
</Card>
)}
</div>
}
}

Разумеется, JSX больше взаимосвязан с JavaScript, чем HTML-шаблоны в Angular, хорошо это или плохо уже зависит от ваших личных предпочтений. Вместо директивы *ngFor мы используем конструктор map для перебора объявлений.

Теперь Angular может показаться фреймворком, который очень сильно навязывает использование TypeScript, однако на самом деле именно в JSX TypeScript раскрывает себя на максимум. Совместно с добавлением CSS-модулей (импортированных выше), это действительно превращает ваш шаблонный код в истинный завершенный код. Каждая отдельная деталь проверяется по типу. Компоненты, атрибуты, даже CSS-классы (appStyle.floatRight и style.messageCard, см. ниже). И, конечно же, довольно простая структура JSX компенсируется более удобным разделением на компоненты и фрагменты, чем в шаблонном подходе Angular.

Локальный CSS:

.messageCard {
margin-bottom: 1rem;
}

Глобальный CSS:

.floatRight {
float: right;
}

Скомпилированный CSS:

.floatRight__qItBM {
float: right;
}

.messageCard__1Dt_9 {
margin-bottom: 1rem;
}

Как вы можете видеть, загрузчик CSS-модулей добавляет каждому классу случайный постфикс, что обеспечивает уникальность каждого класса. Такой способ – это самое простое решение для избежание ненужных конфликтов. Затем классы могут ссылаться на импортированные объекты webpack. Одним из возможных недостатков является отсутствие возможности создавать CSS-класс для дальнейшей работы с ним, как это было в примере с Angular. С другой стороны, это скорее плюс, так как заставляет вас правильно инкапсулировать стили.

Итого: Свои предпочтения я отдаю JSX, так как он мне нравится немного больше, чем Angular-шаблоны. Причинами являются поддержка автодополнение кода и проверка типов. Это просто киллер-фича. На данный момент, Angular имеет AOT компилятор, в котором также есть несколько фич, так например, автодополнение кода также работает, но не со всем содержимым, поэтому нельзя сказать, что это аналогично тому, что есть JSX/TypeScript.

GraphQL – Загрузка данных

Итак, мы приняли решение использовать GraphQL для хранения даных для нашего приложения. Одним из наиболее простых способов создания back-end, основанного на GraphQL, это использование некоторого BaaS, например, Graphcool. Так, вот что мы сделали. Изначально, просто определяем модели и атрибуты, и ваше GRUD-приложение уже выглядит неплохо.

Стандартный код

Так как некоторый, связанный с GraphQL, код на 100% одинаков для обеих реализаций, то давайте не будет повторять его дважды:

const PostsQuery = gql`
query PostsQuery {
allPosts(orderBy: createdAt_DESC, first: 5)
{
id,
name,
title,
message
}
}
`

GraphQL – это язык запросов, целью которого является обеспечение более широкого функционала по сравнению с классическим RESTful endpoints. Давайте рассмотрим конкретный запрос:

  • Начнем с того, что PostsQuery – это просто название этого запроса, для того, чтобы потом было проще ссылаться на него, название может быть любое;
  • allPosts – это наиболее важная часть, которая ссылается на функцию, отвечающая за запрос всех записей с помощью модели «Post». Данное название было создано средством Graphcool;
  • orderBy и first – это параметры функции allPosts. createdAt – один из атрибутов модели «Post». first: 5 означает, что результатом запроса будет 5;
  • id,name, title, и message – это атрибуты модели «Post», которые мы хотим включить в наш результат. Остальные атрибуты будут отфильтрованы.

Как вы уже видели – это очень мощный инструмент. Посетите данную страницу, чтобы подробнее познакомиться с запросами в GraphQL.

interface Post {
id: string
name: string
title: string
message: string
}

interface PostsQueryResult {
allPosts: Array<Post>
}

И да, внимательные пользователи TypeScript, мы создали интерфейс для результатов GraphQL.

Angular

@Injectable()
export class PostsService {
posts = []

constructor(private apollo: Apollo) { }

initializePosts() {
this.apollo.query<PostsQueryResult>({
query: PostsQuery,
fetchPolicy: 'network-only'
}).subscribe(({ data }) => {
this.posts = data.allPosts
})
}
}

Запрос в GraphQL является RxJS наблюдаемым, поэтому и мы подписываемся на это. Все это работает, немного как promise, однако не совсем, нам просто не везет с использованием async/await. Конечно, все еще остается метод toPromise, но, как мне кажется, это не самый удачный способ для Angular. В данном случае, мы устанавливаем fetchPolicy: 'network-only', так как не хотим кэшировать данные, но хотим обновлять их.

React

export class PostsStore {
appStore: AppStore

@observable posts: Array<Post> = []

constructor() {
this.appStore = AppStore.getInstance()
}

async initializePosts() {
const result = await this.appStore.apolloClient.query<PostsQueryResult>({
query: PostsQuery,
fetchPolicy: 'network-only'
})
this.posts = result.data.allPosts
}
}

React-версия практически идентична, но так как apolloClient использует promises, мы можем использовать преимущество синтаксиса async/await. Однако в React существуют другие подходу, когда просто идет «склеивание» запросов GraphQL с компонентами более высокого порядка, но для меня это выглядит, как смешивание вместе уровень данных и уровень представления, что уже немного перебор.

Итого: Идея RxJS подписки против механизма async/await выявила, что это реально аналогичные подходы.

GraphQL – Сохранение данных

Стандартный код

Снова общий, связанный с GraphQL,  код для обоих подходов:

const AddPostMutation = gql`
mutation AddPostMutation($name: String!, $title: String!, $message: String!) {
createPost(
name: $name,
title: $title,
message: $message
) {
id
}
}
`

Целью изменения данных является создание или обновление записей. По этой причине, будет выгодно объявить некоторые переменные как изменяемые, так как это обеспечит способ передачи данных. Итак, у нас есть переменные name, title и message, которые имеют тип String, которые нам необходимо заполнять каждый раз, при вызове этих изменяемых данных. Функция createPost, опять же таки определяется средством Graphcool. Мы определяем для ключевых слов модели “Post”, что им будет присваиваться значение наших изменяемых переменных (name, title и message), а также мы хотим, чтобы ответом приходил только id новосозданного объявления.

Angular

@Injectable()
export class FormService {
constructor(
private apollo: Apollo,
private router: Router,
private appService: AppService
) { }

addPost(value) {
this.apollo.mutate({
mutation: AddPostMutation,
variables: {
name: this.appService.username,
title: value.title,
message: value.message
}
}).subscribe(({ data }) => {
this.router.navigate(['/posts'])
}, (error) => {
console.log('there was an error sending the query', error)
})
}

}

При вызове, apollo.mutate нам необходимо указать мутацию, которую мы вызываем, и необходимые переменные. Мы получаем результат в калбэке subscribe, и используем встроенный router для возврата к списку объявлений.

React

export class FormStore {
constructor() {
this.appStore = AppStore.getInstance()
this.routerStore = RouterStore.getInstance()
this.postFormState = new PostFormState()
}

submit = async () => {
await this.postFormState.form.validate()
if (this.postFormState.form.error) return
const result = await this.appStore.apolloClient.mutate(
{
mutation: AddPostMutation,
variables: {
name: this.appStore.username,
title: this.postFormState.title.value,
message: this.postFormState.message.value
}
}
)
this.goBack()
}

goBack = () => {
this.routerStore.history.push('/posts')
}
}

Данный пример очень похож на то, что было выше, с разницей в более “ручное” внедрение зависимостей, и использование механизма async/await.

Итого: Опять же, здесь не так много различий. Использование subscribe вместо async/await – это единственное отличие.

Формы

Мы хотим достичь следующих целей при создании форм нашего приложения:

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

React

export const check = (validator, message, options) =>
(value) => (!validator(value, options) && message)

export const checkRequired = (msg: string) => check(nonEmpty, msg)

export class PostFormState {
title = new FieldState('').validators(
checkRequired('Title is required'),
check(isLength, 'Title must be at least 4 characters long.', { min: 4 }),
check(isLength, 'Title cannot be more than 24 characters long.', { max: 24 }),
)
message = new FieldState('').validators(
checkRequired('Message cannot be blank.'),
check(isLength, 'Message is too short, minimum is 50 characters.', { min: 50 }),
check(isLength, 'Message is too long, maximum is 1000 characters.', { max: 1000 }),
)
form = new FormState({
title: this.title,
message: this.message
})
}

Итак, библиотека formstate работает следующим образом: Для каждого поля на вашей форме необходимо определить FieldState. Вводимые параметры являются исходными значениями. Свойство validators вызывает функцию, которая возвращает “false”, если значение прошло валидацию, или сообщение о том, что значение не прошло валидацию. Совместно с дополнительным функциями-помощниками check и checkRequired, это может выглядеть по-хорошему декларативно.

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

@inject('appStore', 'formStore')
@observer
export class FormComponent extends React.Component<FormComponentProps, any> {
render() {
const { appStore, formStore } = this.props
const { postFormState } = formStore
return <div>
<h2> Create a new post </h2>
<h3> You are now posting as {appStore.username} </h3>
<Input
type='text'
label='Title'
name='title'
error={postFormState.title.error}
value={postFormState.title.value}
onChange={postFormState.title.onChange}
/>
<Input
type='text'
multiline={true}
rows={3}
label='Message'
name='message'
error={postFormState.message.error}
value={postFormState.message.value}
onChange={postFormState.message.onChange}
/>

Экземпляр FormState предоставляет доступ к свойствам value, onChange, и error, которые в дальнейшем могут быть использованы любым front-end компонентом.

<Button
label='Cancel'
onClick={formStore.goBack}
raised
accent
/> &nbsp;
<Button
label='Submit'
onClick={formStore.submit}
raised
disabled={postFormState.form.hasError}
primary
/>

</div>

}
}

При form.hasError = true кнопка становится неактивной. Кнопка «Отправить» отправляет форму на GraphQL мутацию, которую мы описали ранее.

Angular

В Angular мы собираемся использовать FormService и FormBuilder, которые являются частью пакета @angular/forms.

@Component({
selector: 'app-form',
templateUrl: './form.component.html',
providers: [
FormService
]
})
export class FormComponent {
postForm: FormGroup
validationMessages = {
'title': {
'required': 'Title is required.',
'minlength': 'Title must be at least 4 characters long.',
'maxlength': 'Title cannot be more than 24 characters long.'
},
'message': {
'required': 'Message cannot be blank.',
'minlength': 'Message is too short, minimum is 50 characters',
'maxlength': 'Message is too long, maximum is 1000 characters'
}
}

Во-первых, давайте определим валидационное письмо.

constructor(
private router: Router,
private formService: FormService,
public appService: AppService,
private fb: FormBuilder,
) {
this.createForm()
}

createForm() {
this.postForm = this.fb.group({
title: ['',
[Validators.required,
Validators.minLength(4),
Validators.maxLength(24)]
],
message: ['',
[Validators.required,
Validators.minLength(50),
Validators.maxLength(1000)]
],
})
}

Используя FormBuilder, можно очень легко создать структуру формы, даже еще более лаконично, чем в примере React.

get validationErrors() {
const errors = {}
Object.keys(this.postForm.controls).forEach(key => {
errors[key] = ''
const control = this.postForm.controls[key]
if (control && !control.valid) {
const messages = this.validationMessages[key]
Object.keys(control.errors).forEach(error => {
errors[key] += messages[error] + ' '
})
}
})
return errors
}

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

onSubmit({ value, valid }) {
if (!valid) {
return
}
this.formService.addPost(value)
}

onCancel() {
this.router.navigate(['/posts'])
}
}

Снова, если форма прошла валидацию, то данные могут быть отправлены на GraphQL мутацию.

<h2> Create a new post </h2>
<h3> You are now posting as {{appService.username}} </h3>
<form [formGroup]="postForm" (ngSubmit)="onSubmit(postForm)" novalidate>
<md-input-container>
<input mdInput placeholder="Title" formControlName="title">
<md-error>{{validationErrors['title']}}</md-error>
</md-input-container>
<br>
<br>
<md-input-container>
<textarea mdInput placeholder="Message" formControlName="message"></textarea>
<md-error>{{validationErrors['message']}}</md-error>
</md-input-container>
<br>
<br>
<button md-raised-button (click)="onCancel()" color="warn">Cancel</button>
<button
md-raised-button
type="submit"
color="primary"
[disabled]="postForm.dirty && !postForm.valid">Submit</button>
<br>
<br>
</form>

Наиболее важным является то, что необходимо ссылаться на форму formGroup, которую мы создали с помощью FormBuilder, которому присвоено значение  [formGroup]="postForm". Поля вне формы привязаны к ней с помощью свойства formControlName. И вновь, мы деактивируем кнопку «Submit», если форма не проходит валидацию. Также необходимо добавить «грязную» проверку, так как здесь возможна ситуация при которой «негрязные» формы не будут проходить валидацию. Также мы хотим, чтобы начальное состояние кнопки было “Доступна”.

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

Размер приложения

Ох, еще один пункт. При создании приложения размер JS-файлов был уменьшен, с использованием настроек по умолчанию из генераторов приложения: в частности, Tree Shaking для React и AOT компилятор в Angular.

Получаются следующие размеры приложений:

  • Angular: 1200 KB
  • React: 300 KB

Ну, я нисколько не удивлен. Angular всегда имел больший размер.

При использовании архивирования (gzip) размер уменьшается до 275 kb и 127 kb соответственно.

Просто помните, что изначально это поставщики библиотек. Количество реального кода в этих приложениях минимально по сравнению с реальными приложениями, так как наши приложения были созданы исключительно с целью сравнения двух библиотек. Поэтому отношение будет скорее всего 1 к 2, чем 1 к 4. Также помните, что при подключении множества сторонних библиотек в React, размер вашего приложения тоже будет увеличиваться в разы.

Гибкость библиотек или Надежность фреймворков

Ну что ж, кажется, мы не можем дать определенного ответа (опять!), что лучше подходит для веб-разработки React или Angular.

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

Если вы любите готовые стеки технологий, сильное внедрение зависимостей и в планах у вас есть использование RxJS, то выбирайте Angular.

Но если вам близко продумывание и создание своего стека технологий, вам нравится прямота JSX, и вы предпочитаете более простые обрабатываемые свойства, то ваш выбор – это React в связке с MobX.

Еще раз повторюсь, вы можете посмотреть весь исходный код приложения из этой статьи здесь и здесь.

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

 

Сперва выберите свою парадигму программирования

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

React/Redux с их неизменяемой/однонаправленной парадигмой – это абсолютно другой «зверь».

Не обманывайтесь небольшими размерами библиотеки Redux. Библиотека может быть маленькой, но тем не менее это фреймворк. Наиболее удачный опыт работы с Redux направлен на использование redux-совместимых библиотек таких, как:  Redux Saga для асинхронного кода и выборки данных,  Redux Form для управления формами,  Reselect для запоминания селекторов (обработанные значения Redux) и среди всех остальных Recompose, которая предназначена для более точного управления жизненным циклом. Также наблюдается небольшой сдвиг в Redux-сообществе с  Immutable.js на  Ramda или  lodash/fp, которые работают с «чистыми» JavaScript объектами вместо их конвертирования.

Хорошим примером современного Redux является всем хорошо известный React Boilerplate. Это внушительный стек разработки, но если вы посмотрите на него, то поймете, что он реально очень и очень сильно отличается от всего, что мы видели в данной статье.

По моему мнению, Angular оценивается немного несправедливо большей частью JavaScript-сообщества. Большинство тех, кто выражает свое недовольство, не до конца понимает огромного сдвига, который произошел между старым AngularJS и сегодняшним Angular-ом. Поэтому я считаю, что Angular – это простой и продуктивный фреймворк, который легко «захватит» весь IT-мир в течении 1-2 лет.

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

Что же касается MobX, то здесь применяется аналогичная оценка. Эта библиотека действительно замечательна, но также недооценена.

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

Изменяемые данные и привязка данных или неизменяемые данные и однонаправленные данные, вот это, кажется, настоящая задача.

Ссылка на оригинальную статью
Перевод: Александр Давыдов

Другие статьи по теме

Изучаем Angular: видео, подкасты и полезные ссылки

25 понятных туториалов для изучения React Native

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ