Има безброй статии, които спорят дали React или Angular е по-добрият избор за уеб разработка. Имаме ли нужда от още един?
Причината да напиша тази статия е, че никой от на статии публикувано вече - макар и да съдържат страхотни прозрения - отиват достатъчно задълбочено, за да може практичен разработчик отпред да реши кой може да отговаря на техните нужди.
В тази статия ще научите как Angular и React имат за цел да решат подобни проблеми отпред, макар и с много различни философии, и дали изборът на единия или другия е просто въпрос на лични предпочитания. За да ги сравним, ще изградим едно и също приложение два пъти, веднъж с Angular и след това отново с React.
Преди две години написах статия за Реагирайте екосистемата . Наред с други точки, в статията се твърди, че Angular е станал жертва на „смърт чрез предварително обявяване“. Тогава изборът между Angular и почти всичко останало беше лесен за всеки, който не искаше проектът им да работи на остаряла рамка. Angular 1 беше остарял, а Angular 2 дори не беше наличен в алфа версия.
Погледнато назад, страховете бяха повече или по-малко оправдани. Angular 2 се промени драстично и дори премина през основно пренаписване точно преди финалната версия.
Две години по-късно имаме Angular 4 с обещание за относителна стабилност от тук нататък.
Сега какво?
Някои хора казват, че сравняването на React и Angular е като сравняване на ябълки с портокали. Докато едната е библиотека, която се занимава с изгледи, другата е пълноценна рамка.
Разбира се, повечето React разработчици ще добави няколко библиотеки към React, за да го превърне в цялостна рамка. След това отново полученият работен поток на този стек често все още е много различен от Angular, така че сравнимостта все още е ограничена.
Най-голямата разлика се крие в държавното управление. Angular идва с пакетно свързване на данни, докато React днес обикновено се допълва от Redux, за да осигури еднопосочен поток от данни и да работи с неизменяеми данни. Това са противоположни подходи сами по себе си и в момента текат безброй дискусии дали променливото / свързването на данни е по-добро или по-лошо от неизменяемото / еднопосочно.
Тъй като React е по-лесно хакнат, за целите на това сравнение реших да създам React настройка, която отразява разумно Angular, за да позволи сравняване на кодови фрагменти едно до друго.
Някои ъглови функции, които се открояват, но не са в React по подразбиране, са:
Особеност | Ъглова опаковка | Библиотека на React |
---|---|---|
Свързване на данни, инжектиране на зависимост (DI) | @ ъглова / сърцевина | MobX |
Изчислени свойства | rxjs | MobX |
Базирано на компоненти маршрутизиране | @ ъглова / рутер | React Router v4 |
Компоненти на материалния дизайн | @ ъглова / материал | React Toolbox |
CSS обхванат до компоненти | @ ъглова / сърцевина | CSS модули |
Валидиране на формуляри | @ ъглови / форми | FormState |
Генератор на проекти | @ ъглова / клип | React Scripts TS |
Свързването на данни е безспорно по-лесно от еднопосочния подход. Разбира се, би било възможно да отидете в напълно противоположна посока и да използвате Redux или mobx-state-дърво с React и ngrx с ъглова. Но това би било тема за друга публикация.
Докато производителността е загрижена, обикновените гетери в Angular просто не подлежат на извикване, когато се извикват при всяка рендиране. Възможно е да се използва Предмет на поведение от RsJS , което върши работата.
С React е възможно да се използва @computed от MobX, който постига същата цел, с може би малко по-хубав API.
Инжектирането на зависимост е доста противоречиво, защото противоречи на настоящата парадигма на React за функционално програмиране и неизменност. Както се оказва, някакъв вид инжектиране на зависимости е почти задължително в обвързваща среда на данни, тъй като помага при отделянето (и по този начин подиграването и тестването), когато няма отделна архитектура на слоя данни.
Още едно предимство на DI (поддържа се в Angular) е възможността да има различни цикли на живот в различни магазини. Повечето актуални парадигми на React използват някакъв вид глобално състояние на приложението, което се съпоставя с различни компоненти, но от моя опит е твърде лесно да се въведат грешки при почистване на глобалното състояние на демонтирания компонент.
Наличието на магазин, който се създава при монтиране на компоненти (и е безпроблемно достъпен за децата на този компонент), изглежда наистина полезно и често пренебрегвано понятие.
Извън кутията в Angular, но доста лесно възпроизводим и с MobX.
Базираното на компоненти маршрутизиране позволява на компонентите да управляват собствените си под-маршрути, вместо да имат една голяма глобална конфигурация на рутера. Този подход най-накрая стигна до react-router
във версия 4.
Винаги е хубаво да започнете с някои компоненти от по-високо ниво, а дизайнът на материалите се превърна в нещо като общоприет избор по подразбиране, дори в проекти извън Google.
Умишлено съм избрал React Toolbox над обикновено препоръчваните Потребителски интерфейс на материала , тъй като потребителският интерфейс на Material сериозно се самопризна проблеми с производителността с техния вграден CSS подход, който те планират да решат в следващата версия.
Освен това, PostCSS / cssnext използван в React Toolbox така или иначе започва да замества Sass / LESS.
CSS класовете са нещо като глобални променливи. Има многобройни подходи за организиране на CSS за предотвратяване на конфликти (включително ДОБРЕ ), но има ясна настояща тенденция в използването на библиотеки, които помагат за обработката на CSS, за да се предотвратят тези конфликти, без да е необходим преден край разработчик да разработи сложни системи за именуване на CSS.
Проверките на формулярите са нетривиална и много широко използвана функция. Добре е тези да са покрити от библиотека, за да се предотврати повторението на кода и грешките.
Наличието на CLI генератор за проект е малко по-удобно, отколкото да се налага да клонирате бойлерни плочи от GitHub.
Така че ще създадем същото приложение в React и Angular. Нищо грандиозно, просто Shoutboard, който позволява на всеки да публикува съобщения на обща страница.
Можете да изпробвате приложенията тук:
Ако искате да имате целия изходен код, можете да го получите от GitHub:
Ще забележите, че сме използвали TypeScript и за приложението React. Предимствата на проверката на типа в TypeScript са очевидни. И сега, тъй като най-накрая в TypeScript 2 пристигна по-доброто обработване на вноса, async / await и rest rest, той оставя Babel / ES7 / Поток в праха.
Също така, нека добавим Клиент Аполо и към двете, защото искаме да използваме 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( , document.getElementById('root') )
Компонентът се използва за инжектиране на зависимост в MobX. Той запазва магазините в контекст, така че React компонентите да могат да ги инжектират по-късно. Да, контекстът на React може (може да се използва) безопасно .
Версията на React е малко по-къса, защото няма декларации на модули - обикновено просто импортирате и е готова за употреба. Понякога този вид твърда зависимост е нежелан (тестване), така че за глобалните магазини за единични продукти трябваше да използвам този десетилетия GoF модел :
export class AppStore { static instance: AppStore static getInstance() @observable username = 'Mr. User' }
Angular’s Router е инжекционен, така че може да се използва от всяко място, не само от компоненти. За да постигнем същото при реакция, използваме mobx-response-router пакет и инжектирайте routerStore
.
Резюме: Стартирането на двете приложения е доста лесно. React има предимство, което е по-просто, използвайки само импортиране вместо модули, но, както ще видим по-късно, тези модули могат да бъдат доста удобни. Правенето на единични снимки ръчно е малко неприятно. Що се отнася до синтаксиса на декларацията за маршрутизиране, JSON срещу JSX е само въпрос на предпочитание.
Така че има два случая за превключване на маршрут. Декларация, като се използва
елементи и императив, извикващ директно API за маршрутизация (и по този начин местоположение).
Ъглова
Home Posts {this.props.children}
React Router също може да зададе класа на активната връзка с activeClassName
.
Тук не можем да предоставим директно името на класа, защото е направено уникално от компилатора на CSS модули и трябва да използваме style
помощник. Повече за това по-късно.
Както се вижда по-горе, React Router използва елемента вътре в anelement. Тъй като елементът просто обвива и монтира текущия маршрут, това означава, че под-маршрутите на текущия компонент са само 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
линии вече.
Dashboard
Clicks since last visit: {{homeService.counter}} Click!
Синтаксис на Angular’s template, може би доста елегантен. Харесва ми [()]
пряк път, който работи като двупосочно свързване на данни, но под капака всъщност е свързване на атрибут + събитие. Както повелява жизненият цикъл на нашите услуги, 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 { homeStore: HomeStore componentWillMount() { this.homeStore = new HomeStore() } render() { return } }
За да управляваме правилно жизнения цикъл, трябва да свършим малко повече работа, отколкото в Angular пример. Ние обвиваме HomeComponent
вътре в Provider
, което получава нов екземпляр на HomeStore
на всяко монтиране.
interface HomeComponentProps { appStore?: AppStore, homeStore?: HomeStore } @inject('appStore', 'homeStore') @observer export class HomeComponent extends React.Component { render() { const { homeStore, appStore } = this.props return Dashboard
Clicks since last visit: {homeStore.counter} Click! } }
HomeComponent
използва @observer
декоратор за прослушване на промените в @observable
Имоти.
Механизмът под капака на това е доста интересен, така че нека да го разгледаме накратко тук. @observable
декоратор заменя свойство в обект с getter и setter, което му позволява да прихваща повиквания. Когато функцията за рендиране на @observer
повикан компонент се извиква, тези свойства getters се извикват и те запазват препратка към компонента, който ги е извикал.
След това, когато се извика setter и стойността се промени, се извикват функциите за рендиране на компоненти, използвали свойството на последното изобразяване. Сега данните за това кои свойства се използват къде се актуализират и целият цикъл може да започне отначало.
Много прост механизъм и доста ефективен. По-задълбочено обяснение тук .
@inject
декоратор се използва за инжектиране appStore
и homeStore
екземпляри в реквизита на HomeComponent
На този етап всеки от тези магазини има различен жизнен цикъл. appStore
е същото по време на живота на приложението, но homeStore
е прясно създаден при всяка навигация до маршрута “/ home”.
Предимството на това е, че не е необходимо да почиствате свойствата ръчно, както е в случая, когато всички магазини са глобални, което е мъка, ако маршрутът е някаква страница с подробности, която всеки път съдържа напълно различни данни.
Резюме: Тъй като управлението на жизнения цикъл на доставчика е присъща характеристика на Angular’s DI, разбира се, е по-лесно да го постигнете там. Версията на 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
промени.
{homeStore.counterMessage} Click!
След това препращаме към свойството (и 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 за току-що изчислени свойства е малко по-многословна от примера за реакция и трябва да управлявате абонаментите ръчно (като тук в конструктора).
{homeService.counterMessage } Click!
Обърнете внимание как можем да препращаме темата RxJS с | async
тръба. Това е хубаво докосване, много по-кратко от необходимостта да се абонирате за вашите компоненти. input
компонентът се задвижва от [(ngModel)]
директива. Въпреки че изглежда странно, всъщност е доста елегантно. Просто синтактична захар за свързване на данни на стойност с appService.username
и автоматично присвояване на стойност от въведеното от потребителя събитие.
Резюме: Изчислените свойства са по-лесни за внедряване в React / MobX, отколкото в Angular / RxJS, но RxJS може да предостави някои по-полезни FRP функции, които може да бъдат оценени по-късно.
За да покажем как шаблонират стекове един срещу друг, нека използваме компонента Публикации, който показва списък с публикации.
Ъглова
@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, към който се прави препратка от този компонент, се обхваща към този компонент, което означава, че съдържанието не може да повлияе на нищо извън компонента.
add Hello {{appService.username}}
{{post.title}} {{post.name}} {{post.message}}
В 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 { postsStore: PostsStore componentWillMount() { this.postsStore = new PostsStore() this.postsStore.initializePosts() } render() { return } }
В React отново трябва да използваме Provider
подход да се направи PostsStore
зависимост „преходна“. Също така импортираме CSS стилове, посочени като style
и appStyle
, за да можете да използвате класовете от тези CSS файлове в JSX.
interface PostsComponentProps { appStore?: AppStore, postsStore?: PostsStore } @inject('appStore', 'postsStore') @observer export class PostsComponent extends React.Component { render() { const { postsStore, appStore } = this.props return Hello {appStore.username}
{postsStore.posts.map(post => {post.message} )} } }
Естествено, JSX се чувства много повече JavaScript-y от HTML шаблоните на Angular, което може да бъде добро или лошо нещо в зависимост от вашия вкус. Вместо *ngFor
директива, ние използваме map
конструкция за итерация над публикации.
Сега Angular може да е рамката, която най-много рекламира TypeScript, но всъщност това е JSX, където TypeScript наистина блести. С добавянето на CSS модули (импортирани по-горе), той наистина превръща кодирането на вашия шаблон в zen за завършване на кода. Всяко едно нещо е типово проверено. Компоненти, атрибути, дори 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 back-end е използването на някои 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 }
Да, като добри граждани на TypeScript, ние създаваме интерфейси за резултати от GraphQL.
Ъглова
@Injectable() export class PostsService { posts = [] constructor(private apollo: Apollo) { } initializePosts() { this.apollo.query({ query: PostsQuery, fetchPolicy: 'network-only' }).subscribe(({ data }) => { this.posts = data.allPosts }) } }
Запитването на GraphQL е наблюдавано от RxJS и ние се абонираме за него. Работи малко като обещание, но не съвсем, така че нямаме късмет, използвайки async/await
. Разбира се, все още има да обещае , но изглежда така или иначе не е ъгловият начин. Задаваме fetchPolicy: 'network-only'
защото в този случай не искаме да кешираме данните, но извличаме всеки път.
Реагирайте
export class PostsStore { appStore: AppStore @observable posts: Array = [] constructor() { this.appStore = AppStore.getInstance() } async initializePosts() { const result = await this.appStore.apolloClient.query({ query: PostsQuery, fetchPolicy: 'network-only' }) this.posts = result.data.allPosts } }
Версията на React е почти идентична, но като apolloClient
тук използва обещания, можем да се възползваме от async/await
синтаксис. В React има и други подходи, които просто „залепват“ графичните заявки компоненти от по-висок ред , но ми се струваше, че смесването на данните и слоя за представяне е твърде малко.
Резюме: Идеите на RxJS абонамент срещу async / await са наистина еднакви.
Общ код
Отново някои кодове, свързани с 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
callback и използвайте инжектирания 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
.
Резюме: Отново тук няма голяма разлика. Абонирайте се срещу 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 }) }
Така че форма състояние библиотеката работи по следния начин: За всяко поле на вашия формуляр дефинирате FieldState
. Предаденият параметър е началната стойност. validators
свойството приема функция, която връща „false“, когато стойността е валидна, и съобщение за проверка, когато стойността не е валидна. С check
и checkRequired
помощни функции, всичко това може да изглежда добре декларативно.
За да има валидиране за целия формуляр, е полезно също да обгърнете тези полета с FormState
инстанция, която след това осигурява общата валидност.
@inject('appStore', 'formStore') @observer export class FormComponent extends React.Component { render() { const { appStore, formStore } = this.props const { postFormState } = formStore return Create a new post
You are now posting as {appStore.username}
FormState
екземпляр предоставя value
, onChange
и error
свойства, които могат лесно да се използват с всички предни компоненти.
} }
Когато form.hasError
е true
, ние държим бутона деактивиран. Бутонът за изпращане изпраща формуляра към мутацията GraphQL, представена по-рано.
Ъглова
В 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.
Create a new post
You are now posting as {{appService.username}}
{{validationErrors['title']}}
{{validationErrors['message']}}
Cancel Submit
Най-важното е да се позовем на groupGroup, който сме създали с FormBuilder, който е [formGroup]='postForm'
възлагане. Полетата във формуляра са обвързани с модела на формуляра чрез formControlName
Имот. Отново деактивираме бутона „Изпращане“, когато формулярът не е валиден. Също така трябва да добавим мръсната проверка, тъй като тук нецапаната форма все още може да бъде невалидна. Искаме обаче първоначалното състояние на бутона да бъде „активирано“.
Резюме: Този подход към формулярите в React и Angular е доста различен както при проверка, така и при фронтове на шаблони. Ъгловият подход включва малко повече „магия“, вместо просто свързване, но, от друга страна, е по-пълен и задълбочен.
О, още нещо. Производствените минимизирани размери на JS пакети, с настройки по подразбиране от генераторите на приложения: по-специално Tree Shaking в React и AOT компилация в Angular.
Е, тук няма много изненада. Ъгловата винаги е била по-обемната.
Когато използвате gzip, размерите намаляват съответно до 275kb и 127kb.
Само имайте предвид, че това са основно всички библиотеки на доставчици. Количеството на действителния код на приложението е минимално при сравнение, което не е така в реалното приложение. Там съотношението вероятно би било по-скоро 1: 2, отколкото 1: 4. Също така, когато започнете да включвате много библиотеки на трети страни с React, размерът на пакета също има тенденция да расте доста бързо.
Така че изглежда не сме успели (отново!) Да намерим ясен отговор дали Angular или React е по-добър за уеб разработка.
Оказва се, че работните потоци за разработка в React и Angular могат да бъдат много сходни, в зависимост от това с кои библиотеки сме избрали да използваме React. Тогава това е въпрос основно на лични предпочитания.
Ако харесвате готови стекове, мощно инжектиране на зависимост и планирате да използвате някои екстри от RxJS, изберете Angular.
Ако искате да се занимавате и да изграждате сами стека си, харесвате праволинейността на JSX и предпочитате по-прости изчислими свойства, изберете React / MobX.
Отново можете да получите пълния изходен код на приложението от тази статия тук и тук .
Или, ако предпочитате по-големи, примери от RealWorld:
Програмирането с React / MobX всъщност е по-подобно на Angular, отколкото с React / Redux. Има някои забележими разлики в шаблоните и управлението на зависимостите, но те имат едни и същи променлив / свързване на данни парадигма.
React / Redux със своя неизменен / еднопосочен парадигмата е съвсем различен звяр.
Не се заблуждавайте от малкия отпечатък на библиотеката Redux. Може да е малко, но въпреки това е рамка. Повечето от най-добрите практики на Redux днес са фокусирани върху използването на библиотеки, съвместими с редукс, като Redux Saga за извличане на асинхронен код и данни, Редукс форма за управление на формуляри, Преизберете за запаметени селектори (изчислени стойности на Redux). и Прекомпозирайте наред с други за по-фино управление на жизнения цикъл. Също така, има промяна в общността Redux от Immutable.js да се Рамда или lodash / fp , които работят с обикновени JS обекти, вместо да ги преобразуват.
Хубав пример за съвременния Redux е добре познатият React Boilerplate . Това е страхотен стек за разработка, но ако го погледнете, той наистина е много, много различен от всичко, което сме виждали в тази публикация досега.
Чувствам, че Angular получава малко несправедливо отношение от по-гласовитата част на JavaScript общността. Много хора, които изразяват недоволство от него, вероятно не оценяват огромната промяна, която се случи между старата AngularJS и днешната Angular. Според мен това е много чиста и продуктивна рамка, която ще завладее света, ако се появи 1-2 години по-рано.
И все пак, Angular се затвърждава стабилно, особено в корпоративния свят, с големи екипи и нужди от стандартизация и дългосрочна подкрепа. Или казано по друг начин, Angular е начина, по който инженерите на Google смятат, че трябва да се направи уеб разработка, ако това все още е нещо.
Що се отнася до MobX, се прилага подобна оценка. Наистина страхотно, но недооценено.
В заключение: преди да изберете между React и Angular, първо изберете вашата парадигма за програмиране.
променлив / свързване на данни или неизменен / еднопосочен , това ... изглежда е истинският проблем.
React е JavaScript библиотека за изграждане на потребителски интерфейси. Той се занимава с изгледите и ви позволява да изберете останалата част от вашата архитектура отпред. Обаче около него се е развила силна библиотечна екосистема, която ви позволява да изградите цялостна рамка около React, като добавите няколко библиотеки към нея.
Дефинирайте всички стойности, които служат като основа за изчислено свойство като BehaviourSubject (достъпно чрез RxJS), и се абонирайте ръчно за всеки предмет, от който зависи свойството.