Angular vs. React: care este mai bun pentru dezvoltarea web?

Publicat: 2022-03-11

Există nenumărate articole care dezbat dacă React sau Angular este alegerea mai bună pentru dezvoltarea web. Mai avem nevoie de altul?

Motivul pentru care am scris acest articol este că niciunul dintre articolele publicate deja – deși conțin informații excelente – nu este suficient de aprofundat pentru ca un dezvoltator practic front-end să decidă care dintre ele se potrivește nevoilor lor.

În acest articol, veți afla cum Angular și React urmăresc ambele să rezolve probleme front-end similare, deși cu filozofii foarte diferite, și dacă alegerea uneia sau a celeilalte este doar o chestiune de preferință personală. Pentru a le compara, vom construi aceeași aplicație de două ori, o dată cu Angular și apoi din nou cu React.

Anunțul prematur al lui Angular

Acum doi ani, am scris un articol despre Ecosistemul React. Printre alte puncte, articolul susținea că Angular a devenit victima „morii prin pre-anunț”. Pe atunci, alegerea între Angular și aproape orice altceva era una ușoară pentru oricine nu dorea ca proiectul lor să ruleze pe un cadru învechit. Angular 1 era învechit, iar Angular 2 nici măcar nu era disponibil în versiunea alfa.

Privind retrospectiv, temerile erau mai mult sau mai puțin justificate. Angular 2 s-a schimbat dramatic și chiar a trecut printr-o rescriere majoră chiar înainte de lansarea finală.

Doi ani mai târziu, avem Angular 4 cu o promisiune de relativa stabilitate de aici înainte.

Acum ce?

Angular vs. React: compararea mere și portocale

Unii oameni spun că a compara React și Angular este ca și cum ai compara merele cu portocale. În timp ce una este o bibliotecă care se ocupă de vizualizări, cealaltă este un cadru cu drepturi depline.

Desigur, majoritatea dezvoltatorilor React vor adăuga câteva biblioteci la React pentru a-l transforma într-un cadru complet. Apoi, din nou, fluxul de lucru rezultat al acestei stive este adesea foarte diferit de Angular, astfel încât comparabilitatea este încă limitată.

Cea mai mare diferență constă în managementul statului. Angular vine cu legare de date, în timp ce React astăzi este de obicei îmbunătățit de Redux pentru a oferi un flux de date unidirecțional și a lucra cu date imuabile. Acestea sunt abordări opuse în sine și nenumărate discuții au loc acum dacă legarea mutabilă/de date este mai bună sau mai proastă decât imuabilă/unidirecțională.

Un teren de joc egal

Deoarece React este celebru mai ușor de piratat, am decis, în scopul acestei comparații, să construiesc o configurație React care oglindește Angular destul de aproape pentru a permite compararea una lângă alta a fragmentelor de cod.

Anumite caracteristici Angular care ies în evidență, dar care nu sunt în React în mod implicit sunt:

Caracteristică Pachet unghiular Biblioteca React
Legarea datelor, injecția de dependență (DI) @unghiular/nucleu MobX
Proprietăți calculate rxjs MobX
Rutare bazată pe componente @unghiular/ruter React Router v4
Componente de proiectare materiale @unghiular/material Caseta de instrumente React
CSS încadrat în componente @unghiular/nucleu module CSS
Validari formulare @unghiular/forme FormState
Generator de proiecte @angular/cli React Scripts TS

Legarea datelor

Legarea datelor este probabil mai ușor de început decât abordarea unidirecțională. Desigur, ar fi posibil să mergeți în direcția complet opusă și să folosiți Redux sau mobx-state-tree cu React și ngrx cu Angular. Dar acesta ar fi un subiect pentru o altă postare.

Proprietăți calculate

În ceea ce privește performanța, geterii simpli din Angular sunt pur și simplu excluși, deoarece sunt chemați la fiecare randare. Este posibil să utilizați BehaviorSubject de la RsJS, care face treaba.

Cu React, este posibil să utilizați @computed din MobX, care atinge același obiectiv, cu un API puțin mai frumos.

Injecție de dependență

Injecția de dependență este oarecum controversată, deoarece merge împotriva paradigmei actuale React de programare funcțională și imuabilitate. După cum se dovedește, un fel de injecție de dependență este aproape indispensabilă în mediile de legare a datelor, deoarece ajută la decuplare (și, prin urmare, batjocură și testare) acolo unde nu există o arhitectură separată a stratului de date.

Un alt avantaj al DI (acceptat în Angular) este capacitatea de a avea cicluri de viață diferite ale diferitelor magazine. Majoritatea paradigmelor React actuale folosesc un fel de stare globală a aplicației care se mapează la diferite componente, dar din experiența mea, este prea ușor să introduci erori atunci când curățați starea globală la demontarea componentelor.

A avea un magazin care este creat pe suport de componentă (și să fie perfect disponibil pentru copiii acestei componente) pare a fi un concept cu adevărat util și adesea trecut cu vederea.

Ieșit din cutie în Angular, dar destul de ușor de reprodus și cu MobX.

Dirijare

Rutarea bazată pe componente permite componentelor să-și gestioneze propriile sub-rute în loc să aibă o configurație globală mare de router. Această abordare a ajuns în cele din urmă la react-router în versiunea 4.

Design material

Este întotdeauna plăcut să începeți cu unele componente de nivel superior, iar designul materialului a devenit ceva ca o alegere implicită acceptată universal, chiar și în proiectele non-Google.

Am ales în mod deliberat React Toolbox față de Material UI recomandat în mod obișnuit, deoarece Material UI are probleme serioase de performanță auto-mărturisite cu abordarea lor inline-CSS, pe care intenționează să le rezolve în următoarea versiune.

În plus, PostCSS/cssnext folosit în React Toolbox începe oricum să înlocuiască Sass/LESS.

CSS acoperit

Clasele CSS sunt ceva de genul variabilelor globale. Există numeroase abordări de organizare a CSS pentru a preveni conflictele (inclusiv BEM), dar există o tendință actuală clară în utilizarea bibliotecilor care ajută la procesarea CSS pentru a preveni aceste conflicte, fără a fi nevoie ca un dezvoltator front-end să elaboreze sisteme de denumire CSS elaborate.

Validarea formularului

Validarea formularelor este o caracteristică netrivială și foarte utilizată. Bine să le aveți pe cele acoperite de o bibliotecă pentru a preveni repetarea codului și erorile.

Generator de proiecte

A avea un generator CLI pentru un proiect este puțin mai convenabil decât a trebui să cloneze boilerplate din GitHub.

Aceeași aplicație, construită de două ori

Deci vom crea aceeași aplicație în React și Angular. Nimic spectaculos, doar un Shoutboard care permite oricui să posteze mesaje pe o pagină comună.

Puteți încerca aplicațiile aici:

  • Shoutboard Angular
  • Shoutboard React

Aplicație Shoutboard

Dacă doriți să aveți întregul cod sursă, îl puteți obține de pe GitHub:

  • Shoutboard Sursă unghiulară
  • Sursa Shoutboard React

Veți observa că am folosit TypeScript și pentru aplicația React. Avantajele verificării tipului în TypeScript sunt evidente. Și acum, pe măsură ce gestionarea mai bună a importurilor, asincron/așteptarea și răspândirea restului au ajuns în sfârșit în TypeScript 2, lasă Babel/ES7/Flow în praf.

De asemenea, să adăugăm Apollo Client la ambele pentru că vrem să folosim GraphQL. Adică, REST este grozav, dar după un deceniu și ceva, devine vechi.

Bootstrap și rutare

Mai întâi, să aruncăm o privire la punctele de intrare ale ambelor aplicații.

unghiular

 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' }

Practic, toate componentele pe care dorim să le folosim în aplicație trebuie să meargă la declarații. Toate bibliotecile terță parte la import și toate magazinele globale către furnizori. Componentele pentru copii au acces la toate acestea, cu posibilitatea de a adăuga mai multe lucruri locale.

Reacţiona

 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') )

Componenta <Provider/> este utilizată pentru injectarea dependenței în MobX. Salvează magazinele în context, astfel încât componentele React să le poată injecta mai târziu. Da, contextul React poate fi folosit (probabil) în siguranță.

Versiunea React este puțin mai scurtă, deoarece nu există declarații de modul - de obicei, doar importați și este gata de utilizare. Uneori, acest tip de dependență greu este nedorit (testare), așa că pentru magazinele globale singleton, a trebuit să folosesc acest model GoF vechi de zeci de ani:

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

Router-ul Angular este injectabil, deci poate fi folosit de oriunde, nu doar de componente. Pentru a obține același lucru în react, folosim pachetul mobx-react-router și injectăm routerStore .

Rezumat: Bootstrapping ambelor aplicații este destul de simplă. React are un avantaj fiind mai simplu, folosind doar importuri în loc de module, dar, așa cum vom vedea mai târziu, acele module pot fi destul de utile. Efectuarea manuală a singletonurilor este un pic o pacoste. În ceea ce privește sintaxa declarației de rutare, JSON vs. JSX este doar o chestiune de preferință.

Legături și navigare imperativă

Deci, există două cazuri pentru comutarea unei rute. Declarativ, folosind elemente <a href...> și imperativ, apelând direct API-ul de rutare (și, prin urmare, locație).

unghiular

 <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 detectează automat ce routerLink este activ și îi pune o clasă corespunzătoare routerLinkActive , astfel încât să poată fi stilizat.

Routerul folosește elementul special <router-outlet> pentru a reda orice cale curentă dictează. Este posibil să avem multe <router-outlet> , pe măsură ce săpăm mai adânc în sub-componentele aplicației.

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

Modulul router poate fi injectat în orice serviciu (în mod pe jumătate magic prin tipul său TypeScript), declarația private îl stochează apoi pe instanță fără a fi nevoie de o atribuire explicită. Utilizați metoda de navigate pentru a schimba adresele URL.

Reacţiona

 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 poate seta și clasa de legătură activă cu activeClassName .

Aici, nu putem furniza numele clasei în mod direct, deoarece a fost făcut unic de către compilatorul de module CSS și trebuie să folosim ajutorul de style . Mai multe despre asta mai târziu.

După cum sa văzut mai sus, React Router utilizează elementul <Switch> în interiorul unui element <App> . Deoarece elementul <Switch> doar înfășoară și montează ruta curentă, înseamnă că subrutele componentei curente sunt doar this.props.children . Deci și asta e composabil.

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

Pachetul mobx-router-store permite, de asemenea, injectare și navigare ușoară.

Rezumat: Ambele abordări ale rutare sunt destul de comparabile. Angular pare a fi mai intuitiv, în timp ce React Router are o compoziție ceva mai simplă.

Injecție de dependență

S-a dovedit deja benefică separarea stratului de date de stratul de prezentare. Ceea ce încercăm să realizăm cu DI aici este să facem ca componentele straturilor de date (numite aici model/magazin/serviciu) să urmeze ciclul de viață al componentelor vizuale și, astfel, să permitem să facem una sau mai multe instanțe ale unor astfel de componente fără a fi nevoie să atingeți global. stat. De asemenea, ar trebui să fie posibilă combinarea și potrivirea straturilor de date și vizualizare compatibile.

Exemplele din acest articol sunt foarte simple, așa că toate chestiile DI ar putea părea exagerate, dar sunt utile pe măsură ce aplicația crește.

unghiular

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

Deci orice clasă poate fi făcută @injectable , iar proprietățile și metodele sale sunt puse la dispoziție componentelor.

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

Înregistrând HomeService la providers componentei, îl punem la dispoziție exclusiv acestei componente. Nu este un singleton acum, dar fiecare instanță a componentei va primi o copie nouă, proaspătă pe suportul componentei. Aceasta înseamnă că nu există date învechite de la utilizarea anterioară.

În schimb, AppService a fost înregistrat în app.module (vezi mai sus), deci este un singleton și rămâne același pentru toate componentele, deși durata de viață a aplicației. Capacitatea de a controla ciclul de viață al serviciilor din componente este un concept foarte util, dar subapreciat.

DI funcționează prin atribuirea instanțelor de serviciu constructorului componentei, identificate prin tipurile TypeScript. În plus, cuvintele cheie public atribuie automat parametrii this , astfel încât să nu mai fie nevoie să scriem acele linii plictisitoare 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>

Sintaxa șablonului lui Angular, probabil destul de elegantă. Îmi place comanda rapidă [()] , care funcționează ca o legare de date în două direcții, dar sub capotă, este de fapt o legare de atribut + eveniment. După cum dictează ciclul de viață al serviciilor noastre, homeService.counter se va reseta de fiecare dată când navigăm departe de /home , dar appService.username rămâne și este accesibil de oriunde.

Reacţiona

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

Cu MobX, trebuie să adăugăm decoratorul @observable la orice proprietate pe care dorim să o facem observabilă.

 @observer export class Home extends React.Component<any, any> { homeStore: HomeStore componentWillMount() { this.homeStore = new HomeStore() } render() { return <Provider homeStore={this.homeStore}> <HomeComponent /> </Provider> } }

Pentru a gestiona corect ciclul de viață, trebuie să lucrăm puțin mai mult decât în ​​exemplul Angular. Împachetăm HomeComponent într-un Provider , care primește o instanță nouă de HomeStore pe fiecare montură.

 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 folosește decoratorul @observer pentru a asculta modificările în proprietățile @observable .

Mecanismul de sub capotă al acestuia este destul de interesant, așa că haideți să-l parcurgem pe scurt aici. Decoratorul @observable înlocuiește o proprietate dintr-un obiect cu getter și setter, ceea ce îi permite să intercepteze apelurile. Atunci când funcția de randare a unei componente augmentate @observer este apelată, acești aplicatori de proprietăți sunt apelați și păstrează o referință la componenta care le-a numit.

Apoi, atunci când setter este apelat și valoarea este schimbată, sunt apelate funcțiile de randare ale componentelor care au folosit proprietatea la ultima randare. Acum, datele despre proprietățile care sunt utilizate sunt actualizate și întregul ciclu poate începe de la capăt.

Un mecanism foarte simplu, și destul de performant. Explicație mai aprofundată aici.

Decoratorul @inject este folosit pentru a injecta appStore și homeStore în elementele de recuzită HomeComponent . În acest moment, fiecare dintre aceste magazine are un ciclu de viață diferit. appStore este același pe durata de viață a aplicației, dar homeStore este proaspăt creat la fiecare navigare către ruta „/home”.

Avantajul acestui lucru este că nu este necesară curățarea manuală a proprietăților, așa cum este cazul când toate magazinele sunt globale, ceea ce este o durere dacă ruta este o pagină „detaliată” care conține date complet diferite de fiecare dată.

Rezumat: În calitate de management al ciclului de viață al furnizorului într-o caracteristică inerentă a DI-ului Angular, este, desigur, mai simplu să îl realizați acolo. Versiunea React este, de asemenea, utilizabilă, dar implică mult mai mult boilerplate.

Proprietăți calculate

Reacţiona

Să începem cu React pe aceasta, are o soluție mai simplă.

 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` } }

Deci avem o proprietate calculată care se leagă de counter și returnează un mesaj pluralizat corespunzător. Rezultatul counterMessage este stocat în cache și recalculat numai atunci când counter se schimbă.

 <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>

Apoi, facem referire la proprietate (și metoda de increment ) din șablonul JSX. Câmpul de intrare este condus prin legarea la o valoare și lăsând o metodă din appStore să gestioneze evenimentul utilizatorului.

unghiular

Pentru a obține același efect în Angular, trebuie să fim puțin mai inventivi.

 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) } }

Trebuie să definim toate valorile care servesc ca bază pentru o proprietate calculată ca BehaviorSubject . Proprietatea calculată în sine este, de asemenea, un BehaviorSubject , deoarece orice proprietate calculată poate servi ca intrare pentru o altă proprietate calculată.

Desigur, RxJS poate face mult mai mult decât asta, dar acesta ar fi un subiect pentru un articol complet diferit. Dezavantajul minor este că această utilizare trivială a RxJS doar pentru proprietățile calculate este puțin mai detaliată decât exemplul de reactie și trebuie să gestionați manual abonamentele (ca aici în constructor).

 <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>

Observați cum putem face referire la subiectul RxJS cu | async conductă | async . Este o atingere plăcută, mult mai scurtă decât trebuie să vă abonați la componente. Componenta de input este condusă de directiva [(ngModel)] . În ciuda faptului că arată ciudat, este de fapt destul de elegant. Doar un zahăr sintactic pentru legarea de date a valorii la appService.username și atribuirea automată a valorii de la evenimentul de intrare de utilizator.

Rezumat: Proprietățile calculate sunt mai ușor de implementat în React/MobX decât în ​​Angular/RxJS, dar RxJS ar putea oferi câteva caracteristici FRP mai utile, care ar putea fi apreciate mai târziu.

Șabloane și CSS

Pentru a arăta cum se adună șablonul unul față de celălalt, să folosim componenta Postări care afișează o listă de postări.

unghiular

 @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() } }

Această componentă doar conectează HTML, CSS și serviciile injectate și, de asemenea, apelează funcția de încărcare a postărilor din API la inițializare. AppService este un singleton definit în modulul aplicației, în timp ce PostsService este tranzitoriu, cu o instanță nouă creată la fiecare componentă creată. CSS la care se face referire din această componentă este inclus în această componentă, ceea ce înseamnă că conținutul nu poate afecta nimic în afara componentei.

 <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>

În șablonul HTML, facem referire în principal la componente din Angular Material. Pentru a le avea disponibile, a fost necesar să le includă în importurile app.module (vezi mai sus). Directiva *ngFor este folosită pentru a repeta componenta md-card pentru fiecare postare.

CSS local:

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

CSS-ul local doar mărește una dintre clasele prezente pe componenta md-card .

CSS global:

 .float-right { float: right; }

Această clasă este definită în fișierul global style.css pentru a o face disponibilă pentru toate componentele. Poate fi referit în modul standard, class="float-right" .

CSS compilat:

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

În CSS compilat, putem vedea că CSS-ul local a fost inclus în componenta redată utilizând selectorul de atribute [_ngcontent-c1] . Fiecare componentă Angular redată are o clasă generată ca aceasta în scopuri de definire a domeniului CSS.

Avantajul acestui mecanism este că putem face referire la clase în mod normal, iar domeniul de aplicare este gestionat „sub capotă”.

Reacţiona

 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> } }

În React, din nou, trebuie să folosim abordarea Provider pentru a face dependența PostsStore „tranzitorie”. Importăm și stiluri CSS, la care se face referire ca style și appStyle , pentru a putea folosi clasele din acele fișiere CSS în 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> } }

Desigur, JSX se simte mult mai JavaScript-y decât șabloanele HTML ale Angular, ceea ce poate fi un lucru bun sau rău, în funcție de gusturile tale. În loc de directiva *ngFor , folosim construcția map pentru a repeta peste postări.

Acum, Angular ar putea fi cadrul care promovează cel mai mult TypeScript, dar de fapt este JSX unde TypeScript strălucește cu adevărat. Odată cu adăugarea modulelor CSS (importate mai sus), se transformă într-adevăr codificarea șablonului în zen pentru completarea codului. Fiecare lucru este verificat de tip. Componente, atribute, chiar și clase CSS ( appStyle.floatRight și style.messageCard , vezi mai jos). Și, desigur, natura slabă a JSX încurajează împărțirea în componente și fragmente puțin mai mult decât șabloanele lui Angular.

CSS local:

 .messageCard { margin-bottom: 1rem; }

CSS global:

 .floatRight { float: right; }

CSS compilat:

 .floatRight__qItBM { float: right; } .messageCard__1Dt_9 { margin-bottom: 1rem; }

După cum puteți vedea, încărcătorul de module CSS postfix fiecare clasă CSS cu un postfix aleatoriu, care garantează unicitatea. O modalitate simplă de a evita conflictele. Clasele sunt apoi referite prin intermediul obiectelor importate webpack. Un posibil dezavantaj al acestui lucru poate fi că nu puteți să creați un CSS cu o clasă și să-l măriți, așa cum am făcut în exemplul Angular. Pe de altă parte, acest lucru poate fi de fapt un lucru bun, deoarece te obligă să încapsulezi stilurile în mod corespunzător.

Rezumat: Mie personal îmi place JSX ceva mai mult decât șabloanele Angular, mai ales datorită suportului pentru completarea codului și verificarea tipului. Aceasta este într-adevăr o caracteristică ucigașă. Angular are acum compilatorul AOT, care poate identifica și câteva lucruri, completarea codului funcționează și pentru aproximativ jumătate din elementele de acolo, dar nu este chiar atât de complet ca JSX/TypeScript.

GraphQL - Încărcarea datelor

Așa că am decis să folosim GraphQL pentru a stoca date pentru această aplicație. Una dintre cele mai ușoare moduri de a crea back-end GraphQL este să utilizați unele BaaS, cum ar fi Graphcool. Deci asta am făcut. Practic, definiți doar modele și atribute, iar CRUD-ul dvs. este gata.

Cod comun

Deoarece unele dintre codurile legate de GraphQL sunt 100% la fel pentru ambele implementări, să nu-l repetăm ​​de două ori:

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

GraphQL este un limbaj de interogare menit să ofere un set mai bogat de funcționalități în comparație cu punctele finale RESTful clasice. Să analizăm această interogare specială.

  • PostsQuery este doar un nume pentru această interogare la care să se facă referire mai târziu, poate fi numit orice.
  • allPosts este cea mai importantă parte - face referire la funcția de a interoga toate înregistrările cu modelul „Post”. Acest nume a fost creat de Graphcool.
  • orderBy și first sunt parametrii funcției allPosts . createdAt este unul dintre atributele modelului Post . first: 5 înseamnă că va returna doar primele 5 rezultate ale interogării.
  • id , name , title și message sunt atributele modelului Post pe care dorim să fie incluse în rezultat. Alte atribute vor fi filtrate.

După cum puteți vedea deja, este destul de puternic. Consultați această pagină pentru a vă familiariza mai mult cu interogările GraphQL.

 interface Post { id: string name: string title: string message: string } interface PostsQueryResult { allPosts: Array<Post> }

Da, ca buni cetățeni TypeScript, creăm interfețe pentru rezultatele GraphQL.

unghiular

 @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 }) } }

Interogarea GraphQL este un observabil RxJS și ne abonam la ea. Funcționează un pic ca o promisiune, dar nu chiar, așa că nu avem noroc să folosim async/await . Desigur, mai există toPromise, dar oricum nu pare să fie calea Angular. Am setat fetchPolicy: 'network-only' pentru că, în acest caz, nu dorim să memorăm în cache datele, ci să reîncărcăm de fiecare dată.

Reacţiona

 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 } }

Versiunea React este aproape identică, dar deoarece apolloClient folosește promisiuni, putem profita de sintaxa async/await . Există și alte abordări în React care doar „lipesc” interogările GraphQL la componente de ordin superior, dar mi s-a părut că amestecă un pic prea mult stratul de date și de prezentare.

Rezumat: Ideile RxJS subscribe vs async/wait sunt într-adevăr aceleași.

GraphQL - Salvarea datelor

Cod comun

Din nou, un cod legat de GraphQL:

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

Scopul mutațiilor este de a crea sau actualiza înregistrări. Prin urmare, este benefic să declarați unele variabile cu mutația, deoarece acestea sunt modalitatea de transmitere a datelor în ea. Deci avem name , title și variabile message , tastate ca String , pe care trebuie să le umplem de fiecare dată când numim această mutație. Funcția createPost , din nou, este definită de Graphcool. Specificăm că cheile modelului Post vor avea valori din variabilele de mutație și, de asemenea, dorim ca doar id -ul Postului nou creat să fie trimis în schimb.

unghiular

 @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) }) } }

Când apelăm apollo.mutate , trebuie să furnizăm mutația pe care o apelăm și variabilele. Obținem rezultatul în apel invers de subscribe și folosim router injectat pentru a naviga înapoi la lista de postări.

Reacţiona

 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') } }

Foarte asemănător cu cel de mai sus, cu diferența de injecție mai „manuală” a dependenței și utilizarea async/await .

Rezumat: Din nou, nu este mare diferență aici. Subscribe vs async/wait este practic tot ceea ce diferă.

Forme

Dorim să atingem următoarele obiective cu formularele din această aplicație:

  • Legarea de date a câmpurilor la un model
  • Mesaje de validare pentru fiecare câmp, reguli multiple
  • Suport pentru verificarea validității întregului formular

Reacţiona

 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 }) }

Deci, biblioteca formstate funcționează după cum urmează: Pentru fiecare câmp al formularului, definiți un FieldState . Parametrul transmis este valoarea inițială. Proprietatea validators preia o funcție, care returnează „false” atunci când valoarea este validă și un mesaj de validare când valoarea nu este validă. Cu funcțiile de ajutor check și checkRequired , totul poate arăta frumos declarativ.

Pentru a avea validarea pentru întregul formular, este benefic să includeți și acele câmpuri cu o instanță FormState , care oferă apoi validitatea agregată.

 @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} />

Instanța FormState oferă proprietăți value , onChange și error , care pot fi utilizate cu ușurință cu orice componente front-end.

 <Button label='Cancel' onClick={formStore.goBack} raised accent /> &nbsp; <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.