Angular vs. React: Web Geliştirme için Hangisi Daha İyi?
Yayınlanan: 2022-03-11Web geliştirme için React veya Angular'ın daha iyi bir seçim olup olmadığını tartışan sayısız makale var. Bir taneye daha ihtiyacımız var mı?
Bu makaleyi yazmamın nedeni, daha önce yayınlanmış makalelerin hiçbirinin - harika içgörüler içermelerine rağmen - pratik bir ön uç geliştiricinin hangisinin ihtiyaçlarına uygun olduğuna karar vermesi için yeterince derin olmamasıdır.
Bu makalede, Angular ve React'in çok farklı felsefelere sahip olsa da benzer ön uç sorunları nasıl çözmeyi amaçladığını ve birini mi yoksa diğerini mi seçmenin yalnızca kişisel bir tercih meselesi olduğunu öğreneceksiniz. Bunları karşılaştırmak için aynı uygulamayı bir kez Angular ve ardından tekrar React ile olmak üzere iki kez oluşturacağız.
Angular'ın Zamansız Duyurusu
İki yıl önce React Ekosistemi hakkında bir makale yazmıştım. Makale, diğer noktaların yanı sıra, Angular'ın “ön duyuru ile ölüm” kurbanı olduğunu savundu. O zamanlar, Angular ile hemen hemen her şey arasındaki seçim, projelerinin eski bir çerçeve üzerinde çalışmasını istemeyen herkes için kolaydı. Angular 1 eskiydi ve Angular 2 alfa sürümünde bile mevcut değildi.
Geriye dönüp bakıldığında, korkular aşağı yukarı haklı çıktı. Angular 2 çarpıcı biçimde değişti ve hatta son sürümden hemen önce büyük bir yeniden yazma sürecinden geçti.
İki yıl sonra, şu andan itibaren göreceli istikrar vaadiyle Angular 4'e sahibiz.
Şimdi ne olacak?
Angular ve React: Elmaları ve Portakalları Karşılaştırma
Bazı insanlar React ve Angular'ı karşılaştırmanın elmaları portakallarla karşılaştırmak gibi olduğunu söylüyor. Biri görünümlerle ilgilenen bir kütüphane iken, diğeri tam teşekküllü bir çerçevedir.
Elbette çoğu React geliştiricisi, onu eksiksiz bir çerçeveye dönüştürmek için React'e birkaç kitaplık ekleyecektir. Sonra tekrar, bu yığının ortaya çıkan iş akışı genellikle Angular'dan çok farklıdır, bu nedenle karşılaştırılabilirlik hala sınırlıdır.
En büyük fark devlet yönetiminde yatmaktadır. Angular, veri bağlama ile birlikte gelir, oysa React bugün tek yönlü veri akışı sağlamak ve değişmez verilerle çalışmak için genellikle Redux tarafından artırılır. Bunlar kendi başlarına karşıt yaklaşımlardır ve şimdi değiştirilebilir/veri bağlamanın değişmez/tek yönlü olmaktan daha iyi mi yoksa daha kötü mü olduğu konusunda sayısız tartışma devam etmektedir.
Bir Seviye Oyun Alanı
React'in hacklenmesi daha kolay olduğu için, bu karşılaştırmanın amacı için, kod parçacıklarının yan yana karşılaştırılmasına izin vermek için Angular'ı makul ölçüde yakından yansıtan bir React kurulumu oluşturmaya karar verdim.
Öne çıkan ancak varsayılan olarak React'te olmayan belirli Açısal özellikler şunlardır:
Özellik | açısal paket | Tepki kitaplığı |
---|---|---|
Veri bağlama, bağımlılık ekleme (DI) | @açısal/çekirdek | MobX |
Hesaplanan özellikler | rxjs | MobX |
Bileşen tabanlı yönlendirme | @açısal/yönlendirici | React Router v4 |
Malzeme tasarım bileşenleri | @açısal/malzeme | React Toolbox |
Bileşenlere göre kapsamlı CSS | @açısal/çekirdek | CSS modülleri |
Form doğrulamaları | @açısal/formlar | FormState |
proje üreticisi | @açısal/cli | React Komut Dosyaları TS |
Bağlanma verileri
Veri bağlama, tek yönlü yaklaşımdan başlamak için tartışmasız daha kolaydır. Tabii ki, tamamen zıt yönde gitmek ve React ile Redux veya mobx-durum ağacını ve Angular ile ngrx kullanmak mümkün olacaktır. Ama bu başka bir yazının konusu olacak.
Hesaplanan Özellikler
Performans söz konusu olsa da, Angular'daki düz alıcılar, her işlemede çağrıldıkları için söz konusu değildir. İşi yapan RsJS'den BehaviorSubject kullanmak mümkündür.
React ile, muhtemelen biraz daha güzel API ile aynı hedefe ulaşan MobX'ten @computed kullanmak mümkündür.
Bağımlılık Enjeksiyonu
Bağımlılık enjeksiyonu biraz tartışmalıdır çünkü mevcut React işlevsel programlama ve değişmezlik paradigmasına aykırıdır. Görünüşe göre, ayrı bir veri katmanı mimarisinin olmadığı durumlarda ayrıştırmaya (ve dolayısıyla alay etmeye ve test etmeye) yardımcı olduğu için, veri bağlama ortamlarında bir tür bağımlılık enjeksiyonu neredeyse vazgeçilmezdir.
DI'nin bir diğer avantajı (Angular'da desteklenir), farklı mağazaların farklı yaşam döngülerine sahip olma yeteneğidir. Mevcut React paradigmalarının çoğu, farklı bileşenlerle eşlenen bir tür küresel uygulama durumu kullanır, ancak deneyimlerime göre, bileşen bağlantısının kaldırılmasında genel durumu temizlerken hataların ortaya çıkması çok kolaydır.
Bileşen montajında oluşturulan bir mağazaya sahip olmak (ve bu bileşenin çocukları için sorunsuz bir şekilde kullanılabilir olmak) gerçekten yararlı ve genellikle gözden kaçan bir kavram gibi görünüyor.
Angular'da kullanıma hazır, ancak MobX ile de kolayca yeniden üretilebilir.
yönlendirme
Bileşen tabanlı yönlendirme, bileşenlerin tek bir büyük küresel yönlendirici yapılandırmasına sahip olmak yerine kendi alt rotalarını yönetmelerine olanak tanır. Bu yaklaşım nihayet sürüm 4'te react-router
sağladı.
Malzeme Tasarımı
Bazı üst düzey bileşenlerle başlamak her zaman iyidir ve malzeme tasarımı, Google dışı projelerde bile evrensel olarak kabul edilen bir varsayılan seçenek haline gelmiştir.
Materyal UI'nin bir sonraki sürümde çözmeyi planladıkları satır içi-CSS yaklaşımlarında ciddi performans sorunları olduğu için, genellikle önerilen Material UI yerine React Toolbox'ı bilinçli olarak seçtim.
Ayrıca React Toolbox'ta kullanılan PostCSS/cssnext zaten Sass/LESS'in yerini almaya başlıyor.
Kapsamlı CSS
CSS sınıfları, global değişkenler gibi bir şeydir. Çatışmaları önlemek için (BEM dahil) CSS'yi düzenlemeye yönelik çok sayıda yaklaşım vardır, ancak bir ön uç geliştiricinin ayrıntılı CSS adlandırma sistemleri tasarlamasına gerek kalmadan bu çakışmaları önlemek için CSS'yi işlemeye yardımcı olan kitaplıkları kullanma konusunda net bir güncel eğilim vardır.
Form Doğrulama
Form doğrulamaları önemsiz olmayan ve çok yaygın olarak kullanılan bir özelliktir. Kod tekrarını ve hataları önlemek için bunların bir kütüphane kapsamında olması güzel.
Proje Üreticisi
Bir proje için bir CLI oluşturucuya sahip olmak, GitHub'dan kazan plakalarını klonlamaktan biraz daha uygundur.
Aynı Uygulama, İki Kez Oluşturuldu
Yani aynı uygulamayı React ve Angular'da oluşturacağız. Muhteşem bir şey değil, sadece herkesin ortak bir sayfaya mesaj göndermesine izin veren bir Shoutboard.
Uygulamaları buradan deneyebilirsiniz:
- Shotboard Açısal
- Shotboard Tepki
Tüm kaynak koduna sahip olmak istiyorsanız GitHub'dan alabilirsiniz:
- Shotboard Açısal kaynak
- Shotboard React kaynağı
React uygulaması için de TypeScript kullandığımızı fark edeceksiniz. TypeScript'te tür denetiminin avantajları açıktır. Ve şimdi, içe aktarma, async/await ve dinlenme yayılımının daha iyi işlenmesi sonunda TypeScript 2'ye ulaştığından, Babel/ES7/Flow'u toz içinde bırakıyor.
Ayrıca her ikisine de Apollo Client ekleyelim çünkü GraphQL kullanmak istiyoruz. Demek istediğim, REST harika ama on yıl kadar sonra eskiyor.
Önyükleme ve Yönlendirme
İlk olarak, her iki uygulamanın giriş noktalarına bir göz atalım.
Açısal
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' }
Temel olarak uygulamada kullanmak istediğimiz tüm bileşenlerin bildirimlere gitmesi gerekiyor. Tüm üçüncü taraf kitaplıkları içe aktarılacak ve tüm küresel mağazalar sağlayıcılara. Alt bileşenler, daha fazla yerel öğe ekleme fırsatı ile tüm bunlara erişebilir.
Tepki
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') )
<Provider/>
bileşeni, MobX'te bağımlılık enjeksiyonu için kullanılır. React bileşenlerinin bunları daha sonra enjekte edebilmesi için depoları bağlama kaydeder. Evet, React bağlamı (tartışmalı olarak) güvenle kullanılabilir.
React sürümü biraz daha kısadır çünkü modül bildirimi yoktur - genellikle yalnızca içe aktarırsınız ve kullanıma hazırdır. Bazen bu tür bir katı bağımlılık istenmeyen bir durumdur (test), bu nedenle küresel singleton mağazaları için onlarca yıllık bu GoF modelini kullanmak zorunda kaldım:
export class AppStore { static instance: AppStore static getInstance() { return AppStore.instance || (AppStore.instance = new AppStore()) } @observable username = 'Mr. User' }
Angular's Router enjekte edilebilir, yani sadece bileşenlerden değil her yerden kullanılabilir. Aynısını tepkide elde etmek için mobx-react-router paketini kullanıyoruz ve routerStore
enjekte ediyoruz.
Özet: Her iki uygulamayı da önyüklemek oldukça basittir. React'in modüller yerine sadece import kullanmanın daha basit bir avantajı var, ancak daha sonra göreceğimiz gibi, bu modüller oldukça kullanışlı olabilir. Tekilleri manuel olarak yapmak biraz sıkıntı verici. Yönlendirme bildirimi sözdizimine gelince, JSON vs. JSX sadece bir tercih meselesidir.
Bağlantılar ve Zorunlu Gezinme
Yani bir rotayı değiştirmek için iki durum vardır. Bildirimsel, <a href...>
öğelerini kullanan ve zorunlu, yönlendirme (ve dolayısıyla konum) API'sini doğrudan çağıran.
Açısal
<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, hangi routerLink
etkin olduğunu otomatik olarak algılar ve stil verilebilmesi için uygun bir routerLinkActive
sınıfı koyar.
Yönlendirici, geçerli yolun gerektirdiği her şeyi işlemek için özel <router-outlet>
öğesini kullanır. Uygulamanın alt bileşenlerini daha derinlemesine incelediğimiz için birçok <router-outlet>
s olması mümkündür.
@Injectable() export class FormService { constructor(private router: Router) { } goBack() { this.router.navigate(['/posts']) } }
Yönlendirici modülü herhangi bir hizmete enjekte edilebilir (TypeScript türüne göre yarı sihirli bir şekilde), private
bildirim daha sonra açık atamaya gerek kalmadan örnekte saklar. URL'leri değiştirmek için navigate
yöntemini kullanın.
Tepki
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 ayrıca activeClassName
ile aktif bağlantının sınıfını da ayarlayabilir.
Burada, sınıf adını doğrudan sağlayamıyoruz, çünkü sınıf, CSS modül derleyicisi tarafından benzersiz hale getirildi ve style
yardımcısını kullanmamız gerekiyor. Daha sonra.
Yukarıda görüldüğü gibi, React Router bir <App>
öğesi içinde <Switch>
öğesini kullanır. <Switch>
öğesi yalnızca geçerli rotayı sarar ve bağlar, bu, geçerli bileşenin alt rotalarının yalnızca this.props.children
olduğu anlamına gelir. Yani bu da birleştirilebilir.
export class FormStore { routerStore: RouterStore constructor() { this.routerStore = RouterStore.getInstance() } goBack = () => { this.routerStore.history.push('/posts') } }
mobx-router-store
paketi aynı zamanda kolay enjeksiyon ve navigasyon sağlar.
Özet: Yönlendirmeye yönelik her iki yaklaşım da oldukça karşılaştırılabilir. Angular daha sezgisel görünüyor, React Router ise biraz daha basit bir şekilde birleştirilebilir.
Bağımlılık Enjeksiyonu
Veri katmanını sunum katmanından ayırmanın faydalı olduğu zaten kanıtlanmıştır. Burada DI ile elde etmeye çalıştığımız şey, veri katmanlarının bileşenlerinin (burada model/mağaza/hizmet olarak adlandırılır) görsel bileşenlerin yaşam döngüsünü takip etmesini sağlamak ve böylece global dokunmaya gerek kalmadan bu tür bileşenlerin bir veya daha fazla örneğini oluşturmaya izin vermektir. belirtmek, bildirmek. Ayrıca, uyumlu veri ve görselleştirme katmanlarını karıştırmak ve eşleştirmek mümkün olmalıdır.
Bu makaledeki örnekler çok basittir, bu nedenle tüm DI işleri aşırıya kaçmış gibi görünebilir, ancak uygulama büyüdükçe kullanışlı hale gelir.
Açısal
@Injectable() export class HomeService { message = 'Welcome to home page' counter = 0 increment() { this.counter++ } }
Böylece herhangi bir sınıf @injectable
yapılabilir ve özellikleri ve yöntemleri bileşenler için kullanılabilir hale getirilebilir.
@Component({ selector: 'app-home', templateUrl: './home.component.html', providers: [ HomeService ] }) export class HomeComponent { constructor( public homeService: HomeService, public appService: AppService, ) { } }
HomeService
bileşenin providers
kaydederek, onu yalnızca bu bileşen için kullanılabilir hale getiriyoruz. Bu artık bir tekil değil, ancak bileşenin her bir örneği, bileşenin montajında yeni olan yeni bir kopya alacak. Bu, önceki kullanımdan eski veri olmadığı anlamına gelir.
Buna karşılık, AppService
, app.module
(yukarıya bakın), bu nedenle bir tektir ve uygulamanın ömrü boyunca tüm bileşenler için aynı kalır. Bileşenlerden hizmetlerin yaşam döngüsünü kontrol edebilmek çok faydalı, ancak yeterince takdir edilmeyen bir kavramdır.
DI, hizmet örneklerini, TypeScript türleriyle tanımlanan bileşenin yapıcısına atayarak çalışır. Ek olarak, public
anahtar sözcükleri parametreleri otomatik olarak this
öğesine atar, böylece sıkıcı this.homeService = homeService
satırlarını artık yazmamıza gerek kalmaz.
<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>
Angular'ın şablon sözdizimi, tartışmasız oldukça zarif. 2 yönlü veri bağlama gibi çalışan [()]
kısayolunu seviyorum, ancak kaputun altında, aslında bir öznitelik bağlama + olayıdır. Hizmetlerimizin yaşam döngüsünün gerektirdiği gibi, /home
konumundan her uzaklaştığımızda homeService.counter
sıfırlanacak, ancak appService.username
kalır ve her yerden erişilebilir.
Tepki
import { observable } from 'mobx' export class HomeStore { @observable counter = 0 increment = () => { this.counter++ } }
MobX ile, gözlemlenebilir hale getirmek istediğimiz herhangi bir özelliğe @observable
dekoratörünü eklememiz gerekiyor.
@observer export class Home extends React.Component<any, any> { homeStore: HomeStore componentWillMount() { this.homeStore = new HomeStore() } render() { return <Provider homeStore={this.homeStore}> <HomeComponent /> </Provider> } }
Yaşam döngüsünü doğru yönetmek için Angular örneğinden biraz daha fazla çalışmamız gerekiyor. HomeStore
, her montajda yeni bir HomeComponent
örneği alan bir Provider
içine sarıyoruz.
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
, @observable
özelliklerindeki değişiklikleri dinlemek için @observer
dekoratörünü kullanır.
Bunun kaput altı mekanizması oldukça ilginç, o yüzden burada kısaca üzerinden geçelim. @observable
dekoratör, bir nesnedeki bir özelliği alıcı ve ayarlayıcı ile değiştirir, bu da çağrıları engellemesine izin verir. Bir @observer
artırılmış bileşeninin oluşturma işlevi çağrıldığında, bu özellik alıcıları çağrılır ve kendilerini çağıran bileşene bir referans tutarlar.
Daha sonra setter çağrıldığında ve değer değiştirildiğinde, özelliği son render'da kullanan bileşenlerin render fonksiyonları çağrılır. Artık hangi özelliklerin nerede kullanıldığına dair veriler güncelleniyor ve tüm döngü baştan başlayabilir.
Çok basit bir mekanizma ve aynı zamanda oldukça performanslı. Daha ayrıntılı açıklama burada.
@inject
dekoratörü, appStore
ve homeStore
örneklerini HomeComponent
sahne öğelerine enjekte etmek için kullanılır. Bu noktada, bu mağazaların her birinin farklı yaşam döngüsü vardır. appStore
, uygulamanın ömrü boyunca aynıdır, ancak homeStore
“/home” rotasına yapılan her navigasyonda yeni oluşturulur.
Bunun yararı, tüm mağazaların global olduğu durumda olduğu gibi, özellikleri manuel olarak temizlemenin gerekli olmamasıdır; bu, rotanın her seferinde tamamen farklı veriler içeren bir "ayrıntı" sayfası olması durumunda acı vericidir.
Özet: Angular'ın DI'sinin doğal bir özelliğinde sağlayıcı yaşam döngüsü yönetimi olarak, orada bunu başarmak elbette daha basittir. React versiyonu da kullanılabilir ancak çok daha fazla ortak bilgi içerir.
Hesaplanan Özellikler
Tepki
Bu konuda React ile başlayalım, daha basit bir çözümü var.
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` } }
Dolayısıyla, counter
bağlanan ve uygun şekilde çoğullaştırılmış bir mesaj döndüren hesaplanmış bir özelliğimiz var. counterMessage
sonucu önbelleğe alınır ve yalnızca counter
değiştiğinde yeniden hesaplanır.
<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>
Ardından, JSX şablonundan özelliğe (ve increment
yöntemine) başvururuz. Giriş alanı, bir değere bağlanarak ve appStore
bir yöntemin user olayını işlemesine izin vererek sürülür.
Açısal
Angular'da aynı etkiyi elde etmek için biraz daha yaratıcı olmamız gerekiyor.
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) } }
Hesaplanan bir özellik için temel teşkil eden tüm değerleri BehaviorSubject
olarak tanımlamamız gerekiyor. Hesaplanan özelliğin kendisi de bir BehaviorSubject
'dir, çünkü hesaplanan herhangi bir özellik, başka bir hesaplanmış özellik için girdi görevi görebilir.
Tabii ki, RxJS
çok daha fazlasını yapabilir, ancak bu tamamen farklı bir makalenin konusu olacaktır. Küçük dezavantajı, yalnızca hesaplanmış özellikler için RxJS'nin bu önemsiz kullanımının, tepki örneğinden biraz daha ayrıntılı olması ve abonelikleri manuel olarak yönetmeniz gerekir (burada yapıcıda olduğu gibi).
<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>
| async
ile RxJS konusuna nasıl başvurabileceğimize dikkat edin. | async
boru. Bu hoş bir dokunuş, bileşenlerinize abone olmanızdan çok daha kısa. input
bileşeni, [(ngModel)]
yönergesi tarafından çalıştırılır. Garip görünmesine rağmen, aslında oldukça zarif. appService.username
veri bağlaması ve kullanıcı girdi olayından otomatik olarak değer ataması için yalnızca sözdizimsel bir şeker.
Özet: Hesaplanan özelliklerin React/MobX'te uygulanması Angular/RxJS'den daha kolaydır, ancak RxJS daha sonra takdir edilebilecek bazı daha kullanışlı FRP özellikleri sağlayabilir.

Şablonlar ve CSS
Şablonlamanın birbirine karşı nasıl yığıldığını göstermek için, gönderilerin bir listesini görüntüleyen Gönderiler bileşenini kullanalım.
Açısal
@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() } }
Bu bileşen yalnızca HTML, CSS ve enjekte edilen hizmetleri birbirine bağlar ve ayrıca başlatma sırasında API'den gönderileri yüklemek için işlevi çağırır. AppService
, uygulama modülünde tanımlanan bir tekildir, PostsService
ise geçicidir ve her bileşen oluşturulduğunda yeni bir örnek oluşturulur. Bu bileşenden başvurulan CSS, bu bileşenin kapsamına girer; bu, içeriğin bileşen dışındaki hiçbir şeyi etkileyemeyeceği anlamına gelir.
<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>
HTML şablonunda, çoğunlukla Angular Material'daki bileşenlere başvururuz. Bunları kullanılabilir hale getirmek için, bunları app.module
içe aktarmalarına dahil etmek gerekiyordu (yukarıya bakın). *ngFor
yönergesi, her gönderi için md-card
bileşenini tekrarlamak için kullanılır.
Yerel CSS:
.mat-card { margin-bottom: 1rem; }
Yerel CSS, md-card
bileşeninde bulunan sınıflardan birini genişletir.
Genel CSS:
.float-right { float: right; }
Bu sınıf, tüm bileşenler için kullanılabilir hale getirmek için global style.css
dosyasında tanımlanmıştır. Standart yolla başvurulabilir, class="float-right"
.
Derlenmiş CSS:
.float-right { float: right; } .mat-card[_ngcontent-c1] { margin-bottom: 1rem; }
Derlenmiş CSS'de, [_ngcontent-c1]
öznitelik seçici kullanılarak yerel CSS'nin işlenen bileşene kapsamının belirlendiğini görebiliriz. Oluşturulan her Angular bileşeni, CSS kapsam belirleme amaçları için bunun gibi oluşturulmuş bir sınıfa sahiptir.
Bu mekanizmanın avantajı, sınıflara normal olarak başvurabilmemiz ve kapsam belirlemenin "başlık altında" ele alınmasıdır.
Tepki
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> } }
React'te, PostsStore
bağımlılığını "geçici" yapmak için yine Provider
yaklaşımını kullanmamız gerekiyor. Ayrıca, bu CSS dosyalarından sınıfları JSX'te kullanabilmek için style
ve appStyle
olarak adlandırılan CSS stillerini içe aktarıyoruz.
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> } }
Doğal olarak, JSX, zevkinize bağlı olarak iyi veya kötü bir şey olabilen Angular'ın HTML şablonlarından çok daha fazla JavaScript-y hissediyor. *ngFor
yönergesi yerine, gönderileri yinelemek için map
yapısını kullanırız.
Şimdi, Angular, TypeScript'i en çok öne çıkaran çerçeve olabilir, ancak aslında TypeScript'in gerçekten parladığı yer JSX. CSS modüllerinin eklenmesiyle (yukarıda içe aktarılmıştır), şablon kodlamanızı gerçekten kod tamamlama zenine dönüştürür. Her şey tip kontrollüdür. Bileşenler, nitelikler, hatta CSS sınıfları ( appStyle.floatRight
ve style.messageCard
, aşağıya bakın). Ve elbette, JSX'in yalın doğası, bileşenlere ve parçalara ayrılmayı Angular'ın şablonlarından biraz daha fazla teşvik eder.
Yerel CSS:
.messageCard { margin-bottom: 1rem; }
Genel CSS:
.floatRight { float: right; }
Derlenmiş CSS:
.floatRight__qItBM { float: right; } .messageCard__1Dt_9 { margin-bottom: 1rem; }
Gördüğünüz gibi, CSS Modülleri yükleyicisi, benzersizliği garanti eden rastgele bir son ek ile her CSS sınıfına son ekler. Çatışmaları önlemenin basit bir yolu. Sınıflara daha sonra web paketi içe aktarılan nesneler aracılığıyla başvurulur. Bunun olası bir dezavantajı, Angular örneğinde yaptığımız gibi, yalnızca bir sınıfla bir CSS oluşturup onu büyütememeniz olabilir. Öte yandan, bu aslında iyi bir şey olabilir, çünkü sizi stilleri düzgün bir şekilde kapsüllemeye zorlar.
Özet: Ben şahsen JSX'i Angular şablonlarından biraz daha çok seviyorum, özellikle kod tamamlama ve tip denetimi desteği nedeniyle. Bu gerçekten öldürücü bir özellik. Angular artık birkaç şeyi tespit edebilen AOT derleyicisine sahip, kod tamamlama da oradaki şeylerin yaklaşık yarısı için çalışıyor, ancak neredeyse JSX/TypeScript kadar eksiksiz değil.
GraphQL - Veri Yükleme
Bu nedenle, bu uygulama için verileri depolamak için GraphQL kullanmaya karar verdik. GraphQL arka uç oluşturmanın en kolay yollarından biri Graphcool gibi bazı BaaS kullanmaktır. Biz de öyle yaptık. Temel olarak, sadece modelleri ve nitelikleri tanımlarsınız ve CRUD'niz kullanıma hazırdır.
Ortak Kod
GraphQL ile ilgili kodlardan bazıları her iki uygulama için de %100 aynı olduğundan, bunu iki kez tekrarlamayalım:
const PostsQuery = gql` query PostsQuery { allPosts(orderBy: createdAt_DESC, first: 5) { id, name, title, message } } `
GraphQL, klasik RESTful uç noktalarına kıyasla daha zengin bir işlevsellik seti sağlamayı amaçlayan bir sorgu dilidir. Bu özel sorguyu inceleyelim.
-
PostsQuery
, bu sorgunun daha sonra başvurmak üzere yalnızca bir adıdır, herhangi bir ad verilebilir. -
allPosts
en önemli kısımdır - "Post" modeliyle tüm kayıtları sorgulama işlevine başvurur. Bu isim Graphcool tarafından oluşturuldu. -
orderBy
vefirst
,allPosts
işlevinin parametreleridir.createdAt
,Post
modelinin niteliklerinden biridir.first: 5
, sorgunun yalnızca ilk 5 sonucunu döndüreceği anlamına gelir. -
id
,name
,title
vemessage
sonuca dahil edilmesini istediğimizPost
modelinin özellikleridir. Diğer özellikler filtrelenecektir.
Zaten gördüğünüz gibi, oldukça güçlü. Kendinizi GraphQL sorgularıyla daha yakından tanımak için bu sayfaya göz atın.
interface Post { id: string name: string title: string message: string } interface PostsQueryResult { allPosts: Array<Post> }
Evet, iyi TypeScript vatandaşları olarak GraphQL sonuçları için arayüzler oluşturuyoruz.
Açısal
@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 }) } }
GraphQL sorgusu gözlemlenebilir bir RxJS'dir ve biz ona aboneyiz. Biraz söz gibi çalışıyor, ama tam olarak değil, bu yüzden async/await
kullanırken şansımız kalmadı. Tabii ki, hala söz var, ama yine de Angular yolu gibi görünmüyor. fetchPolicy: 'network-only'
ayarladık çünkü bu durumda verileri önbelleğe almak istemiyoruz, ancak her seferinde yeniden getiriyoruz.
Tepki
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 } }
React sürümü hemen hemen aynıdır, ancak burada apolloClient
vaatleri kullandığından, async/await
sözdiziminden yararlanabiliriz. React'te GraphQL sorgularını daha yüksek dereceli bileşenlere "bantlayan" başka yaklaşımlar da var, ancak bana veri ve sunum katmanını biraz fazla karıştırmak gibi geldi.
Özet: RxJS aboneliğine karşı async/await'in fikirleri gerçekten oldukça aynıdır.
GraphQL - Verileri Kaydetme
Ortak Kod
Yine, GraphQL ile ilgili bazı kodlar:
const AddPostMutation = gql` mutation AddPostMutation($name: String!, $title: String!, $message: String!) { createPost( name: $name, title: $title, message: $message ) { id } } `
Mutasyonların amacı, kayıtları oluşturmak veya güncellemektir. Bu nedenle, mutasyona sahip bazı değişkenleri bildirmek faydalıdır çünkü bunlar, içine veri aktarmanın yolu budur. Dolayısıyla, bu mutasyonu her çağırdığımızda doldurmamız gereken, String
olarak yazılan name
, title
ve message
değişkenlerimiz var. createPost
işlevi yine Graphcool tarafından tanımlanır. Post
modelinin anahtarlarının mutasyon değişkenlerinden değerler alacağını ve karşılığında sadece yeni oluşturulan Post'un id
gönderilmesini istediğimizi belirtiyoruz.
Açısal
@Injectable() export class FormService { constructor( private apollo: Apollo, private router: Router, private appService: AppService ) { } addPost(value) { this.apollo.mutate({ mutation: AddPostMutation, variables: { name: this.appService.username, title: value.title, message: value.message } }).subscribe(({ data }) => { this.router.navigate(['/posts']) }, (error) => { console.log('there was an error sending the query', error) }) } }
apollo.mutate
, dediğimiz mutasyonu ve değişkenleri de sağlamamız gerekiyor. Geri arama subscribe
sonucu alıyoruz ve gönderi listesine geri dönmek için enjekte edilen router
kullanıyoruz.
Tepki
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') } }
Daha "manuel" bağımlılık enjeksiyonu ve async/await
kullanımı farkıyla yukarıdakine çok benzer.
Özet: Yine, burada pek bir fark yok. abone ol vs. async/await temelde farklı olan tek şeydir.
Formlar
Bu uygulamada formlarla aşağıdaki hedeflere ulaşmak istiyoruz:
- Alanların bir modele veri bağlaması
- Her alan için doğrulama mesajları, çoklu kurallar
- Tüm formun geçerli olup olmadığını kontrol etme desteği
Tepki
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 }) }
Böylece formstate kitaplığı şu şekilde çalışır: Formunuzun her alanı için bir FieldState
tanımlarsınız. Geçirilen parametre başlangıç değeridir. validators
özelliği, değer geçerli olduğunda "false" ve değer geçerli olmadığında bir doğrulama mesajı döndüren bir işlev alır. check
ve checkRequired
yardımcı işlevleriyle, hepsi güzel bir şekilde bildirimsel görünebilir.
Tüm formun doğrulanmasına sahip olmak için, bu alanları daha sonra toplam geçerliliği sağlayan bir FormState
örneğiyle sarmak da yararlıdır.
@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} />
FormState
örneği, herhangi bir ön uç bileşeniyle kolayca kullanılabilen value
, onChange
ve error
özellikleri sağlar.
<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.