Angular vs. React: co jest lepsze do tworzenia stron internetowych?

Opublikowany: 2022-03-11

Istnieje niezliczona ilość artykułów, w których dyskutuje się, czy React czy Angular to lepszy wybór do tworzenia stron internetowych. Czy potrzebujemy jeszcze jednego?

Powodem, dla którego napisałem ten artykuł, jest to, że żaden z opublikowanych już artykułów – chociaż zawierają świetne spostrzeżenia – nie jest wystarczająco dogłębny, aby praktyczny programista front-end mógł zdecydować, który z nich może odpowiadać jego potrzebom.

W tym artykule dowiesz się, w jaki sposób Angular i React mają na celu rozwiązywanie podobnych problemów front-endowych, choć przy użyciu bardzo różnych filozofii, i czy wybór jednej lub drugiej jest tylko kwestią osobistych preferencji. Aby je porównać, zbudujemy tę samą aplikację dwa razy, raz za pomocą Angulara, a potem ponownie za pomocą Reacta.

Nieterminowe ogłoszenie Angulara

Dwa lata temu napisałem artykuł o React Ecosystem. W artykule argumentowano między innymi, że Angular padł ofiarą „śmierci przez zapowiedź”. W tamtych czasach wybór między Angularem a prawie wszystkim innym był łatwy dla każdego, kto nie chciał, aby jego projekt działał na przestarzałym frameworku. Angular 1 był przestarzały, a Angular 2 nie był nawet dostępny w wersji alfa.

Z perspektywy czasu obawy były mniej lub bardziej uzasadnione. Angular 2 zmienił się diametralnie, a nawet przeszedł gruntowną przeróbkę tuż przed ostatecznym wydaniem.

Dwa lata później mamy Angular 4 z obietnicą względnej stabilności odtąd.

Co teraz?

Angular vs. React: porównanie jabłek i pomarańczy

Niektórzy twierdzą, że porównywanie Reacta i Angulara jest jak porównywanie jabłek do pomarańczy. Podczas gdy jedna jest biblioteką zajmującą się widokami, druga jest pełnoprawnym frameworkiem.

Oczywiście większość programistów Reacta doda kilka bibliotek do Reacta, aby przekształcić go w kompletny framework. Z drugiej strony, wynikowy przepływ pracy tego stosu często nadal bardzo różni się od Angulara, więc porównywalność jest nadal ograniczona.

Największa różnica polega na zarządzaniu państwem. Angular jest dostarczany z dołączonym wiązaniem danych, podczas gdy obecnie React jest zwykle rozszerzany przez Redux, aby zapewnić jednokierunkowy przepływ danych i pracować z niezmiennymi danymi. Są to przeciwstawne podejścia same w sobie, a teraz toczą się niezliczone dyskusje na temat tego, czy zmienne/wiązanie danych jest lepsze czy gorsze niż niezmienne/jednokierunkowe.

Równe pole gry

Ponieważ React jest znany z łatwiejszego hackowania, zdecydowałem, aby na potrzeby tego porównania zbudować konfigurację React, która dość dokładnie odzwierciedla Angulara, aby umożliwić równoległe porównanie fragmentów kodu.

Niektóre funkcje Angulara, które wyróżniają się, ale domyślnie nie są dostępne w React to:

Funkcja Pakiet kątowy Biblioteka reakcji
Wiązanie danych, wstrzykiwanie zależności (DI) @kątowy/rdzeń MobX
Obliczone właściwości rxjs MobX
Routing oparty na komponentach @kątowy/router React Router v4
Elementy konstrukcyjne materiałów @kątowe/materiałowe Reaguj przybornik
CSS ograniczony do komponentów @kątowy/rdzeń Moduły CSS
Walidacje formularzy @kątowe/formy Stan formularza
Generator projektów @kątowe/cli Reaguj skrypty TS

Wiązanie danych

Powiązanie danych jest prawdopodobnie łatwiejsze niż podejście jednokierunkowe. Oczywiście można by pójść w zupełnie odwrotnym kierunku i użyć Reduxa lub mobx-state-tree z Reactem, a ngrx z Angularem. Ale to byłby temat na inny post.

Obliczone właściwości

Jeśli chodzi o wydajność, zwykłe gettery w Angular po prostu nie wchodzą w rachubę, ponieważ są wywoływane przy każdym renderowaniu. Możliwe jest użycie BehaviorSubject z RsJS, który wykonuje zadanie.

Dzięki React możliwe jest użycie @computed z MobX, który osiąga ten sam cel, z prawdopodobnie nieco ładniejszym interfejsem API.

Wstrzykiwanie zależności

Wstrzykiwanie zależności jest dość kontrowersyjne, ponieważ jest sprzeczne z obecnym paradygmatem React dotyczącym programowania funkcjonalnego i niezmienności. Jak się okazuje, pewien rodzaj wstrzykiwania zależności jest prawie niezbędny w środowiskach wiążących dane, ponieważ pomaga w odsprzęganiu (a tym samym mockowaniu i testowaniu) tam, gdzie nie ma oddzielnej architektury warstwy danych.

Kolejną zaletą DI (obsługiwanej w Angular) jest możliwość posiadania różnych cykli życia różnych sklepów. Większość obecnych paradygmatów Reacta używa pewnego rodzaju globalnego stanu aplikacji, który mapuje do różnych komponentów, ale z mojego doświadczenia wynika, że ​​bardzo łatwo jest wprowadzić błędy podczas czyszczenia stanu globalnego po odmontowaniu komponentu.

Posiadanie sklepu, który jest tworzony po zamontowaniu komponentu (i jest bezproblemowo dostępny dla dzieci tego komponentu) wydaje się być naprawdę użytecznym i często pomijanym pomysłem.

Po wyjęciu z pudełka w Angularze, ale dość łatwo odtwarzalny również z MobX.

Rozgromienie

Routing oparty na komponentach umożliwia komponentom zarządzanie własnymi trasami podrzędnymi zamiast posiadania jednej dużej globalnej konfiguracji routera. Takie podejście w końcu dotarło do react-router w wersji 4.

Wygląd materiału

Zawsze fajnie jest zacząć od niektórych komponentów wyższego poziomu, a projektowanie materiałów stało się czymś w rodzaju powszechnie akceptowanego wyboru domyślnego, nawet w projektach innych niż Google.

Celowo wybrałem React Toolbox zamiast zwykle zalecanego interfejsu Material UI, ponieważ Material UI ma poważne problemy z wydajnością związane z podejściem inline-CSS, które planują rozwiązać w następnej wersji.

Poza tym PostCSS/cssnext używany w React Toolbox zaczyna i tak zastępować Sass/LESS.

CSS w zakresie

Klasy CSS są czymś w rodzaju zmiennych globalnych. Istnieje wiele podejść do organizowania CSS w celu zapobiegania konfliktom (w tym BEM), ale istnieje wyraźna tendencja do korzystania z bibliotek, które pomagają przetwarzać CSS w celu zapobiegania tym konfliktom bez potrzeby opracowywania przez programistę front-end skomplikowanych systemów nazewnictwa CSS.

Walidacja formularza

Walidacja formularzy to nietrywialna i bardzo szeroko stosowana funkcja. Dobrze mieć te objęte biblioteką, aby zapobiec powtarzaniu kodu i błędom.

Generator projektów

Posiadanie generatora CLI dla projektu jest nieco wygodniejsze niż klonowanie szablonów z GitHub.

Ta sama aplikacja, zbudowana dwa razy

Dlatego stworzymy tę samą aplikację w React i Angular. Nic spektakularnego, po prostu Shoutboard, który pozwala każdemu publikować wiadomości na wspólnej stronie.

Możesz wypróbować aplikacje tutaj:

  • Deskorolka kątowa
  • Reaguj na tablicę

Aplikacja do tabliczki graficznej

Jeśli chcesz mieć cały kod źródłowy, możesz go pobrać z GitHub:

  • Źródło kątowe tablicy rozdzielczej
  • Źródło reakcji na tablicę

Zauważysz, że użyliśmy również TypeScript dla aplikacji React. Zalety sprawdzania typu w TypeScript są oczywiste. A teraz, gdy w TypeScript 2 pojawiła się lepsza obsługa importu, async/await i rest, co pozostawia Babel/ES7/Flow w kurzu.

Dodajmy również klienta Apollo do obu, ponieważ chcemy używać GraphQL. To znaczy, REST jest świetny, ale po mniej więcej dekadzie starzeje się.

Bootstrap i routing

Najpierw przyjrzyjmy się punktom wejścia obu aplikacji.

Kątowy

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

W zasadzie wszystkie komponenty, których chcemy użyć w aplikacji, muszą przejść do deklaracji. Wszystkie biblioteki innych firm do importu i wszystkie sklepy globalne do dostawców. Komponenty dla dzieci mają dostęp do tego wszystkiego, z możliwością dodania większej ilości lokalnych rzeczy.

Reagować

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

Komponent <Provider/> jest używany do wstrzykiwania zależności w MobX. Zapisuje sklepy w kontekście, aby komponenty React mogły je później wstrzyknąć. Tak, kontekst React może być (prawdopodobnie) bezpiecznie używany.

Wersja React jest nieco krótsza, ponieważ nie ma deklaracji modułów - zwykle wystarczy importować i jest gotowa do użycia. Czasami tego rodzaju twarda zależność jest niepożądana (testowanie), więc dla globalnych sklepów singleton musiałem użyć tego kilkudziesięcioletniego wzorca GoF:

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

Router Angulara można wstrzykiwać, dzięki czemu można go używać z dowolnego miejsca, nie tylko z komponentów. Aby osiągnąć to samo w React, używamy pakietu mobx-react-router i wstrzykujemy routerStore .

Podsumowanie: Bootstrapowanie obu aplikacji jest dość proste. React ma tę przewagę, że jest prostszy, używając tylko importów zamiast modułów, ale, jak zobaczymy później, te moduły mogą być całkiem przydatne. Ręczne robienie singli jest trochę uciążliwe. Jeśli chodzi o składnię deklaracji routingu, JSON vs. JSX to tylko kwestia preferencji.

Linki i imperatywna nawigacja

Tak więc są dwa przypadki zmiany trasy. Deklaratywny, wykorzystujący elementy <a href...> i imperatywny, wywołujący bezpośrednio API routingu (a tym samym lokalizacji).

Kątowy

 <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 automatycznie wykrywa, który routerLink jest aktywny i umieszcza na nim odpowiednią klasę routerLinkActive , dzięki czemu można go stylizować.

Router używa specjalnego elementu <router-outlet> do renderowania tego, co nakazuje bieżąca ścieżka. Możliwe jest posiadanie wielu <router-outlet> , ponieważ zagłębiamy się w podkomponenty aplikacji.

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

Moduł routera może zostać wstrzyknięty do dowolnej usługi (w połowie magicznie przez jego typ TypeScript), a następnie private deklaracja przechowuje go w instancji bez konieczności jawnego przypisywania. Użyj metody navigate , aby przełączyć adresy URL.

Reagować

 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 może również ustawić klasę aktywnego łącza za pomocą activeClassName .

W tym przypadku nie możemy podać nazwy klasy bezpośrednio, ponieważ została ona stworzona przez kompilator modułów CSS i musimy użyć helpera style . Więcej o tym później.

Jak widać powyżej, React Router używa elementu <Switch> wewnątrz elementu <App> . Ponieważ element <Switch> po prostu zawija i montuje bieżącą trasę, oznacza to, że pod-trasy bieżącego komponentu to po prostu this.props.children . Więc to też można komponować.

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

Pakiet mobx-router-store umożliwia również łatwe wstrzykiwanie i nawigację.

Podsumowanie: Oba podejścia do routingu są dość porównywalne. Angular wydaje się być bardziej intuicyjny, podczas gdy React Router ma nieco prostszą możliwość komponowania.

Wstrzykiwanie zależności

Udowodniono już, że korzystne jest oddzielenie warstwy danych od warstwy prezentacji. To, co staramy się tutaj osiągnąć za pomocą DI, to sprawić, aby komponenty warstw danych (tutaj nazywane modelem/store/service) podążały za cyklem życia komponentów wizualnych, a tym samym umożliwić wykonanie jednej lub wielu instancji takich komponentów bez konieczności dotykania globalnych stan. Ponadto powinno być możliwe mieszanie i dopasowywanie kompatybilnych warstw danych i wizualizacji.

Przykłady w tym artykule są bardzo proste, więc wszystkie rzeczy związane z DI mogą wydawać się przesadą, ale przydaje się, gdy aplikacja się rozrasta.

Kątowy

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

Tak więc każdą klasę można uczynić @injectable , a jej właściwości i metody udostępnić komponentom.

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

Rejestrując HomeService u providers komponentu, udostępniamy go wyłącznie temu komponentowi. Nie jest to teraz singleton, ale każda instancja komponentu otrzyma nową kopię, świeżą na montowaniu komponentu. Oznacza to brak przestarzałych danych z poprzedniego użycia.

W przeciwieństwie do tego, AppService został zarejestrowany w app.module (patrz wyżej), więc jest singletonem i pozostaje taki sam dla wszystkich komponentów, przez cały czas życia aplikacji. Możliwość kontrolowania cyklu życia usług z komponentów to bardzo przydatna, ale niedoceniana koncepcja.

DI działa, przypisując wystąpienia usługi do konstruktora składnika, identyfikowanego przez typy TypeScript. Dodatkowo public słowa kluczowe automatycznie przypisują parametry do this , dzięki czemu nie musimy już pisać tych nudnych 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>

Składnia szablonu Angulara, prawdopodobnie dość elegancka. Podoba mi się skrót [()] , który działa jak dwukierunkowe wiązanie danych, ale pod maską jest to właściwie wiązanie atrybutu + zdarzenie. Zgodnie z cyklem życia naszych usług, homeService.counter zostanie zresetowany za każdym razem, gdy opuścimy /home , ale nazwa appService.username pozostanie i będzie dostępna z dowolnego miejsca.

Reagować

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

W MobX musimy dodać dekorator @observable do każdej właściwości, którą chcemy, aby była obserwowalna.

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

Aby prawidłowo zarządzać cyklem życia, musimy wykonać nieco więcej pracy niż w przykładzie Angulara. Zawijamy HomeComponent wewnątrz Provider , który otrzymuje nową instancję HomeStore na każdym montowaniu.

 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 używa dekoratora @observer do nasłuchiwania zmian we właściwościach @observable .

Mechanizm tego pod maską jest dość interesujący, więc przyjrzyjmy się temu pokrótce tutaj. Dekorator @observable zastępuje właściwość w obiekcie getterem i setterem, co pozwala mu przechwytywać wywołania. Kiedy funkcja renderowania rozszerzonego komponentu @observer jest wywoływana, te właściwości są wywoływane i zachowują odniesienie do komponentu, który je wywołał.

Następnie, po wywołaniu settera i zmianie wartości, wywoływane są funkcje renderowania komponentów, które używały właściwości podczas ostatniego renderowania. Teraz dane o tym, które właściwości są używane, gdzie są aktualizowane, i cały cykl można rozpocząć od nowa.

Bardzo prosty mechanizm, a do tego całkiem wydajny. Więcej szczegółowych wyjaśnień tutaj.

Dekorator @inject służy do wstrzykiwania instancji appStore i homeStore do właściwości HomeComponent . W tym momencie każdy z tych sklepów ma inny cykl życia. appStore działa tak samo w czasie życia aplikacji, ale homeStore jest tworzony na nowo przy każdej nawigacji do trasy „/home”.

Zaletą tego jest to, że nie jest konieczne ręczne czyszczenie właściwości, jak to ma miejsce, gdy wszystkie sklepy są globalne, co jest uciążliwe, jeśli trasa jest stroną „szczegółów”, która za każdym razem zawiera zupełnie inne dane.

Podsumowanie: Ponieważ zarządzanie cyklem życia dostawcy jest nieodłączną cechą DI Angulara, oczywiście łatwiej jest to osiągnąć. Wersja React jest również użyteczna, ale zawiera znacznie więcej schematów.

Obliczone właściwości

Reagować

Zacznijmy od Reacta, który ma prostsze rozwiązanie.

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

Mamy więc obliczoną właściwość, która wiąże się z counter i zwraca wiadomość w liczbie mnogiej. Wynik counterMessage jest buforowany i przeliczany tylko w przypadku zmiany counter .

 <Input type='text' label='Edit your name' name='username' value={appStore.username} onChange={appStore.onUsernameChange} /> <span>{homeStore.counterMessage}</span> <button onClick={homeStore.increment}>Click!</button>

Następnie odwołujemy się do właściwości (i metody increment ) z szablonu JSX. Pole wejściowe jest sterowane przez powiązanie z wartością i zezwolenie na obsługę zdarzenia użytkownika przez metodę z appStore .

Kątowy

Aby osiągnąć ten sam efekt w Angularze, musimy być nieco bardziej pomysłowi.

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

Musimy zdefiniować wszystkie wartości, które służą jako podstawa obliczonej właściwości jako BehaviorSubject . Sama obliczona właściwość jest również BehaviorSubject , ponieważ każda obliczona właściwość może służyć jako dane wejściowe dla innej obliczonej właściwości.

Oczywiście RxJS potrafi znacznie więcej niż tylko to, ale byłby to temat na zupełnie inny artykuł. Niewielkim minusem jest to, że to trywialne użycie RxJS tylko dla obliczonych właściwości jest nieco bardziej szczegółowe niż przykład React i musisz ręcznie zarządzać subskrypcjami (jak tutaj w konstruktorze).

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

Zwróć uwagę, jak możemy odwołać się do tematu RxJS za pomocą | async rura | async . To miły akcent, znacznie krótszy niż konieczność subskrypcji komponentów. Składnik input jest sterowany przez dyrektywę [(ngModel)] . Pomimo tego, że wygląda dziwnie, jest całkiem elegancki. Po prostu cukierek składniowy do wiązania danych wartości z appService.username i automatycznego przypisywania wartości ze zdarzenia wejściowego użytkownika.

Podsumowanie: Obliczone właściwości są łatwiejsze do zaimplementowania w React/MobX niż w Angular/RxJS, ale RxJS może zapewnić kilka bardziej przydatnych funkcji FRP, które mogą być docenione później.

Szablony i CSS

Aby pokazać, jak szablony układają się względem siebie, użyjmy komponentu Posty, który wyświetla listę postów.

Kątowy

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

Ten składnik po prostu łączy ze sobą HTML, CSS i wstrzyknięte usługi, a także wywołuje funkcję do ładowania postów z interfejsu API podczas inicjalizacji. AppService to singleton zdefiniowany w module aplikacji, podczas gdy PostsService jest przejściowy, z nowym wystąpieniem tworzonym za każdym razem, gdy tworzony jest składnik. CSS, do którego odwołuje się ten składnik, jest objęty zakresem tego składnika, co oznacza, że ​​zawartość nie może wpływać na nic poza składnikiem.

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

W szablonie HTML odwołujemy się głównie do komponentów z Angular Material. Aby były dostępne, konieczne było uwzględnienie ich w imporcie app.module (patrz wyżej). Dyrektywa *ngFor jest używana do powtarzania komponentu md-card dla każdego postu.

Lokalny CSS:

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

Lokalny CSS tylko rozszerza jedną z klas obecnych w komponencie md-card .

Globalny CSS:

 .float-right { float: right; }

Ta klasa jest zdefiniowana w globalnym pliku style.css , aby była dostępna dla wszystkich komponentów. Można się do niego odwoływać w standardowy sposób, class="float-right" .

Skompilowany CSS:

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

W skompilowanym CSS możemy zobaczyć, że lokalny CSS został ograniczony do renderowanego komponentu za pomocą selektora atrybutu [_ngcontent-c1] . Każdy renderowany komponent Angular ma wygenerowaną klasę taką jak ta dla celów określania zakresu CSS.

Zaletą tego mechanizmu jest to, że możemy normalnie odwoływać się do klas, a ustalanie zakresu odbywa się „pod maską”.

Reagować

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

W React ponownie musimy użyć podejścia Provider , aby zależność PostsStore „przejściowa”. Importujemy również style CSS, określane jako style i appStyle , aby móc używać klas z tych plików CSS w 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> } }

Oczywiście JSX działa znacznie bardziej w JavaScript niż szablony HTML Angulara, co może być dobre lub złe, w zależności od upodobań. Zamiast dyrektywy *ngFor używamy konstrukcji map do iteracji po postach.

Teraz Angular może być frameworkiem, który najbardziej zachwala TypeScript, ale w rzeczywistości to JSX naprawdę błyszczy. Dzięki dodaniu modułów CSS (zaimportowanych powyżej) naprawdę zmienia kodowanie szablonu w zen uzupełniania kodu. Każda rzecz jest sprawdzana pod kątem typu. Komponenty, atrybuty, a nawet klasy CSS ( appStyle.floatRight i style.messageCard , patrz poniżej). I oczywiście szczupła natura JSX zachęca do dzielenia na komponenty i fragmenty nieco bardziej niż szablony Angulara.

Lokalny CSS:

 .messageCard { margin-bottom: 1rem; }

Globalny CSS:

 .floatRight { float: right; }

Skompilowany CSS:

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

Jak widać, moduł ładujący moduły CSS dodaje do każdej klasy CSS losowy przyrostek, co gwarantuje unikalność. Prosty sposób na uniknięcie konfliktów. Klasy są następnie przywoływane przez obiekty zaimportowane z pakietu webpack. Jedną z możliwych wad tego może być to, że nie można po prostu stworzyć CSS z klasą i ją rozszerzyć, jak to zrobiliśmy w przykładzie Angulara. Z drugiej strony może to być naprawdę dobra rzecz, ponieważ zmusza cię do właściwego enkapsulacji stylów.

Podsumowanie: Osobiście lubię JSX trochę bardziej niż szablony Angular, szczególnie ze względu na wsparcie uzupełniania kodu i sprawdzania typu. To naprawdę zabójcza funkcja. Angular ma teraz kompilator AOT, który również może wykryć kilka rzeczy, uzupełnianie kodu działa również dla około połowy rzeczy, ale nie jest tak kompletne jak JSX/TypeScript.

GraphQL — ładowanie danych

Dlatego zdecydowaliśmy się użyć GraphQL do przechowywania danych dla tej aplikacji. Jednym z najłatwiejszych sposobów tworzenia zaplecza GraphQL jest użycie niektórych BaaS, takich jak Graphcool. Więc to właśnie zrobiliśmy. Zasadniczo po prostu definiujesz modele i atrybuty, a twoje CRUD jest gotowe.

Wspólny kod

Ponieważ część kodu związanego z GraphQL jest w 100% taka sama dla obu implementacji, nie powtarzajmy tego dwa razy:

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

GraphQL to język zapytań mający na celu zapewnienie bogatszego zestawu funkcjonalności w porównaniu z klasycznymi punktami końcowymi RESTful. Przeanalizujmy to konkretne zapytanie.

  • PostsQuery to tylko nazwa, do której zapytanie będzie się odwoływać później, może mieć dowolną nazwę.
  • allPosts jest najważniejszą częścią - odwołuje się do funkcji, która odpytuje wszystkie rekordy za pomocą modelu `Post`. Ta nazwa została stworzona przez Graphcool.
  • orderBy i first to parametry funkcji allPosts . createdAt to jeden z atrybutów modelu Post . first: 5 oznacza, że ​​zwróci tylko 5 pierwszych wyników zapytania.
  • id , name , title i message to atrybuty modelu Post , które chcemy uwzględnić w wyniku. Inne atrybuty zostaną odfiltrowane.

Jak już widać, jest dość potężny. Sprawdź tę stronę, aby lepiej zapoznać się z zapytaniami GraphQL.

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

Tak, jako dobrzy obywatele TypeScriptu, tworzymy interfejsy dla wyników GraphQL.

Kątowy

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

Zapytanie GraphQL jest obserwowalnym RxJS i subskrybujemy je. Działa to trochę jak obietnica, ale nie do końca, więc nie mamy szczęścia przy użyciu async/await . Oczywiście wciąż jest do Obietnicy, ale i tak nie wygląda to na Angulara. Ustawiamy fetchPolicy: 'network-only' ponieważ w tym przypadku nie chcemy buforować danych, ale za każdym razem pobieramy ponownie.

Reagować

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

Wersja React jest prawie identyczna, ale ponieważ apolloClient używa tutaj obietnic, możemy skorzystać ze składni async/await . Istnieją inne podejścia w React, które po prostu „przyklejają” zapytania GraphQL do komponentów wyższego rzędu, ale wydawało mi się, że mieszanie razem warstwy danych i prezentacji jest odrobinę za dużo.

Podsumowanie: Idee subskrypcji RxJS i async/await są naprawdę takie same.

GraphQL - Zapisywanie danych

Wspólny kod

Ponownie, trochę kodu związanego z GraphQL:

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

Celem mutacji jest tworzenie lub aktualizacja rekordów. Dlatego korzystne jest zadeklarowanie niektórych zmiennych z mutacją, ponieważ w ten sposób przekazuje się do niej dane. Mamy więc zmienne name , title i message wpisane jako String , które musimy wypełnić za każdym razem, gdy wywołamy tę mutację. Funkcja createPost ponownie jest zdefiniowana przez Graphcool. Określamy, że klucze modelu Post będą miały wartości z naszych zmiennych mutacji, a także, że chcemy, aby w zamian został przesłany tylko id nowo utworzonego Posta.

Kątowy

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

Wywołując apollo.mutate , musimy podać wywołaną przez nas mutację oraz zmienne. Otrzymujemy wynik w wywołaniu zwrotnym subscribe i używamy wstrzykniętego router , aby przejść z powrotem do listy postów.

Reagować

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

Bardzo podobny do powyższego, z różnicą bardziej „ręcznego” wstrzykiwania zależności i użycia async/await .

Podsumowanie: Znowu nie ma tu dużej różnicy. Subskrybuj vs. asynchroniczny/await to w zasadzie wszystko, co się różni.

Formularze

Za pomocą formularzy w tej aplikacji chcemy osiągnąć następujące cele:

  • Wiązanie danych pól z modelem
  • Komunikaty walidacyjne dla każdego pola, wiele reguł
  • Wsparcie dla sprawdzenia poprawności całego formularza

Reagować

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

Tak więc biblioteka formstate działa w następujący sposób: Dla każdego pola formularza definiujesz FieldState . Przekazywany parametr jest wartością początkową. Właściwość validators przyjmuje funkcję, która zwraca „false”, gdy wartość jest poprawna, oraz komunikat walidacji, gdy wartość jest nieprawidłowa. Dzięki funkcjom pomocniczym check i checkRequired wszystko może wyglądać ładnie deklaratywnie.

Aby mieć walidację dla całego formularza, warto również otoczyć te pola instancją FormState , która następnie zapewnia poprawność zagregowaną.

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

Instancja FormState udostępnia właściwości value , onChange i error , których można łatwo używać z dowolnymi komponentami frontonu.

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