Angular vs. React: qual è il migliore per lo sviluppo Web?
Pubblicato: 2022-03-11Ci sono innumerevoli articoli là fuori che discutono se React o Angular sia la scelta migliore per lo sviluppo web. Ne serve ancora un altro?
Il motivo per cui ho scritto questo articolo è perché nessuno degli articoli già pubblicati, sebbene contengano grandi spunti, è abbastanza approfondito da consentire a un pratico sviluppatore front-end di decidere quale potrebbe soddisfare le proprie esigenze.
In questo articolo imparerai come Angular e React mirano entrambi a risolvere problemi front-end simili anche se con filosofie molto diverse e se la scelta dell'uno o dell'altro è semplicemente una questione di preferenza personale. Per confrontarli, costruiremo la stessa applicazione due volte, una volta con Angular e poi di nuovo con React.
Annuncio prematuro di Angular
Due anni fa, ho scritto un articolo sull'ecosistema React. Tra gli altri punti, l'articolo affermava che Angular era diventato vittima di "morte per pre-annuncio". All'epoca, la scelta tra Angular e quasi ogni altra cosa era facile per chiunque non volesse che il proprio progetto venisse eseguito su un framework obsoleto. Angular 1 era obsoleto e Angular 2 non era nemmeno disponibile nella versione alfa.
Col senno di poi, i timori erano più o meno giustificati. Angular 2 è cambiato radicalmente e ha anche subito un'importante riscrittura poco prima del rilascio finale.
Due anni dopo, abbiamo Angular 4 con una promessa di relativa stabilità da qui in poi.
E adesso?
Angular vs. React: confronto di mele e arance
Alcune persone dicono che confrontare React e Angular è come confrontare le mele con le arance. Mentre uno è una libreria che si occupa di visualizzazioni, l'altro è un framework a tutti gli effetti.
Naturalmente, la maggior parte degli sviluppatori di React aggiungerà alcune librerie a React per trasformarlo in un framework completo. Inoltre, il flusso di lavoro risultante di questo stack è spesso ancora molto diverso da Angular, quindi la comparabilità è ancora limitata.
La differenza più grande sta nella gestione dello stato. Angular viene fornito con l'associazione dei dati in bundle, mentre React oggi è solitamente aumentato da Redux per fornire un flusso di dati unidirezionale e lavorare con dati immutabili. Questi sono approcci opposti a sé stanti e ora sono in corso innumerevoli discussioni sul fatto che il legame mutabile/dati sia migliore o peggiore di immutabile/unidirezionale.
Un campo di gioco alla pari
Poiché React è notoriamente più facile da hackerare, ho deciso, ai fini di questo confronto, di creare una configurazione React che rispecchi Angular ragionevolmente da vicino per consentire il confronto fianco a fianco dei frammenti di codice.
Alcune funzionalità di Angular che si distinguono ma non sono in React per impostazione predefinita sono:
| Caratteristica | Pacchetto angolare | Libreria di reazione |
|---|---|---|
| Data binding, inserimento delle dipendenze (DI) | @angular/core | MobX |
| Proprietà calcolate | rxjs | MobX |
| Routing basato sui componenti | @angolare/router | Reagire Router v4 |
| Componenti di design dei materiali | @angolare/materiale | Cassetta degli attrezzi di reazione |
| CSS con ambito ai componenti | @angular/core | Moduli CSS |
| Convalide dei moduli | @angolare/forme | StatoForma |
| Generatore di progetti | @angolare/cli | Script di reazione TS |
Associazione dati
L'associazione dei dati è probabilmente più facile da iniziare rispetto all'approccio unidirezionale. Naturalmente, sarebbe possibile andare nella direzione completamente opposta e usare Redux o mobx-state-tree con React e ngrx con Angular. Ma questo sarebbe un argomento per un altro post.
Proprietà calcolate
Per quanto riguarda le prestazioni, i semplici getter in Angular sono semplicemente fuori discussione poiché vengono chiamati ad ogni rendering. È possibile utilizzare BehaviorSubject da RsJS, che fa il lavoro.
Con React, è possibile utilizzare @computed da MobX, che raggiunge lo stesso obiettivo, con API probabilmente un po' più belle.
Iniezione di dipendenza
L'iniezione di dipendenza è piuttosto controversa perché va contro l'attuale paradigma React di programmazione funzionale e immutabilità. A quanto pare, una sorta di iniezione di dipendenza è quasi indispensabile negli ambienti di associazione dati, poiché aiuta con il disaccoppiamento (e quindi la presa in giro e il test) dove non esiste un'architettura a livello di dati separata.
Un altro vantaggio di DI (supportato in Angular) è la possibilità di avere diversi cicli di vita di diversi negozi. La maggior parte dei paradigmi React attuali utilizza una sorta di stato globale dell'app che esegue il mapping a componenti diversi, ma dalla mia esperienza è fin troppo facile introdurre bug durante la pulizia dello stato globale allo smontaggio del componente.
Avere un negozio che viene creato sul montaggio del componente (ed essere perfettamente disponibile per i figli di questo componente) sembra essere un concetto davvero utile e spesso trascurato.
Fuori dagli schemi in Angular, ma abbastanza facilmente riproducibile anche con MobX.
Instradamento
L'instradamento basato sui componenti consente ai componenti di gestire le proprie sottoroute invece di avere un'unica grande configurazione di router globale. Questo approccio è finalmente riuscito a react-router nella versione 4.
Progettazione materiale
È sempre bello iniziare con alcuni componenti di livello superiore e il design dei materiali è diventato qualcosa di simile a una scelta predefinita universalmente accettata, anche nei progetti non Google.
Ho scelto deliberatamente React Toolbox rispetto all'interfaccia utente materiale solitamente consigliata, poiché l'interfaccia utente materiale ha seri problemi di prestazioni autoconfessati con il loro approccio CSS in linea, che intendono risolvere nella prossima versione.
Inoltre, PostCSS/cssnext utilizzato in React Toolbox inizia comunque a sostituire Sass/LESS.
CSS con ambito
Le classi CSS sono qualcosa come le variabili globali. Esistono numerosi approcci per organizzare i CSS per prevenire i conflitti (incluso BEM), ma c'è una chiara tendenza attuale nell'uso di librerie che aiutano a elaborare CSS per prevenire quei conflitti senza la necessità di uno sviluppatore front-end per ideare elaborati sistemi di denominazione CSS.
Convalida del modulo
Le convalide dei moduli sono una funzionalità non banale e molto utilizzata. Buono avere quelli coperti da una libreria per prevenire la ripetizione del codice e i bug.
Generatore di progetti
Avere un generatore CLI per un progetto è solo un po' più conveniente che dover clonare boilerplate da GitHub.
Stessa applicazione, costruita due volte
Quindi creeremo la stessa applicazione in React e Angular. Niente di spettacolare, solo una Shoutboard che permette a chiunque di postare messaggi su una pagina comune.
Puoi provare le applicazioni qui:
- Shoutboard angolare
- Reazione di Shoutboard
Se vuoi avere l'intero codice sorgente, puoi ottenerlo da GitHub:
- Sorgente angolare Shoutboard
- Sorgente Shoutboard Reagire
Noterai che abbiamo utilizzato anche TypeScript per l'app React. I vantaggi del controllo del tipo in TypeScript sono evidenti. E ora, poiché una migliore gestione delle importazioni, async/await e rest spread è finalmente arrivata in TypeScript 2, lascia Babel/ES7/Flow nella polvere.
Inoltre, aggiungiamo Apollo Client ad entrambi perché vogliamo usare GraphQL. Voglio dire, REST è fantastico, ma dopo un decennio o giù di lì, invecchia.
Bootstrap e Routing
Per prima cosa, diamo un'occhiata ai punti di ingresso di entrambe le applicazioni.
Angolare
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' }Fondamentalmente, tutti i componenti che vogliamo utilizzare nell'applicazione devono andare alle dichiarazioni. Tutte le librerie di terze parti per l'importazione e tutti gli archivi globali per i fornitori. I componenti dei bambini hanno accesso a tutto questo, con l'opportunità di aggiungere altro materiale locale.
Reagire
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') ) Il componente <Provider/> viene utilizzato per l'inserimento delle dipendenze in MobX. Salva i negozi nel contesto in modo che i componenti di React possano iniettarli in un secondo momento. Sì, il contesto React può (probabilmente) essere utilizzato in modo sicuro.
La versione React è un po' più breve perché non ci sono dichiarazioni di moduli - di solito, si importa semplicemente ed è pronta per l'uso. A volte questo tipo di dipendenza è indesiderato (test), quindi per i negozi singleton globali, ho dovuto utilizzare questo modello GoF vecchio di decenni:
export class AppStore { static instance: AppStore static getInstance() { return AppStore.instance || (AppStore.instance = new AppStore()) } @observable username = 'Mr. User' } Angular's Router è iniettabile, quindi può essere utilizzato da qualsiasi luogo, non solo dai componenti. Per ottenere lo stesso risultato in reazione, utilizziamo il pacchetto mobx-react-router e routerStore .
Riepilogo: il bootstrap di entrambe le applicazioni è abbastanza semplice. React ha il vantaggio di essere più semplice, usando solo le importazioni invece dei moduli, ma, come vedremo in seguito, quei moduli possono essere abbastanza utili. Fare i singleton manualmente è un po' una seccatura. Per quanto riguarda la sintassi della dichiarazione di routing, JSON vs. JSX è solo una questione di preferenza.
Collegamenti e navigazione imperativa
Quindi ci sono due casi per cambiare rotta. Dichiarativo, utilizzando elementi <a href...> e imperativo, chiamando direttamente l'API di routing (e quindi posizione).
Angolare
<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 rileva automaticamente quale routerLink è attivo e inserisce una classe routerLinkActive appropriata su di esso, in modo che possa essere modellato.
Il router utilizza l'elemento speciale <router-outlet> per eseguire il rendering di qualsiasi percorso corrente imponga. È possibile avere molti <router-outlet> s, mentre scaviamo più a fondo nei sottocomponenti dell'applicazione.
@Injectable() export class FormService { constructor(private router: Router) { } goBack() { this.router.navigate(['/posts']) } } Il modulo router può essere iniettato in qualsiasi servizio (metà magicamente dal suo tipo TypeScript), la dichiarazione private lo memorizza quindi sull'istanza senza la necessità di un'assegnazione esplicita. Usa il metodo di navigate per cambiare gli URL.
Reagire
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 può anche impostare la classe del collegamento attivo con activeClassName .
Qui, non possiamo fornire direttamente il nome della classe, perché è stato reso unico dal compilatore dei moduli CSS e dobbiamo usare l'helper di style . Ne parleremo più avanti.
Come visto sopra, React Router utilizza l'elemento <Switch> all'interno di un elemento <App> . Poiché l'elemento <Switch> esegue il wrapping e monta il percorso corrente, significa che i percorsi secondari del componente corrente sono solo this.props.children . Quindi anche quello è componibile.
export class FormStore { routerStore: RouterStore constructor() { this.routerStore = RouterStore.getInstance() } goBack = () => { this.routerStore.history.push('/posts') } } Il mobx-router-store consente anche una facile iniezione e navigazione.
Riepilogo: entrambi gli approcci al routing sono abbastanza comparabili. Angular sembra essere più intuitivo, mentre React Router ha una componibilità un po' più semplice.
Iniezione di dipendenza
Si è già dimostrato utile separare il livello di dati dal livello di presentazione. Quello che stiamo cercando di ottenere con DI qui è fare in modo che i componenti dei livelli di dati (qui chiamati modello/store/servizio) seguano il ciclo di vita dei componenti visivi e quindi consentire di creare una o più istanze di tali componenti senza la necessità di toccare il globale stato. Inoltre, dovrebbe essere possibile combinare e abbinare dati compatibili e livelli di visualizzazione.
Gli esempi in questo articolo sono molto semplici, quindi tutte le cose DI potrebbero sembrare eccessive, ma tornano utili man mano che l'applicazione cresce.
Angolare
@Injectable() export class HomeService { message = 'Welcome to home page' counter = 0 increment() { this.counter++ } } Quindi qualsiasi classe può essere resa @injectable e le sue proprietà e metodi resi disponibili ai componenti.
@Component({ selector: 'app-home', templateUrl: './home.component.html', providers: [ HomeService ] }) export class HomeComponent { constructor( public homeService: HomeService, public appService: AppService, ) { } } Registrando l' HomeService ai providers del componente, lo rendiamo disponibile esclusivamente a questo componente. Non è un singleton ora, ma ogni istanza del componente riceverà una nuova copia, fresca di montaggio del componente. Ciò significa che non ci sono dati obsoleti dall'uso precedente.
Al contrario, AppService è stato registrato in app.module (vedi sopra), quindi è un singleton e rimane lo stesso per tutti i componenti, nonostante la vita dell'applicazione. Essere in grado di controllare il ciclo di vita dei servizi dai componenti è un concetto molto utile, ma sottovalutato.
DI funziona assegnando le istanze del servizio al costruttore del componente, identificato dai tipi TypeScript. Inoltre, le parole chiave public assegnano automaticamente i parametri a this , in modo che non abbiamo più bisogno di scrivere quelle noiose 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> La sintassi del modello di Angular, probabilmente piuttosto elegante. Mi piace la scorciatoia [()] , che funziona come un'associazione dati a 2 vie, ma sotto il cofano è in realtà un'associazione di attributi + evento. In base al ciclo di vita dei nostri servizi, homeService.counter verrà ripristinato ogni volta che ci allontaniamo da /home , ma appService.username rimane ed è accessibile da qualsiasi luogo.
Reagire
import { observable } from 'mobx' export class HomeStore { @observable counter = 0 increment = () => { this.counter++ } } Con MobX, dobbiamo aggiungere il decoratore @observable a qualsiasi proprietà che vogliamo rendere osservabile.
@observer export class Home extends React.Component<any, any> { homeStore: HomeStore componentWillMount() { this.homeStore = new HomeStore() } render() { return <Provider homeStore={this.homeStore}> <HomeComponent /> </Provider> } } Per gestire correttamente il ciclo di vita, dobbiamo fare un po' più di lavoro rispetto all'esempio Angular. Avvolgiamo l' HomeComponent all'interno di un Provider , che riceve una nuova istanza di HomeStore su ogni mount.
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> } } @observer HomeComponent ascoltare le modifiche nelle proprietà @observable .
Il meccanismo nascosto di questo è piuttosto interessante, quindi esaminiamolo brevemente qui. Il decoratore @observable sostituisce una proprietà in un oggetto con getter e setter, che gli consente di intercettare le chiamate. Quando viene chiamata la funzione di rendering di un componente aumentato di @observer , quelle proprietà getter vengono chiamate e mantengono un riferimento al componente che le ha chiamate.
Quindi, quando viene chiamato setter e il valore viene modificato, vengono chiamate le funzioni di rendering dei componenti che hanno utilizzato la proprietà nell'ultimo rendering. Ora, i dati su quali proprietà vengono utilizzate dove vengono aggiornati e l'intero ciclo può ricominciare.
Un meccanismo molto semplice e anche abbastanza performante. Spiegazione più approfondita qui.
Il decoratore @inject viene utilizzato per iniettare istanze di appStore e homeStore negli oggetti di scena di HomeComponent . A questo punto, ciascuno di questi negozi ha un ciclo di vita diverso. appStore è lo stesso durante la vita dell'applicazione, ma homeStore viene creato di fresco ad ogni navigazione verso il percorso “/home”.
Il vantaggio di ciò è che non è necessario pulire le proprietà manualmente come accade quando tutti i negozi sono globali, il che è un problema se il percorso è una pagina di "dettaglio" che contiene dati completamente diversi ogni volta.
Riepilogo: poiché la gestione del ciclo di vita del provider è una caratteristica intrinseca di Angular DI, è ovviamente più semplice ottenerla lì. Anche la versione React è utilizzabile ma comporta molto più standard.
Proprietà calcolate
Reagire
Iniziamo con Reagire su questo, ha una soluzione più semplice.
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` } } Quindi abbiamo una proprietà calcolata che si lega a counter e restituisce un messaggio correttamente pluralizzato. Il risultato di counterMessage viene memorizzato nella cache e ricalcolato solo quando counter cambia.
<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> Quindi, facciamo riferimento alla proprietà (e al metodo di increment ) dal modello JSX. Il campo di input è guidato dall'associazione a un valore e consentendo a un metodo di appStore di gestire l'evento utente.
Angolare
Per ottenere lo stesso effetto in Angular, dobbiamo essere un po' più fantasiosi.
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) } } Abbiamo bisogno di definire tutti i valori che servono come base per una proprietà calcolata come BehaviorSubject . La stessa proprietà calcolata è anche un BehaviorSubject , perché qualsiasi proprietà calcolata può fungere da input per un'altra proprietà calcolata.
Naturalmente, RxJS può fare molto di più di questo, ma sarebbe un argomento per un articolo completamente diverso. Lo svantaggio minore è che questo uso banale di RxJS per le proprietà appena calcolate è un po' più dettagliato dell'esempio di reazione ed è necessario gestire le sottoscrizioni manualmente (come qui nel costruttore).
<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> Nota come possiamo fare riferimento al soggetto RxJS con il | async tubo | async . Questo è un bel tocco, molto più breve della necessità di iscriversi ai tuoi componenti. Il componente di input è guidato dalla direttiva [(ngModel)] . Nonostante sembri strano, in realtà è piuttosto elegante. Solo uno zucchero sintattico per l'associazione dei dati del valore a appService.username e l'assegnazione automatica del valore dall'evento di input dell'utente.

Riepilogo: le proprietà calcolate sono più facili da implementare in React/MobX che in Angular/RxJS, ma RxJS potrebbe fornire alcune funzionalità FRP più utili, che potrebbero essere apprezzate in seguito.
Modelli e CSS
Per mostrare come i modelli si sovrappongono, utilizziamo il componente Post che mostra un elenco di post.
Angolare
@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() } } Questo componente collega semplicemente HTML, CSS e servizi iniettati e chiama anche la funzione per caricare i post dall'API all'inizializzazione. AppService è un singleton definito nel modulo dell'applicazione, mentre PostsService è temporaneo, con una nuova istanza creata ogni volta che viene creato un componente. Il CSS a cui fa riferimento questo componente ha l'ambito di questo componente, il che significa che il contenuto non può influire su nulla al di fuori del componente.
<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> Nel modello HTML, facciamo riferimento principalmente ai componenti di Angular Material. Per averli a disposizione era necessario includerli nelle importazioni di app.module (vedi sopra). La direttiva *ngFor viene utilizzata per ripetere il componente md-card per ogni post.
CSS locale:
.mat-card { margin-bottom: 1rem; } Il CSS locale aumenta solo una delle classi presenti sul componente md-card .
CSS globale:
.float-right { float: right; } Questa classe è definita nel file style.css globale per renderla disponibile per tutti i componenti. Può essere referenziato nel modo standard, class="float-right" .
CSS compilato:
.float-right { float: right; } .mat-card[_ngcontent-c1] { margin-bottom: 1rem; } Nel CSS compilato, possiamo vedere che il CSS locale è stato applicato al componente renderizzato utilizzando il selettore di attributi [_ngcontent-c1] . Ogni componente Angular renderizzato ha una classe generata come questa per scopi di ambito CSS.
Il vantaggio di questo meccanismo è che possiamo fare riferimento alle classi normalmente e l'ambito viene gestito "sotto il cofano".
Reagire
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> } } In React, ancora una volta, dobbiamo utilizzare l'approccio Provider per rendere "transitoria" la dipendenza di PostsStore . Importiamo anche stili CSS, indicati come style e appStyle , per poter utilizzare le classi da quei file CSS in 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> } } Naturalmente, JSX sembra molto più JavaScript-y rispetto ai modelli HTML di Angular, il che può essere una cosa buona o cattiva a seconda dei tuoi gusti. Invece della direttiva *ngFor , utilizziamo il costrutto map per scorrere i post.
Ora, Angular potrebbe essere il framework che promuove di più TypeScript, ma in realtà è JSX dove TypeScript brilla davvero. Con l'aggiunta di moduli CSS (importati sopra), trasforma davvero la codifica del tuo modello in uno zen di completamento del codice. Ogni singola cosa è controllata dal tipo. Componenti, attributi e persino classi CSS ( appStyle.floatRight e style.messageCard , vedi sotto). E, naturalmente, la natura snella di JSX incoraggia la suddivisione in componenti e frammenti un po' più dei modelli di Angular.
CSS locale:
.messageCard { margin-bottom: 1rem; }CSS globale:
.floatRight { float: right; }CSS compilato:
.floatRight__qItBM { float: right; } .messageCard__1Dt_9 { margin-bottom: 1rem; }Come puoi vedere, il caricatore dei moduli CSS aggiunge ad ogni classe CSS un suffisso casuale, che garantisce l'unicità. Un modo semplice per evitare conflitti. Le classi vengono quindi referenziate tramite gli oggetti importati dal webpack. Un possibile inconveniente può essere che non puoi semplicemente creare un CSS con una classe e aumentarlo, come abbiamo fatto nell'esempio Angular. D'altra parte, questa può essere effettivamente una buona cosa, perché ti costringe a incapsulare correttamente gli stili.
Riepilogo: personalmente mi piace un po' di più JSX rispetto ai modelli Angular, soprattutto a causa del completamento del codice e del supporto per il controllo del tipo. Questa è davvero una caratteristica killer. Angular ora ha il compilatore AOT, che può anche individuare alcune cose, il completamento del codice funziona anche per circa la metà delle cose lì, ma non è così completo come JSX/TypeScript.
GraphQL - Caricamento dei dati
Quindi abbiamo deciso di utilizzare GraphQL per archiviare i dati per questa applicazione. Uno dei modi più semplici per creare back-end GraphQL è utilizzare alcuni BaaS, come Graphcool. Quindi è quello che abbiamo fatto. Fondamentalmente, definisci solo modelli e attributi e il tuo CRUD è a posto.
Codice comune
Poiché parte del codice relativo a GraphQL è uguale al 100% per entrambe le implementazioni, non ripetiamolo due volte:
const PostsQuery = gql` query PostsQuery { allPosts(orderBy: createdAt_DESC, first: 5) { id, name, title, message } } `GraphQL è un linguaggio di query volto a fornire un insieme più ricco di funzionalità rispetto ai classici endpoint RESTful. Analizziamo questa particolare query.
-
PostsQueryè solo un nome per questa query a cui fare riferimento in seguito, può essere chiamato qualsiasi cosa. -
allPostsè la parte più importante: fa riferimento alla funzione per interrogare tutti i record con il modello `Post`. Questo nome è stato creato da Graphcool. -
orderByefirstsono parametri della funzioneallPosts.createdAtè uno degli attributi del modelloPost.first: 5significa che restituirà solo i primi 5 risultati della query. -
id,name,titleemessagesono gli attributi del modelloPostche vogliamo includere nel risultato. Gli altri attributi verranno esclusi.
Come puoi già vedere, è piuttosto potente. Dai un'occhiata a questa pagina per familiarizzare di più con le query GraphQL.
interface Post { id: string name: string title: string message: string } interface PostsQueryResult { allPosts: Array<Post> }Sì, da bravi cittadini di TypeScript, creiamo interfacce per i risultati di GraphQL.
Angolare
@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 }) } } La query GraphQL è un osservabile RxJS e ci iscriviamo ad essa. Funziona un po' come una promessa, ma non del tutto, quindi non siamo fortunati a usare async/await . Certo, c'è ancora da promettere, ma non sembra comunque essere il modo angolare. fetchPolicy: 'network-only' perché in questo caso non vogliamo memorizzare nella cache i dati, ma recuperarli ogni volta.
Reagire
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 } } La versione React è quasi identica, ma poiché apolloClient qui usa le promesse, possiamo sfruttare la sintassi async/await . Ci sono altri approcci in React che semplicemente "nastrano" le query GraphQL su componenti di ordine superiore, ma mi è sembrato che mescolasse insieme i dati e il livello di presentazione un po' troppo.
Riepilogo: le idee di sottoscrizione RxJS vs. async/await sono davvero le stesse.
GraphQL - Salvataggio dei dati
Codice comune
Ancora una volta, del codice relativo a GraphQL:
const AddPostMutation = gql` mutation AddPostMutation($name: String!, $title: String!, $message: String!) { createPost( name: $name, title: $title, message: $message ) { id } } ` Lo scopo delle mutazioni è creare o aggiornare record. È quindi vantaggioso dichiarare alcune variabili con la mutazione perché quelle sono il modo in cui passare i dati al suo interno. Quindi abbiamo le variabili name , title e message , digitate come String , che dobbiamo riempire ogni volta che chiamiamo questa mutazione. La funzione createPost , ancora, è definita da Graphcool. Specifichiamo che le chiavi del modello Post avranno valori dalle nostre variabili di mutazione e anche che vogliamo che solo l' id del Post appena creato venga inviato in cambio.
Angolare
@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) }) } } Quando chiamiamo apollo.mutate , dobbiamo fornire la mutazione che chiamiamo e anche le variabili. Otteniamo il risultato nella richiamata subscribe e utilizziamo il router iniettato per tornare all'elenco dei post.
Reagire
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') } } Molto simile a quanto sopra, con la differenza di un'iniezione di dipendenza più "manuale" e l'uso di async/await .
Riepilogo: Ancora una volta, non c'è molta differenza qui. Subscribe vs. async/await è fondamentalmente tutto ciò che differisce.
Le forme
Vogliamo raggiungere i seguenti obiettivi con i moduli in questa applicazione:
- Associazione dati di campi a un modello
- Messaggi di convalida per ogni campo, più regole
- Supporto per verificare se l'intero modulo è valido
Reagire
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 }) } Quindi la libreria formstate funziona come segue: per ogni campo del tuo modulo, definisci un FieldState . Il parametro passato è il valore iniziale. La proprietà validators accetta una funzione, che restituisce "false" quando il valore è valido e un messaggio di convalida quando il valore non è valido. Con le funzioni di supporto check e checkRequired , tutto può sembrare ben dichiarativo.
Per avere la convalida per l'intero modulo, è utile racchiudere anche quei campi con un'istanza FormState , che fornisce quindi la validità aggregata.
@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} /> L'istanza FormState fornisce proprietà value , onChange e error , che possono essere facilmente utilizzate con qualsiasi componente front-end.
<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.
