Angular или React: что лучше для веб-разработки?
Опубликовано: 2022-03-11Существует множество статей, в которых обсуждаются, что лучше для веб-разработки — React или Angular. Нужен ли нам еще один?
Причина, по которой я написал эту статью, заключается в том, что ни одна из уже опубликованных статей, хотя они и содержат отличные идеи, не содержит достаточно подробной информации, чтобы практичный разработчик интерфейса мог решить, какая из них может удовлетворить его потребности.
В этой статье вы узнаете, как Angular и React нацелены на решение схожих задач интерфейса, хотя и с очень разными философиями, и является ли выбор того или другого просто вопросом личных предпочтений. Чтобы сравнить их, мы создадим одно и то же приложение дважды, один раз с помощью Angular, а затем снова с помощью React.
Несвоевременное объявление Angular
Два года назад я написал статью об экосистеме React. Среди прочего, в статье утверждалось, что Angular стал жертвой «смерти по предварительному объявлению». В то время выбор между Angular и почти всем остальным был простым для тех, кто не хотел, чтобы их проект работал на устаревшем фреймворке. Angular 1 устарел, а Angular 2 даже не был доступен в альфа-версии.
Оглядываясь назад, можно сказать, что опасения более или менее оправдались. Angular 2 сильно изменился и даже претерпел значительные изменения непосредственно перед финальным релизом.
Два года спустя у нас есть Angular 4 с обещанием относительной стабильности.
Что теперь?
Angular vs. React: сравнение яблок и апельсинов
Некоторые люди говорят, что сравнивать React и Angular — это все равно, что сравнивать яблоки с апельсинами. В то время как одна библиотека работает с представлениями, другая представляет собой полноценный фреймворк.
Конечно, большинство разработчиков React добавят в React несколько библиотек, чтобы превратить его в полноценный фреймворк. Опять же, результирующий рабочий процесс этого стека часто сильно отличается от Angular, поэтому сопоставимость все еще ограничена.
Самая большая разница заключается в государственном управлении. Angular поставляется со встроенной привязкой данных, тогда как сегодня React обычно дополняется Redux для обеспечения однонаправленного потока данных и работы с неизменяемыми данными. Это противоположные подходы сами по себе, и сейчас ведутся бесчисленные дискуссии о том, лучше или хуже привязка mutable/data, чем неизменяемая/однонаправленная.
Поле для игры на равных
Поскольку React, как известно, легче взломать, я решил для целей этого сравнения создать настройку React, которая достаточно близко отражает Angular, чтобы можно было сравнивать фрагменты кода бок о бок.
Некоторые функции Angular, которые выделяются, но по умолчанию отсутствуют в React:
Характерная черта | Угловой пакет | Библиотека реакции |
---|---|---|
Привязка данных, внедрение зависимостей (DI) | @угловой/ядро | МобХ |
Вычисляемые свойства | rxjs | МобХ |
Компонентная маршрутизация | @угловой/маршрутизатор | Реагирующий маршрутизатор v4 |
Компоненты материального дизайна | @угловой/материал | Набор инструментов для реагирования |
CSS ограничен компонентами | @угловой/ядро | CSS-модули |
Проверка формы | @угловые/формы | FormState |
Генератор проектов | @угловой/кли | Реагировать скрипты TS |
Привязка данных
С привязкой данных, возможно, проще начать, чем с однонаправленного подхода. Конечно, можно было бы пойти совершенно противоположным путем и использовать Redux или mobx-state-tree с React, а ngrx с Angular. Но это будет тема для другого поста.
Вычисленные свойства
Что касается производительности, то о простых геттерах в Angular просто не может быть и речи, поскольку они вызываются при каждом рендеринге. Можно использовать BehaviorSubject из RsJS, который выполняет эту работу.
С React можно использовать @computed из MobX, который достигает той же цели, возможно, с немного более удобным API.
Внедрение зависимости
Внедрение зависимостей вызывает споры, потому что оно идет вразрез с текущей парадигмой функционального программирования и неизменности React. Как оказалось, некоторые виды внедрения зависимостей практически необходимы в средах привязки данных, поскольку они помогают с разделением (и, следовательно, с имитацией и тестированием) там, где нет отдельной архитектуры уровня данных.
Еще одно преимущество DI (поддерживается в Angular) — возможность иметь разные жизненные циклы разных хранилищ. В большинстве современных парадигм React используется какое-то глобальное состояние приложения, которое сопоставляется с различными компонентами, но, исходя из моего опыта, слишком легко внести ошибки при очистке глобального состояния при размонтировании компонента.
Наличие хранилища, которое создается при монтировании компонента (и беспрепятственно доступно для дочерних элементов этого компонента), кажется действительно полезной и часто упускаемой из виду концепцией.
Из коробки в Angular, но довольно легко воспроизводится и в MobX.
Маршрутизация
Маршрутизация на основе компонентов позволяет компонентам управлять своими собственными подмаршрутами вместо одной большой глобальной конфигурации маршрутизатора. Этот подход наконец-то добрался до react-router
в версии 4.
Материальный дизайн
Всегда приятно начинать с компонентов более высокого уровня, а материальный дизайн стал чем-то вроде общепринятого выбора по умолчанию, даже в проектах, не принадлежащих Google.
Я намеренно выбрал React Toolbox, а не обычно рекомендуемый пользовательский интерфейс Material, поскольку пользовательский интерфейс Material имеет серьезные проблемы с производительностью из-за их подхода на основе встроенного CSS, которые они планируют решить в следующей версии.
Кроме того, PostCSS/cssnext, используемый в React Toolbox, все равно начинает заменять Sass/LESS.
Ограниченный CSS
Классы CSS — это что-то вроде глобальных переменных. Существует множество подходов к организации CSS для предотвращения конфликтов (включая БЭМ), но в настоящее время наблюдается четкая тенденция использования библиотек, которые помогают обрабатывать CSS для предотвращения конфликтов без необходимости привлекать разработчика внешнего интерфейса для разработки сложных систем именования CSS.
Проверка формы
Проверка формы — нетривиальная и очень широко используемая функция. Хорошо иметь те, которые покрыты библиотекой, чтобы предотвратить повторение кода и ошибки.
Генератор проектов
Иметь генератор командной строки для проекта немного удобнее, чем клонировать шаблоны из GitHub.
Одно и то же приложение, созданное дважды
Итак, мы собираемся создать одно и то же приложение в React и Angular. Ничего особенного, просто Shoutboard, который позволяет любому размещать сообщения на общей странице.
Вы можете попробовать приложения здесь:
- Угловой
- Реагировать
Если вы хотите получить весь исходный код, вы можете получить его на GitHub:
- Угловой исходный код Shoutboard
- Исходный код Shoutboard React
Вы заметите, что мы также использовали TypeScript для приложения React. Преимущества проверки типов в TypeScript очевидны. И теперь, когда в TypeScript 2 наконец-то появилась улучшенная обработка импорта, async/await и rest spread, он оставляет Babel/ES7/Flow в пыли.
Кроме того, давайте добавим в оба клиента Apollo, потому что мы хотим использовать GraphQL. Я имею в виду, что REST великолепен, но примерно через десять лет он устаревает.
Начальная загрузка и маршрутизация
Во-первых, давайте посмотрим на точки входа обоих приложений.
Угловой
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' }
По сути, все компоненты, которые мы хотим использовать в приложении, должны перейти к объявлениям. Все сторонние библиотеки для импорта и все глобальные хранилища для поставщиков. Дочерние компоненты имеют доступ ко всему этому, с возможностью добавлять больше локальных вещей.
Реагировать
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 немного короче, потому что в ней нет объявлений модулей — обычно вы просто импортируете ее, и она готова к использованию. Иногда такая жесткая зависимость нежелательна (тестирование), поэтому для глобальных одноэлементных хранилищ мне пришлось использовать этот старый шаблон GoF:
export class AppStore { static instance: AppStore static getInstance() { return AppStore.instance || (AppStore.instance = new AppStore()) } @observable username = 'Mr. User' }
Маршрутизатор Angular является инжектируемым, поэтому его можно использовать где угодно, а не только в компонентах. Чтобы добиться того же в реакции, мы используем пакет mobx-react-router и routerStore
.
Резюме: Начальная загрузка обоих приложений довольно проста. Преимущество React в том, что он более прост и использует только импорт вместо модулей, но, как мы увидим позже, эти модули могут быть весьма удобными. Делать синглтоны вручную немного неприятно. Что касается синтаксиса объявления маршрутизации, JSON или JSX — это просто вопрос предпочтений.
Ссылки и императивная навигация
Таким образом, есть два случая переключения маршрута. Декларативный, использующий элементы <a href...>
, и императивный, напрямую вызывающий API маршрутизации (и, следовательно, местоположения).
Угловой
<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 Router автоматически определяет, какой routerLink
активен, и помещает в него соответствующий класс routerLinkActive
, чтобы его можно было стилизовать.
Маршрутизатор использует специальный элемент <router-outlet>
для отображения любого текущего пути. Возможно иметь много <router-outlet>
s, поскольку мы углубляемся в подкомпоненты приложения.
@Injectable() export class FormService { constructor(private router: Router) { } goBack() { this.router.navigate(['/posts']) } }
Модуль маршрутизатора можно внедрить в любую службу (наполовину магическим образом из-за его типа TypeScript), затем объявление private
сохранит его в экземпляре без необходимости явного назначения. Используйте метод navigate
для переключения URL-адресов.
Реагировать
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 Router также может установить класс активной ссылки с помощью activeClassName
.
Здесь мы не можем указать имя класса напрямую, потому что оно было сделано уникальным компилятором модулей CSS, и нам нужно использовать помощник style
. Подробнее об этом позже.
Как показано выше, React Router использует элемент <Switch>
внутри элемента <App>
. Поскольку элемент <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 Router имеет более простую компоновку.
Внедрение зависимости
Уже доказано, что полезно отделить уровень данных от уровня представления. С помощью DI мы пытаемся добиться того, чтобы компоненты слоев данных (здесь называемые моделью/хранилищем/сервисом) следовали жизненному циклу визуальных компонентов и, таким образом, позволяли создавать один или несколько экземпляров таких компонентов без необходимости касаться глобальных компонентов. состояние. Кроме того, должна быть возможность смешивать и сопоставлять совместимые данные и слои визуализации.
Примеры в этой статье очень просты, поэтому все элементы DI могут показаться излишними, но они пригодятся по мере роста приложения.
Угловой
@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
, так что нам больше не нужно писать эти скучные this.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, возможно, довольно элегантный. Мне нравится сочетание клавиш [()]
, которое работает как двусторонняя привязка данных, но на самом деле это привязка атрибута + событие. Как диктует жизненный цикл наших сервисов, homeService.counter
будет сбрасываться каждый раз, когда мы уходим из /home
, но appService.username
остается и доступен отовсюду.
Реагировать
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
используется для внедрения appStore
и homeStore
в свойства HomeComponent
. На данный момент у каждого из этих магазинов свой жизненный цикл. appStore
не меняется в течение жизни приложения, но homeStore
создается при каждом переходе на маршрут «/home».
Преимущество этого в том, что нет необходимости очищать свойства вручную, как это происходит, когда все хранилища являются глобальными, что является проблемой, если маршрут представляет собой какую-то страницу «деталей», которая каждый раз содержит совершенно разные данные.
Резюме: Поскольку управление жизненным циклом провайдера является неотъемлемой функцией DI Angular, конечно, добиться этого там проще. Версия 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, нам нужно быть немного изобретательнее.
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 для только что вычисленных свойств немного более многословно, чем пример с реакцией, и вам нужно управлять подписками вручную (как здесь, в конструкторе).
<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
| async
канал. Это приятное прикосновение, намного короче, чем необходимость подписываться на ваши компоненты. Компонент input
управляется директивой [(ngModel)]
. Несмотря на то, что он выглядит странно, он на самом деле довольно элегантен. Просто синтаксический сахар для привязки данных значения к appService.username
и автоматического назначения значения из события пользовательского ввода.
Резюме: вычисляемые свойства проще реализовать в React/MobX, чем в Angular/RxJS, но RxJS может предоставить некоторые более полезные функции FRP, которые можно оценить позже.

Шаблоны и CSS
Чтобы показать, как шаблоны сочетаются друг с другом, давайте воспользуемся компонентом Posts, который отображает список сообщений.
Угловой
@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 имеет сгенерированный класс, подобный этому, для целей определения области действия CSS.
Преимущество этого механизма в том, что мы можем нормально ссылаться на классы, а область видимости обрабатывается «изнутри».
Реагировать
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
«временной». Мы также импортируем стили CSS, называемые 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-модулей добавляет к каждому классу CSS случайный постфикс, что гарантирует уникальность. Прямой способ избежать конфликтов. Затем на классы ссылаются через импортированные объекты webpack. Одним из возможных недостатков этого может быть то, что вы не можете просто создать CSS с классом и дополнить его, как мы сделали в примере с Angular. С другой стороны, это может быть на самом деле хорошо, потому что заставляет вас правильно инкапсулировать стили.
Резюме: лично мне JSX нравится немного больше, чем шаблоны Angular, особенно из-за поддержки автодополнения кода и проверки типов. Это действительно убийственная функция. У Angular теперь есть компилятор AOT, который также может определить некоторые вещи, завершение кода также работает примерно для половины вещей, но он далеко не так совершенен, как JSX/TypeScript.
GraphQL — загрузка данных
Поэтому мы решили использовать GraphQL для хранения данных для этого приложения. Один из самых простых способов создать серверную часть GraphQL — использовать BaaS, например Graphcool. Вот что мы сделали. По сути, вы просто определяете модели и атрибуты, и ваш CRUD готов к работе.
Общий код
Поскольку некоторый код, связанный с GraphQL, на 100 % одинаков для обеих реализаций, давайте не будем повторять его дважды:
const PostsQuery = gql` query PostsQuery { allPosts(orderBy: createdAt_DESC, first: 5) { id, name, title, message } } `
GraphQL — это язык запросов, предназначенный для предоставления более богатого набора функций по сравнению с классическими конечными точками RESTful. Давайте разберем этот конкретный запрос.
-
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.
Угловой
@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, и мы подписываемся на него. Это немного похоже на обещание, но не совсем, поэтому нам не повезло с использованием async/await
. Конечно, есть еще toPromise, но в любом случае это не похоже на Angular. Мы устанавливаем fetchPolicy: 'network-only'
потому что в этом случае мы не хотим кэшировать данные, а каждый раз обновлять их.
Реагировать
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
использует промисы, мы можем воспользоваться преимуществами синтаксиса 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
будут иметь значения из переменных мутации, а также что мы хотим, чтобы в ответ отправлялся только id
вновь созданного сообщения.
Угловой
@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
для возврата к списку сообщений.
Реагировать
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
.
Резюме: Опять же, здесь нет большой разницы. подписка против асинхронного/ожидания - это в основном все, что отличается.
Формы
Мы хотим достичь следующих целей с помощью форм в этом приложении:
- Привязка данных полей к модели
- Сообщения проверки для каждого поля, несколько правил
- Поддержка проверки правильности всей формы
Реагировать
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
, которые можно легко использовать с любыми внешними компонентами.
<Button label='Cancel' onClick={formStore.goBack} raised accent /> <Button label='Submit' onClick={formStore.submit} raised disabled={postFormState.form.hasError} primary /> </div> } }
When form.hasError
is true
, we keep the button disabled. The submit button sends the form to the GraphQL mutation presented earlier.
Angular
In Angular, we are going to use FormService
and FormBuilder
, which are parts of the @angular/forms
package.
@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' } }
First, let's define the validation messages.
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)] ], }) }
Using FormBuilder
, it's quite easy to create the form structure, even more succintly than in the React example.
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 }
To get bindable validation messages to the right place, we need to do some processing. This code is taken from the official documentation, with a few small changes. Basically, in FormService, the fields keep reference just to active errors, identified by validator name, so we need to manually pair the required messages to affected fields. This is not entirely a drawback; it, for example, lends itself more easily to internationalization.
onSubmit({ value, valid }) { if (!valid) { return } this.formService.addPost(value) } onCancel() { this.router.navigate(['/posts']) } }
Again, when the form is valid, data can be sent to GraphQL mutation.
<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>
The most important thing is to reference the formGroup we have created with the FormBuilder, which is the [formGroup]="postForm"
assignment. Fields inside the form are bound to the form model through the formControlName
property. Again, we disable the “Submit” button when the form is not valid. We also need to add the dirty check, because here, the non-dirty form can still be invalid. We want the initial state of the button to be “enabled” though.
Summary: This approach to forms in React and Angular is quite different on both validation and template fronts. The Angular approach involves a bit more “magic” instead of straightforward binding, but, on the other hand, is more complete and thorough.
Bundle size
Oh, one more thing. The production minified JS bundle sizes, with default settings from the application generators: notably Tree Shaking in React and AOT compilation in Angular.
- Angular: 1200 KB
- React: 300 KB
Well, not much surprise here. Angular has always been the bulkier one.
When using gzip, the sizes go down to 275kb and 127kb respectively.
Just keep in mind, this is basically all vendor libraries. The amount of actual application code is minimal by comparison, which is not the case in a real-world application. There, the ratio would be probably more like 1:2 than 1:4. Also, when you start including a lot of third-party libraries with React, the bundle size also tends to grow rather quickly.
Flexibility of Libraries vs. Robustness of Framework
So it seems that we have not been able (again!) to turn up a clear answer on whether Angular or React is better for web development.
It turns out that the development workflows in React and Angular can be very similar, depending on which libraries we chose to use React with. Then it's a mainly a matter of personal preference.
If you like ready-made stacks, powerful dependency injection and plan to use some RxJS goodies, chose Angular.
If you like to tinker and build your stack yourself, you like the straightforwardness of JSX and prefer simpler computable properties, choose React/MobX.
Again, you can get the complete source code of the application from this article here and here.
Or, if you prefer bigger, RealWorld examples:
- RealWorld Angular 4+
- RealWorld React/MobX
Choose Your Programming Paradigm First
Programming with React/MobX is actually more similar to Angular than with React/Redux. There are some notable differences in templates and dependency management, but they have the same mutable/data binding paradigm.
React/Redux with its immutable/unidirectional paradigm is a completely different beast.
Don't be fooled by the small footprint of the Redux library. It might be tiny, but it's a framework nevertheless. Most of the Redux best practices today are focused on using redux-compatible libraries, like Redux Saga for async code and data fetching, Redux Form for form management, Reselect for memorized selectors (Redux's computed values). and Recompose among others for more fine-grained lifecycle management. Also, there's a shift in Redux community from Immutable.js to Ramda or lodash/fp, which work with plain JS objects instead of converting them.
A nice example of modern Redux is the well-known React Boilerplate. It's a formidable development stack, but if you take a look at it, it is really very, very different from anything we have seen in this post so far.
I feel that Angular is getting a bit of unfair treatment from the more vocal part of JavaScript community. Many people who express dissatisfaction with it probably do not appreciate the immense shift that happened between the old AngularJS and today's Angular. In my opinion, it's a very clean and productive framework that would take the world by storm had it appeared 1-2 years earlier.
Still, Angular is gaining a solid foothold, especially in the corporate world, with big teams and needs for standardization and long-term support. Or to put it in another way, Angular is how Google engineers think web development should be done, if that still amounts to anything.
As for MobX, similar assessment applies. Really great, but underappreciated.
In conclusion: before choosing between React and Angular, choose your programming paradigm first.
mutable/data-binding or immutable/unidirectional , that… seems to be the real issue.