Angular vs. React: Was ist besser für die Webentwicklung?

Veröffentlicht: 2022-03-11

Es gibt unzählige Artikel da draußen, in denen diskutiert wird, ob React oder Angular die bessere Wahl für die Webentwicklung ist. Brauchen wir noch einen?

Der Grund, warum ich diesen Artikel geschrieben habe, ist, dass keiner der bereits veröffentlichten Artikel – obwohl sie großartige Einblicke enthalten – tief genug geht, damit ein praktischer Front-End-Entwickler entscheiden kann, welcher seinen Bedürfnissen entspricht.

In diesem Artikel erfahren Sie, wie Angular und React beide darauf abzielen, ähnliche Frontend-Probleme zu lösen, wenn auch mit sehr unterschiedlichen Philosophien, und ob die Wahl des einen oder anderen nur eine Frage der persönlichen Präferenz ist. Um sie zu vergleichen, werden wir dieselbe Anwendung zweimal erstellen, einmal mit Angular und dann noch einmal mit React.

Angulars verfrühte Ankündigung

Vor zwei Jahren habe ich einen Artikel über das React Ecosystem geschrieben. In dem Artikel wurde unter anderem argumentiert, dass Angular Opfer eines „Todes durch Vorankündigung“ geworden sei. Damals war die Wahl zwischen Angular und fast allem anderen leicht für alle, die nicht wollten, dass ihr Projekt auf einem veralteten Framework läuft. Angular 1 war veraltet und Angular 2 war nicht einmal in der Alpha-Version verfügbar.

Im Nachhinein waren die Befürchtungen mehr oder weniger berechtigt. Angular 2 hat sich dramatisch verändert und wurde kurz vor der endgültigen Veröffentlichung sogar umfassend umgeschrieben.

Zwei Jahre später haben wir Angular 4 mit dem Versprechen relativer Stabilität von nun an.

Was jetzt?

Angular vs. React: Äpfel und Birnen im Vergleich

Einige Leute sagen, dass der Vergleich von React und Angular wie der Vergleich von Äpfeln mit Birnen ist. Während das eine eine Bibliothek ist, die sich mit Ansichten befasst, ist das andere ein vollwertiges Framework.

Natürlich fügen die meisten React-Entwickler React einige Bibliotheken hinzu, um daraus ein vollständiges Framework zu machen. Andererseits unterscheidet sich der resultierende Workflow dieses Stacks oft noch stark von Angular, sodass die Vergleichbarkeit noch eingeschränkt ist.

Der größte Unterschied liegt in der Zustandsverwaltung. Angular wird mit gebündelter Datenbindung geliefert, während React heute normalerweise durch Redux erweitert wird, um einen unidirektionalen Datenfluss bereitzustellen und mit unveränderlichen Daten zu arbeiten. Das sind an sich gegensätzliche Ansätze, und es werden unzählige Diskussionen darüber geführt, ob veränderliche/Datenbindung besser oder schlechter als unveränderlich/unidirektional ist.

Ein Level Playing Field

Da React bekanntermaßen einfacher zu hacken ist, habe ich mich für den Zweck dieses Vergleichs entschieden, ein React-Setup zu erstellen, das Angular ziemlich genau widerspiegelt, um einen direkten Vergleich von Code-Snippets zu ermöglichen.

Bestimmte Angular-Funktionen, die auffallen, aber standardmäßig nicht in React enthalten sind, sind:

Feature Eckiges Paket Bibliothek reagieren
Datenbindung, Abhängigkeitsinjektion (DI) @eckig/Kern MobX
Berechnete Eigenschaften rxjs MobX
Komponentenbasiertes Routing @Angular/Router Reaktionsrouter v4
Komponenten des Materialdesigns @eckig/material Reaktions-Toolbox
CSS auf Komponenten beschränkt @eckig/Kern CSS-Module
Formularvalidierungen @eckig/formen FormState
Projektgenerator @eckig/kli Reaktionsskripte TS

Datenbindung

Die Datenbindung ist zu Beginn wohl einfacher als der unidirektionale Ansatz. Natürlich wäre es möglich, in die völlig entgegengesetzte Richtung zu gehen und Redux oder mobx-state-tree mit React und ngrx mit Angular zu verwenden. Aber das wäre ein Thema für einen anderen Beitrag.

Berechnete Eigenschaften

In Bezug auf die Leistung kommen einfache Getter in Angular einfach nicht in Frage, da sie bei jedem Rendering aufgerufen werden. Es ist möglich, BehaviorSubject von RsJS zu verwenden, was die Aufgabe erledigt.

Mit React ist es möglich, @computed von MobX zu verwenden, was das gleiche Ziel erreicht, mit einer wohl etwas netteren API.

Abhängigkeitsspritze

Dependency Injection ist irgendwie umstritten, weil es gegen das aktuelle React-Paradigma der funktionalen Programmierung und Unveränderlichkeit verstößt. Wie sich herausstellt, ist eine Art Abhängigkeitsinjektion in Datenbindungsumgebungen fast unverzichtbar, da sie beim Entkoppeln (und damit beim Spotten und Testen) hilft, wenn keine separate Datenschichtarchitektur vorhanden ist.

Ein weiterer Vorteil von DI (unterstützt in Angular) ist die Möglichkeit, unterschiedliche Lebenszyklen verschiedener Stores zu haben. Die meisten aktuellen React-Paradigmen verwenden eine Art globalen App-Status, der verschiedenen Komponenten zugeordnet ist, aber meiner Erfahrung nach ist es allzu einfach, Fehler einzuführen, wenn der globale Status beim Unmounten von Komponenten bereinigt wird.

Einen Speicher zu haben, der beim Einhängen von Komponenten erstellt wird (und für die untergeordneten Elemente dieser Komponente nahtlos verfügbar ist), scheint ein wirklich nützliches und oft übersehenes Konzept zu sein.

Out of the box in Angular, aber auch mit MobX recht einfach reproduzierbar.

Routing

Komponentenbasiertes Routing ermöglicht es Komponenten, ihre eigenen Unterrouten zu verwalten, anstatt eine große globale Router-Konfiguration zu haben. Dieser Ansatz hat es endlich bis in den react-router in Version 4 geschafft.

Material Design

Es ist immer schön, mit einigen übergeordneten Komponenten zu beginnen, und Materialdesign ist so etwas wie eine allgemein akzeptierte Standardauswahl geworden, selbst in Nicht-Google-Projekten.

Ich habe React Toolbox bewusst gegenüber dem normalerweise empfohlenen Material UI gewählt, da Material UI ernsthafte Performance-Probleme mit ihrem Inline-CSS-Ansatz hat, die sie in der nächsten Version lösen wollen.

Außerdem beginnt PostCSS/cssnext, das in React Toolbox verwendet wird, sowieso damit, Sass/LESS zu ersetzen.

Scoped CSS

CSS-Klassen sind so etwas wie globale Variablen. Es gibt zahlreiche Ansätze zum Organisieren von CSS, um Konflikte zu verhindern (einschließlich BEM), aber es gibt einen klaren aktuellen Trend zur Verwendung von Bibliotheken, die bei der Verarbeitung von CSS helfen, um diese Konflikte zu vermeiden, ohne dass ein Frontend-Entwickler ausgefeilte CSS-Benennungssysteme entwickeln muss.

Formularvalidierung

Formularvalidierungen sind eine nicht triviale und sehr weit verbreitete Funktion. Gut, wenn diese von einer Bibliothek abgedeckt werden, um Codewiederholungen und Fehler zu vermeiden.

Projektgenerator

Einen CLI-Generator für ein Projekt zu haben, ist nur ein bisschen bequemer, als Boilerplates von GitHub klonen zu müssen.

Gleiche Anwendung, zweimal gebaut

Also werden wir dieselbe Anwendung in React und Angular erstellen. Nichts Spektakuläres, nur ein Shoutboard, das es jedem ermöglicht, Nachrichten auf einer gemeinsamen Seite zu posten.

Hier können Sie die Anwendungen ausprobieren:

  • Shoutboard eckig
  • Shoutboard-Reaktion

Shoutboard-Anwendung

Wenn Sie den gesamten Quellcode haben möchten, können Sie ihn von GitHub herunterladen:

  • Shoutboard Winkelquelle
  • Shoutboard-Reaktionsquelle

Sie werden feststellen, dass wir TypeScript auch für die React-App verwendet haben. Die Vorteile der Typprüfung in TypeScript liegen auf der Hand. Und jetzt, da eine bessere Handhabung von Importen, Async/Await und Rest Spread endlich in TypeScript 2 angekommen ist, lässt es Babel/ES7/Flow im Staub.

Lassen Sie uns auch Apollo Client zu beiden hinzufügen, da wir GraphQL verwenden möchten. Ich meine, REST ist großartig, aber nach einem Jahrzehnt oder so wird es alt.

Bootstrap und Routing

Werfen wir zunächst einen Blick auf die Einstiegspunkte beider Anwendungen.

Eckig

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

Grundsätzlich müssen alle Komponenten, die wir in der Anwendung verwenden möchten, in Deklarationen gehen. Alle Bibliotheken von Drittanbietern für Importe und alle globalen Stores für Anbieter. Kinderkomponenten haben Zugriff auf all dies, mit der Möglichkeit, weitere lokale Inhalte hinzuzufügen.

Reagieren

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

Die Komponente <Provider/> wird für die Abhängigkeitsinjektion in MobX verwendet. Es speichert Stores im Kontext, damit React-Komponenten sie später einfügen können. Ja, React-Kontext kann (wohl) sicher verwendet werden.

Die React-Version ist etwas kürzer, da es keine Moduldeklarationen gibt - normalerweise importieren Sie einfach und es ist einsatzbereit. Manchmal ist diese Art von harter Abhängigkeit unerwünscht (Testen), daher musste ich für globale Singleton-Speicher dieses jahrzehntealte GoF-Muster verwenden:

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

Der Router von Angular ist injizierbar, sodass er von überall verwendet werden kann, nicht nur von Komponenten. Um dasselbe in React zu erreichen, verwenden wir das Paket mobx-react-router und injizieren den routerStore .

Zusammenfassung: Das Bootstrapping beider Anwendungen ist recht einfach. React hat den Vorteil, dass es einfacher ist und nur Importe anstelle von Modulen verwendet, aber wie wir später sehen werden, können diese Module sehr praktisch sein. Das manuelle Erstellen von Singletons ist ein bisschen lästig. Was die Routing-Deklarationssyntax betrifft, ist JSON vs. JSX nur eine Frage der Präferenz.

Links und zwingende Navigation

Es gibt also zwei Fälle für das Wechseln einer Route. Deklarativ unter Verwendung von <a href...> -Elementen und imperativ unter direktem Aufruf der Routing- (und damit Standort-) API.

Eckig

 <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 erkennt automatisch, welcher routerLink aktiv ist, und fügt ihm eine geeignete routerLinkActive -Klasse hinzu, damit er formatiert werden kann.

Der Router verwendet das spezielle <router-outlet> -Element, um den aktuellen Pfad wiederzugeben. Es ist möglich, viele <router-outlet> s zu haben, wenn wir uns eingehender mit den Unterkomponenten der Anwendung befassen.

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

Das Router-Modul kann in jeden Dienst eingefügt werden (halb magisch durch seinen TypeScript-Typ), die private Deklaration speichert es dann auf der Instanz, ohne dass eine explizite Zuweisung erforderlich ist. Verwenden Sie navigate , um URLs zu wechseln.

Reagieren

 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 kann auch die Klasse des aktiven Links mit activeClassName .

Hier können wir den Klassennamen nicht direkt angeben, da er vom CSS-Modul-Compiler eindeutig gemacht wurde und wir den style verwenden müssen. Dazu später mehr.

Wie oben zu sehen ist, verwendet React Router das <Switch> -Element innerhalb eines <App> -Elements. Da das Element <Switch> nur die aktuelle Route umschließt und einhängt, bedeutet dies, dass Unterrouten der aktuellen Komponente nur this.props.children sind. Das ist also auch zusammensetzbar.

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

Das mobx-router-store Paket ermöglicht auch eine einfache Injektion und Navigation.

Fazit: Beide Routing-Ansätze sind durchaus vergleichbar. Angular scheint intuitiver zu sein, während React Router etwas unkomplizierter zusammengesetzt werden kann.

Abhängigkeitsspritze

Es hat sich bereits bewährt, die Datenschicht von der Präsentationsschicht zu trennen. Was wir hier mit DI zu erreichen versuchen, ist, dass die Komponenten der Datenschichten (hier als Modell/Speicher/Dienst bezeichnet) dem Lebenszyklus visueller Komponenten folgen und es somit ermöglichen, eine oder mehrere Instanzen solcher Komponenten zu erstellen, ohne dass sie global berührt werden müssen Zustand. Außerdem sollte es möglich sein, kompatible Daten und Visualisierungsschichten zu mischen und anzupassen.

Die Beispiele in diesem Artikel sind sehr einfach, daher mag das ganze DI-Zeug übertrieben erscheinen, aber es ist praktisch, wenn die Anwendung wächst.

Eckig

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

So kann jede Klasse @injectable gemacht und ihre Eigenschaften und Methoden für Komponenten verfügbar gemacht werden.

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

Mit der Registrierung des HomeService bei den providers der Komponente stellen wir ihn ausschließlich dieser Komponente zur Verfügung. Es ist jetzt kein Singleton, aber jede Instanz der Komponente erhält eine neue Kopie, frisch auf dem Mount der Komponente. Das bedeutet keine veralteten Daten aus der vorherigen Nutzung.

Im Gegensatz dazu wurde der AppService beim app.module (siehe oben) registriert, ist also ein Singleton und bleibt für alle Komponenten gleich, während der gesamten Lebensdauer der Anwendung. In der Lage zu sein, den Lebenszyklus von Diensten von Komponenten aus zu steuern, ist ein sehr nützliches, aber unterschätztes Konzept.

DI funktioniert, indem es die Dienstinstanzen dem Konstruktor der Komponente zuweist, der durch TypeScript-Typen identifiziert wird. Außerdem weisen die public Schlüsselwörter this automatisch die Parameter zu, sodass wir diese langweiligen this.homeService = homeService nicht mehr schreiben müssen.

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

Die Template-Syntax von Angular, wohl recht elegant. Ich mag die Verknüpfung [()] , die wie eine 2-Wege-Datenbindung funktioniert, aber unter der Haube ist sie tatsächlich eine Attributbindung + ein Ereignis. Wie es der Lebenszyklus unserer Dienste vorschreibt, wird homeService.counter jedes Mal zurückgesetzt, wenn wir von /home weg navigieren, aber der appService.username bleibt und ist von überall aus zugänglich.

Reagieren

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

Bei MobX müssen wir den @observable Dekorator zu jeder Eigenschaft hinzufügen, die wir beobachtbar machen möchten.

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

Um den Lebenszyklus korrekt zu verwalten, müssen wir etwas mehr Arbeit leisten als im Angular-Beispiel. Wir packen die HomeComponent in einen Provider , der bei jedem Mount eine neue Instanz von HomeStore erhält.

 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 verwendet den @observer Decorator, um auf Änderungen in @observable Eigenschaften zu hören.

Der Mechanismus unter der Haube ist ziemlich interessant, also gehen wir ihn hier kurz durch. Der @observable Dekorator ersetzt eine Eigenschaft in einem Objekt durch Getter und Setter, wodurch Aufrufe abgefangen werden können. Wenn die @observer einer erweiterten @observer-Komponente aufgerufen wird, werden diese Eigenschafts-Getter aufgerufen und behalten einen Verweis auf die Komponente, die sie aufgerufen hat.

Wenn dann Setter aufgerufen und der Wert geändert wird, werden Renderfunktionen von Komponenten aufgerufen, die die Eigenschaft beim letzten Rendern verwendet haben. Jetzt werden Daten darüber, welche Eigenschaften wo verwendet werden, aktualisiert, und der gesamte Zyklus kann von vorne beginnen.

Ein sehr einfacher Mechanismus und auch ziemlich leistungsfähig. Genauere Erklärung hier.

Der Decorator @inject wird verwendet, um AppStore- und homeStore Instanzen in die Requisiten HomeComponent appStore einzufügen. Zu diesem Zeitpunkt hat jeder dieser Stores einen anderen Lebenszyklus. appStore ist während der Lebensdauer der Anwendung derselbe, aber homeStore wird bei jeder Navigation zur „/home“-Route neu erstellt.

Der Vorteil davon ist, dass es nicht notwendig ist, die Eigenschaften manuell zu bereinigen, wie es der Fall ist, wenn alle Geschäfte global sind, was mühsam ist, wenn die Route eine „Detail“-Seite ist, die jedes Mal völlig andere Daten enthält.

Fazit: Da das Provider Lifecycle Management ein inhärentes Feature von Angulars DI ist, ist es dort natürlich einfacher zu erreichen. Die React-Version ist ebenfalls verwendbar, beinhaltet jedoch viel mehr Boilerplate.

Berechnete Eigenschaften

Reagieren

Beginnen wir mit React, es hat eine einfachere Lösung.

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

Wir haben also eine berechnete Eigenschaft, die an den counter bindet und eine ordnungsgemäß pluralisierte Nachricht zurückgibt. Das Ergebnis von counterMessage wird zwischengespeichert und nur dann neu berechnet, wenn sich counter ändert.

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

Dann verweisen wir auf die Eigenschaft (und increment ) aus der JSX-Vorlage. Das Eingabefeld wird gesteuert, indem es an einen Wert gebunden wird und eine Methode aus dem appStore das Benutzerereignis verarbeitet.

Eckig

Um den gleichen Effekt in Angular zu erzielen, müssen wir etwas erfinderischer sein.

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

Wir müssen alle Werte, die als Grundlage für eine berechnete Eigenschaft dienen, als BehaviorSubject definieren. Die berechnete Eigenschaft selbst ist auch ein BehaviorSubject , da jede berechnete Eigenschaft als Eingabe für eine andere berechnete Eigenschaft dienen kann.

Natürlich kann RxJS noch viel mehr als nur das, aber das wäre ein Thema für einen ganz anderen Artikel. Der kleine Nachteil ist, dass diese triviale Verwendung von RxJS für nur berechnete Eigenschaften etwas ausführlicher ist als das Reaktionsbeispiel, und Sie müssen Abonnements manuell verwalten (wie hier im Konstruktor).

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

Beachten Sie, wie wir mit | async auf das RxJS-Subjekt verweisen können | async Pipe. Das ist eine nette Geste, viel kürzer als das Abonnieren Ihrer Komponenten. Die input wird von der Direktive [(ngModel)] . Obwohl es seltsam aussieht, ist es eigentlich ziemlich elegant. Nur ein syntaktischer Zucker für die Datenbindung des Werts an appService.username und die automatische Zuweisung des Werts aus dem Benutzereingabeereignis.

Zusammenfassung: Berechnete Eigenschaften sind in React/MobX einfacher zu implementieren als in Angular/RxJS, aber RxJS bietet möglicherweise einige nützlichere FRP-Funktionen, die später geschätzt werden könnten.

Vorlagen und CSS

Um zu zeigen, wie Vorlagen sich gegenseitig stapeln, verwenden wir die Posts-Komponente, die eine Liste von Posts anzeigt.

Eckig

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

Diese Komponente verbindet lediglich HTML, CSS und eingefügte Dienste und ruft auch die Funktion zum Laden von Beiträgen von der API bei der Initialisierung auf. AppService ist ein im Anwendungsmodul definierter Singleton, während PostsService vorübergehend ist und bei jeder erstellten Zeitkomponente eine neue Instanz erstellt wird. CSS, auf das von dieser Komponente verwiesen wird, ist auf diese Komponente beschränkt, was bedeutet, dass der Inhalt nichts außerhalb der Komponente beeinflussen kann.

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

Im HTML-Template referenzieren wir hauptsächlich Komponenten aus Angular Material. Um sie verfügbar zu haben, war es notwendig, sie in die app.module Importe aufzunehmen (siehe oben). Die Direktive *ngFor wird verwendet, um die md-card Komponente für jeden Beitrag zu wiederholen.

Lokales CSS:

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

Das lokale CSS erweitert lediglich eine der Klassen, die auf der md-card Komponente vorhanden sind.

Globales CSS:

 .float-right { float: right; }

Diese Klasse ist in der Datei global style.css definiert, um sie für alle Komponenten verfügbar zu machen. Es kann auf die Standardmethode class="float-right" verwiesen werden.

Kompiliertes CSS:

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

In kompiliertem CSS können wir sehen, dass das lokale CSS mithilfe des Attributselektors [_ngcontent-c1] auf die gerenderte Komponente beschränkt wurde. Jede gerenderte Angular-Komponente hat eine generierte Klasse wie diese für CSS-Scoping-Zwecke.

Der Vorteil dieses Mechanismus besteht darin, dass wir Klassen normal referenzieren können und der Bereich „unter der Haube“ gehandhabt wird.

Reagieren

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

Auch in React müssen wir den Provider -Ansatz verwenden, um die PostsStore Abhängigkeit „vorübergehend“ zu machen. Wir importieren auch CSS-Stile, die als style und appStyle werden, um die Klassen aus diesen CSS-Dateien in JSX verwenden zu können.

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

Natürlich fühlt sich JSX viel mehr JavaScript-y an als die HTML-Vorlagen von Angular, was je nach Geschmack eine gute oder schlechte Sache sein kann. Anstelle der Direktive *ngFor verwenden wir das map -Konstrukt, um Posts zu durchlaufen.

Nun, Angular ist vielleicht das Framework, das am meisten für TypeScript wirbt, aber es ist eigentlich JSX, wo TypeScript wirklich glänzt. Mit dem Hinzufügen von CSS-Modulen (oben importiert) verwandelt es Ihre Vorlagencodierung wirklich in Code-Vervollständigungs-Zen. Jedes einzelne Ding ist typgeprüft. Komponenten, Attribute, sogar CSS-Klassen ( appStyle.floatRight und style.messageCard , siehe unten). Und natürlich fördert die schlanke Natur von JSX die Aufteilung in Komponenten und Fragmente etwas mehr als die Vorlagen von Angular.

Lokales CSS:

 .messageCard { margin-bottom: 1rem; }

Globales CSS:

 .floatRight { float: right; }

Kompiliertes CSS:

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

Wie Sie sehen können, postfixiert der CSS Modules Loader jede CSS-Klasse mit einem zufälligen Postfix, was die Eindeutigkeit garantiert. Ein einfacher Weg, um Konflikte zu vermeiden. Klassen werden dann über die importierten Webpack-Objekte referenziert. Ein möglicher Nachteil davon kann sein, dass Sie nicht einfach ein CSS mit einer Klasse erstellen und diese erweitern können, wie wir es im Angular-Beispiel getan haben. Auf der anderen Seite kann dies sogar eine gute Sache sein, weil es Sie dazu zwingt, Stile richtig zu kapseln.

Zusammenfassung: Mir persönlich gefällt JSX etwas besser als Angular-Templates, insbesondere wegen der Code-Vervollständigung und Typüberprüfungsunterstützung. Das ist wirklich ein Killer-Feature. Angular hat jetzt den AOT-Compiler, der auch ein paar Dinge erkennen kann, die Codevervollständigung funktioniert auch für etwa die Hälfte der Sachen dort, aber es ist nicht annähernd so vollständig wie JSX/TypeScript.

GraphQL - Laden von Daten

Daher haben wir uns entschieden, GraphQL zum Speichern von Daten für diese Anwendung zu verwenden. Eine der einfachsten Möglichkeiten, ein GraphQL-Backend zu erstellen, ist die Verwendung von BaaS wie Graphcool. Das haben wir also getan. Im Grunde definieren Sie nur Modelle und Attribute, und Ihr CRUD ist startklar.

Gemeinsamer Kodex

Da ein Teil des GraphQL-bezogenen Codes für beide Implementierungen zu 100 % gleich ist, wiederholen wir ihn nicht zweimal:

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

GraphQL ist eine Abfragesprache, die darauf abzielt, im Vergleich zu klassischen RESTful-Endpunkten einen umfassenderen Funktionsumfang bereitzustellen. Lassen Sie uns diese spezielle Abfrage analysieren.

  • PostsQuery ist nur ein Name für diese Abfrage, auf den später verwiesen wird, sie kann beliebig benannt werden.
  • allPosts ist der wichtigste Teil - er verweist auf die Funktion, alle Datensätze mit dem `Post`-Modell abzufragen. Dieser Name wurde von Graphcool erstellt.
  • orderBy und first sind Parameter der Funktion allPosts . createdAt ist eines der Attribute des Post -Modells. first: 5 bedeutet, dass nur die ersten 5 Ergebnisse der Abfrage zurückgegeben werden.
  • id , name , title und message sind die Attribute des Post -Modells, die in das Ergebnis aufgenommen werden sollen. Andere Attribute werden herausgefiltert.

Wie Sie bereits sehen können, ist es ziemlich mächtig. Sehen Sie sich diese Seite an, um sich mit GraphQL-Abfragen vertraut zu machen.

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

Ja, als gute TypeScript-Bürger erstellen wir Schnittstellen für GraphQL-Ergebnisse.

Eckig

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

Die GraphQL-Abfrage ist ein RxJS-Observable, und wir abonnieren es. Es funktioniert ein bisschen wie ein Versprechen, aber nicht ganz, also haben wir kein Glück mit async/await . Natürlich gibt es noch toPromise, aber es scheint sowieso nicht der Angular-Weg zu sein. Wir setzen fetchPolicy: 'network-only' weil wir in diesem Fall die Daten nicht zwischenspeichern, sondern jedes Mal neu abrufen wollen.

Reagieren

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

Die React-Version ist fast identisch, aber da der apolloClient hier Promises verwendet, können wir die async/await -Syntax nutzen. Es gibt andere Ansätze in React, die die GraphQL-Abfragen einfach auf Komponenten höherer Ordnung „aufkleben“, aber es schien mir, dass die Daten- und Präsentationsebene ein bisschen zu stark vermischt wurden.

Zusammenfassung: Die Ideen des RxJS-Subscribe vs. Async/Await sind wirklich ziemlich gleich.

GraphQL - Speichern von Daten

Gemeinsamer Kodex

Wieder etwas GraphQL-bezogener Code:

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

Der Zweck von Mutationen besteht darin, Datensätze zu erstellen oder zu aktualisieren. Es ist daher vorteilhaft, einige Variablen mit der Mutation zu deklarieren, da auf diese Weise Daten übergeben werden. Wir haben also name - , title - und message -Variablen, die als String eingegeben werden und die wir jedes Mal füllen müssen, wenn wir diese Mutation aufrufen. Die createPost Funktion wird wiederum von Graphcool definiert. Wir geben an, dass die Schlüssel des Post -Modells Werte aus unseren Mutationsvariablen haben und dass nur die id des neu erstellten Post zurückgesendet werden soll.

Eckig

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

Beim Aufruf apollo.mutate müssen wir die aufgerufene Mutation und auch die Variablen angeben. Wir erhalten das Ergebnis im subscribe -Callback und verwenden den injizierten router , um zurück zur Post-Liste zu navigieren.

Reagieren

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

Sehr ähnlich wie oben, mit dem Unterschied einer eher „manuellen“ Abhängigkeitsinjektion und der Verwendung von async/await .

Zusammenfassung: Auch hier kein großer Unterschied. abonnieren vs. async/await ist im Grunde alles, was sich unterscheidet.

Formen

Wir möchten folgende Ziele mit Formularen in dieser Anwendung erreichen:

  • Datenbindung von Feldern an ein Modell
  • Validierungsmeldungen für jedes Feld, mehrere Regeln
  • Unterstützung bei der Überprüfung, ob das gesamte Formular gültig ist

Reagieren

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

Die Formstate-Bibliothek funktioniert also wie folgt: Für jedes Feld Ihres Formulars definieren Sie einen FieldState . Der übergebene Parameter ist der Anfangswert. Die Eigenschaft validators übernimmt eine Funktion, die „false“ zurückgibt, wenn der Wert gültig ist, und eine Validierungsmeldung, wenn der Wert nicht gültig ist. Mit den Hilfsfunktionen check und checkRequired kann das alles schön deklarativ aussehen.

Um die Validierung für das gesamte Formular zu haben, ist es vorteilhaft, diese Felder auch mit einer FormState Instanz zu umschließen, die dann die aggregierte Gültigkeit bereitstellt.

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

Die FormState Instanz stellt value -, onChange - und error -Eigenschaften bereit, die problemlos mit beliebigen Front-End-Komponenten verwendet werden können.

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