Angular vs. React: Mana yang Lebih Baik untuk Pengembangan Web?
Diterbitkan: 2022-03-11Ada banyak artikel di luar sana yang memperdebatkan apakah React atau Angular adalah pilihan yang lebih baik untuk pengembangan web. Apakah kita masih membutuhkan yang lain?
Alasan saya menulis artikel ini adalah karena tidak ada artikel yang diterbitkan—walaupun mengandung wawasan yang luar biasa—cukup mendalam bagi pengembang front-end yang praktis untuk memutuskan mana yang sesuai dengan kebutuhan mereka.
Dalam artikel ini, Anda akan mempelajari bagaimana Angular dan React keduanya bertujuan untuk memecahkan masalah front-end yang serupa meskipun dengan filosofi yang sangat berbeda, dan apakah memilih satu atau yang lain hanyalah masalah preferensi pribadi. Untuk membandingkannya, kita akan membangun aplikasi yang sama dua kali, sekali dengan Angular dan sekali lagi dengan React.
Pengumuman Angular yang Tidak Tepat Waktu
Dua tahun lalu, saya menulis artikel tentang React Ecosystem. Di antara poin-poin lain, artikel tersebut berpendapat bahwa Angular telah menjadi korban "kematian melalui pengumuman sebelumnya." Saat itu, pilihan antara Angular dan hampir semua hal lainnya adalah pilihan yang mudah bagi siapa saja yang tidak ingin proyek mereka berjalan pada kerangka kerja yang sudah usang. Angular 1 sudah usang, dan Angular 2 bahkan tidak tersedia dalam versi alpha.
Kalau dipikir-pikir, ketakutan itu kurang lebih dibenarkan. Angular 2 berubah secara dramatis dan bahkan mengalami penulisan ulang besar sebelum rilis final.
Dua tahun kemudian, kami memiliki Angular 4 dengan janji stabilitas relatif mulai sekarang.
Sekarang apa?
Angular vs. React: Membandingkan Apel dan Jeruk
Beberapa orang mengatakan bahwa membandingkan React dan Angular seperti membandingkan apel dengan jeruk. Sementara satu adalah perpustakaan yang berhubungan dengan pandangan, yang lain adalah kerangka kerja yang lengkap.
Tentu saja, sebagian besar pengembang React akan menambahkan beberapa library ke React untuk mengubahnya menjadi kerangka kerja yang lengkap. Kemudian lagi, alur kerja yang dihasilkan dari tumpukan ini seringkali masih sangat berbeda dari Angular, sehingga komparabilitasnya masih terbatas.
Perbedaan terbesar terletak pada pengelolaan negara. Angular hadir dengan pengikatan data yang dibundel, sedangkan React saat ini biasanya ditambah oleh Redux untuk menyediakan aliran data searah dan bekerja dengan data yang tidak dapat diubah. Itu adalah pendekatan yang berlawanan dalam hak mereka sendiri, dan diskusi yang tak terhitung jumlahnya sekarang sedang berlangsung apakah bisa berubah/pengikatan data lebih baik atau lebih buruk daripada tidak berubah/searah.
Lapangan Bermain Tingkat
Karena React terkenal lebih mudah untuk diretas, saya telah memutuskan, untuk tujuan perbandingan ini, untuk membangun pengaturan React yang mencerminkan Angular cukup dekat untuk memungkinkan perbandingan cuplikan kode secara berdampingan.
Fitur Angular tertentu yang menonjol tetapi tidak ada di React secara default adalah:
Fitur | Paket sudut | Perpustakaan reaksi |
---|---|---|
Pengikatan data, injeksi ketergantungan (DI) | @sudut/inti | MobX |
Properti yang dihitung | rxjs | MobX |
Perutean berbasis komponen | @sudut/router | Bereaksi Router v4 |
Komponen desain bahan: | @sudut/bahan | Kotak Alat Bereaksi |
CSS dicakup ke komponen | @sudut/inti | modul CSS |
Validasi formulir | @sudut/bentuk | bentuk negara |
Pembangkit proyek | @sudut/cli | React Script TS |
Pengikatan Data
Pengikatan data bisa dibilang lebih mudah untuk memulai daripada pendekatan searah. Tentu saja, mungkin untuk pergi ke arah yang benar-benar berlawanan, dan menggunakan Redux atau mobx-state-tree dengan React, dan ngrx dengan Angular. Tapi itu akan menjadi topik untuk posting lain.
Properti yang Dihitung
Sementara menyangkut kinerja, pengambil biasa di Angular tidak diragukan lagi karena dipanggil pada setiap render. Dimungkinkan untuk menggunakan BehaviorSubject dari RsJS, yang berfungsi.
Dengan React, dimungkinkan untuk menggunakan @computed dari MobX, yang mencapai tujuan yang sama, dengan API yang bisa dibilang sedikit lebih bagus.
Injeksi Ketergantungan
Injeksi ketergantungan agak kontroversial karena bertentangan dengan paradigma React saat ini tentang pemrograman fungsional dan kekekalan. Ternyata, beberapa jenis injeksi ketergantungan hampir sangat diperlukan dalam lingkungan pengikatan data, karena membantu decoupling (dan dengan demikian mengejek dan menguji) di mana tidak ada arsitektur lapisan data yang terpisah.
Satu lagi keuntungan DI (didukung dalam Angular) adalah kemampuan untuk memiliki siklus hidup yang berbeda dari toko yang berbeda. Sebagian besar paradigma React saat ini menggunakan semacam status aplikasi global yang memetakan ke komponen yang berbeda, tetapi dari pengalaman saya, terlalu mudah untuk memperkenalkan bug saat membersihkan status global pada pelepasan komponen.
Memiliki toko yang dibuat pada pemasangan komponen (dan tersedia dengan mulus untuk anak-anak komponen ini) tampaknya sangat berguna, dan konsep yang sering diabaikan.
Di luar kotak di Angular, tetapi cukup mudah direproduksi dengan MobX juga.
Rute
Perutean berbasis komponen memungkinkan komponen untuk mengelola sub-rutenya sendiri alih-alih memiliki satu konfigurasi router global yang besar. Pendekatan ini akhirnya berhasil menjadi react-router
di versi 4.
Desain Bahan
Memulai dengan beberapa komponen tingkat yang lebih tinggi selalu menyenangkan, dan desain material telah menjadi sesuatu seperti pilihan default yang diterima secara universal, bahkan dalam proyek non-Google.
Saya sengaja memilih React Toolbox daripada Material UI yang biasanya direkomendasikan, karena Material UI memiliki masalah kinerja yang serius dengan pendekatan CSS inline mereka, yang mereka rencanakan untuk dipecahkan di versi berikutnya.
Selain itu, PostCSS/cssnext yang digunakan di React Toolbox mulai menggantikan Sass/LESS.
Lingkup CSS
Kelas CSS adalah sesuatu seperti variabel global. Ada banyak pendekatan untuk mengatur CSS untuk mencegah konflik (termasuk BEM), tetapi ada tren saat ini yang jelas dalam menggunakan perpustakaan yang membantu proses CSS untuk mencegah konflik tersebut tanpa perlu pengembang front-end untuk merancang sistem penamaan CSS yang rumit.
Validasi Formulir
Validasi formulir adalah fitur yang tidak sepele dan sangat banyak digunakan. Baik untuk memiliki yang dicakup oleh perpustakaan untuk mencegah pengulangan kode dan bug.
Pembuat Proyek
Memiliki generator CLI untuk sebuah proyek hanya sedikit lebih nyaman daripada harus mengkloning boilerplates dari GitHub.
Aplikasi yang Sama, Dibangun Dua Kali
Jadi kita akan membuat aplikasi yang sama di React dan Angular. Tidak ada yang spektakuler, hanya Shoutboard yang memungkinkan siapa saja untuk mengirim pesan ke halaman umum.
Anda dapat mencoba aplikasi di sini:
- Sudut Papan Shout
- Reaksi Papan Shot
Jika Anda ingin memiliki seluruh kode sumber, Anda bisa mendapatkannya dari GitHub:
- Sumber sudut Shoutboard
- Sumber Shoutboard React
Anda akan melihat kami telah menggunakan TypeScript untuk aplikasi React juga. Keuntungan dari pengecekan tipe di TypeScript sudah jelas. Dan sekarang, karena penanganan impor yang lebih baik, async/await dan rest spread akhirnya tiba di TypeScript 2, itu membuat Babel/ES7/Flow dalam debu.
Juga, mari tambahkan Apollo Client ke keduanya karena kita ingin menggunakan GraphQL. Maksud saya, REST itu bagus, tetapi setelah satu dekade atau lebih, itu menjadi tua.
Bootstrap dan Perutean
Pertama, mari kita lihat titik masuk kedua aplikasi.
sudut
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' }
Pada dasarnya, semua komponen yang ingin kita gunakan dalam aplikasi harus pergi ke deklarasi. Semua perpustakaan pihak ketiga untuk diimpor, dan semua toko global untuk penyedia. Komponen anak-anak memiliki akses ke semua ini, dengan kesempatan untuk menambahkan lebih banyak barang lokal.
Reaksi
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') )
Komponen <Provider/>
digunakan untuk injeksi ketergantungan di MobX. Ini menyimpan toko ke konteks sehingga komponen Bereaksi dapat menyuntikkannya nanti. Ya, konteks React dapat (bisa dibilang) digunakan dengan aman.
Versi React sedikit lebih pendek karena tidak ada deklarasi modul - biasanya, Anda hanya mengimpor dan siap digunakan. Terkadang ketergantungan keras semacam ini tidak diinginkan (pengujian), jadi untuk toko tunggal global, saya harus menggunakan pola GoF yang berusia puluhan tahun ini:
export class AppStore { static instance: AppStore static getInstance() { return AppStore.instance || (AppStore.instance = new AppStore()) } @observable username = 'Mr. User' }
Router Angular bersifat injeksi, sehingga dapat digunakan dari mana saja, tidak hanya komponen. Untuk mencapai hal yang sama dalam reaksi, kami menggunakan paket mobx-react-router dan menyuntikkan routerStore
.
Ringkasan: Bootstrap kedua aplikasi cukup mudah. React memiliki keunggulan yang lebih sederhana, hanya menggunakan impor daripada modul, tetapi, seperti yang akan kita lihat nanti, modul-modul itu bisa sangat berguna. Membuat lajang secara manual sedikit merepotkan. Adapun sintaks deklarasi perutean, JSON vs. JSX hanyalah masalah preferensi.
Tautan dan Navigasi Imperatif
Jadi ada dua kasus untuk berpindah rute. Deklaratif, menggunakan elemen <a href...>
, dan imperatif, memanggil API perutean (dan dengan demikian lokasi) secara langsung.
sudut
<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 secara otomatis mendeteksi routerLink
mana yang aktif, dan menempatkan kelas routerLinkActive
yang sesuai di dalamnya, sehingga dapat ditata.
Router menggunakan elemen <router-outlet>
khusus untuk merender apa pun yang ditentukan oleh jalur saat ini. Dimungkinkan untuk memiliki banyak <router-outlet>
, saat kami menggali lebih dalam ke sub-komponen aplikasi.
@Injectable() export class FormService { constructor(private router: Router) { } goBack() { this.router.navigate(['/posts']) } }
Modul router dapat disuntikkan ke layanan apa pun (setengah ajaib dengan tipe TypeScript-nya), deklarasi private
kemudian menyimpannya di instans tanpa perlu penugasan eksplisit. Gunakan metode navigate
untuk beralih URL.
Reaksi
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 juga dapat mengatur kelas link aktif dengan activeClassName
.
Di sini, kita tidak bisa memberikan nama kelas secara langsung, karena sudah dibuat unik oleh compiler modul CSS, dan kita perlu menggunakan style
helper. Lebih lanjut tentang itu nanti.
Seperti yang terlihat di atas, React Router menggunakan elemen <Switch>
di dalam elemen <App>
. Karena elemen <Switch>
hanya membungkus dan memasang rute saat ini, itu berarti bahwa sub-rute dari komponen saat ini hanyalah this.props.children
. Jadi itu juga bisa dikomposisi.
export class FormStore { routerStore: RouterStore constructor() { this.routerStore = RouterStore.getInstance() } goBack = () => { this.routerStore.history.push('/posts') } }
Paket mobx-router-store
juga memungkinkan injeksi dan navigasi yang mudah.
Ringkasan: Kedua pendekatan untuk perutean cukup sebanding. Angular tampaknya lebih intuitif, sementara React Router memiliki komposisi yang sedikit lebih mudah.
Injeksi Ketergantungan
Telah terbukti bermanfaat untuk memisahkan lapisan data dari lapisan presentasi. Apa yang kami coba capai dengan DI di sini adalah membuat komponen lapisan data (di sini disebut model/toko/layanan) mengikuti siklus hidup komponen visual, dan dengan demikian memungkinkan untuk membuat satu atau banyak contoh komponen tersebut tanpa perlu menyentuh global negara. Selain itu, harus dimungkinkan untuk mencampur dan mencocokkan data yang kompatibel dan lapisan visualisasi.
Contoh dalam artikel ini sangat sederhana, jadi semua hal DI mungkin tampak berlebihan, tetapi ini berguna seiring dengan berkembangnya aplikasi.
sudut
@Injectable() export class HomeService { message = 'Welcome to home page' counter = 0 increment() { this.counter++ } }
Jadi setiap kelas dapat dibuat @injectable
, dan properti serta metodenya tersedia untuk komponen.
@Component({ selector: 'app-home', templateUrl: './home.component.html', providers: [ HomeService ] }) export class HomeComponent { constructor( public homeService: HomeService, public appService: AppService, ) { } }
Dengan mendaftarkan HomeService
ke providers
komponen , kami menyediakannya untuk komponen ini secara eksklusif. Ini bukan singleton sekarang, tetapi setiap instance komponen akan menerima salinan baru, baru di mount komponen. Itu berarti tidak ada data basi dari penggunaan sebelumnya.
Sebaliknya, AppService
telah didaftarkan ke app.module
(lihat di atas), jadi ini adalah singleton dan tetap sama untuk semua komponen, meskipun umur aplikasi. Mampu mengontrol siklus hidup layanan dari komponen adalah konsep yang sangat berguna, namun kurang dihargai.
DI bekerja dengan menetapkan instance layanan ke konstruktor komponen, yang diidentifikasi oleh tipe TypeScript. Selain itu, kata kunci public
secara otomatis menetapkan parameter ke this
, sehingga kita tidak perlu lagi menulis baris this.homeService = homeService
yang membosankan.
<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>
Sintaks template Angular, bisa dibilang cukup elegan. Saya suka pintasan [()]
, yang berfungsi seperti pengikatan data 2 arah, tetapi di bawah tenda, itu sebenarnya adalah pengikatan atribut + acara. Seperti yang ditentukan oleh siklus hidup layanan kami, homeService.counter
akan diatur ulang setiap kali kami menavigasi keluar dari /home
, tetapi appService.username
tetap, dan dapat diakses dari mana saja.
Reaksi
import { observable } from 'mobx' export class HomeStore { @observable counter = 0 increment = () => { this.counter++ } }
Dengan MobX, kita perlu menambahkan dekorator @observable
ke properti mana pun yang ingin kita observable.
@observer export class Home extends React.Component<any, any> { homeStore: HomeStore componentWillMount() { this.homeStore = new HomeStore() } render() { return <Provider homeStore={this.homeStore}> <HomeComponent /> </Provider> } }
Untuk mengelola siklus hidup dengan benar, kita perlu melakukan sedikit lebih banyak pekerjaan daripada di contoh Angular. Kami membungkus HomeComponent
di dalam Provider
, yang menerima instance baru HomeStore
di setiap mount.
interface HomeComponentProps { appStore?: AppStore, homeStore?: HomeStore } @inject('appStore', 'homeStore') @observer export class HomeComponent extends React.Component<HomeComponentProps, any> { render() { const { homeStore, appStore } = this.props return <div> <h3>Dashboard</h3> <Input type='text' label='Edit your name' name='username' value={appStore.username} onChange={appStore.onUsernameChange} /> <span>Clicks since last visit: {homeStore.counter}</span> <button onClick={homeStore.increment}>Click!</button> </div> } }
HomeComponent
menggunakan dekorator @observer
untuk mendengarkan perubahan pada properti @observable
.
Mekanisme di bawah kap ini cukup menarik, jadi mari kita bahas secara singkat di sini. Dekorator @observable
menggantikan properti dalam objek dengan pengambil dan penyetel, yang memungkinkannya untuk mencegat panggilan. Ketika fungsi render dari komponen augmented @observer
dipanggil, pengambil properti tersebut dipanggil, dan mereka menyimpan referensi ke komponen yang memanggilnya.
Kemudian, ketika setter dipanggil dan nilainya diubah, fungsi render dari komponen yang menggunakan properti pada render terakhir dipanggil. Sekarang, data tentang properti mana yang digunakan di mana diperbarui, dan seluruh siklus dapat dimulai dari awal.
Mekanisme yang sangat sederhana, dan juga cukup berkinerja. Penjelasan lebih mendalam di sini.
Dekorator @inject
digunakan untuk menyuntikkan appStore
dan homeStore
ke properti HomeComponent
. Pada titik ini, masing-masing toko tersebut memiliki siklus hidup yang berbeda. appStore
sama selama masa pakai aplikasi, tetapi homeStore
baru dibuat pada setiap navigasi ke rute "/ home".
Manfaatnya adalah tidak perlu membersihkan properti secara manual seperti halnya ketika semua toko bersifat global, yang menyebalkan jika rutenya adalah halaman "detail" yang berisi data yang sama sekali berbeda setiap kali.
Ringkasan: Sebagai manajemen siklus hidup penyedia dalam fitur yang melekat pada DI Angular, tentu saja, lebih mudah untuk mencapainya di sana. Versi React juga dapat digunakan tetapi melibatkan lebih banyak boilerplate.
Properti yang Dihitung
Reaksi
Mari kita mulai dengan React yang satu ini, ia memiliki solusi yang lebih mudah.
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` } }
Jadi, kami memiliki properti terkomputasi yang mengikat ke counter
dan mengembalikan pesan jamak yang benar. Hasil counterMessage
di-cache, dan dihitung ulang hanya ketika counter
berubah.
<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>
Kemudian, kami mereferensikan properti (dan metode increment
) dari template JSX. Bidang input didorong oleh pengikatan ke suatu nilai, dan membiarkan metode dari appStore
menangani peristiwa pengguna.
sudut
Untuk mencapai efek yang sama di Angular, kita perlu sedikit lebih inventif.
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) } }
Kita perlu mendefinisikan semua nilai yang berfungsi sebagai dasar untuk properti yang dihitung sebagai BehaviorSubject
. Properti yang dihitung itu sendiri juga merupakan BehaviorSubject
, karena setiap properti yang dihitung dapat berfungsi sebagai input untuk properti yang dihitung lainnya.
Tentu saja, RxJS
dapat melakukan lebih dari sekadar ini, tetapi itu akan menjadi topik untuk artikel yang sama sekali berbeda. Kelemahan kecilnya adalah bahwa penggunaan RxJS yang sepele ini untuk properti yang hanya dihitung sedikit lebih bertele-tele daripada contoh reaksi, dan Anda perlu mengelola langganan secara manual (seperti di sini di 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>
Perhatikan bagaimana kita dapat mereferensikan subjek RxJS dengan | async
pipa | async
. Itu adalah sentuhan yang bagus, jauh lebih singkat daripada perlu berlangganan komponen Anda. Komponen input
digerakkan oleh direktif [(ngModel)]
. Meski terlihat aneh, sebenarnya cukup elegan. Hanya gula sintaksis untuk pengikatan data nilai ke appService.username
, dan penetapan nilai otomatis dari peristiwa input pengguna.
Ringkasan: Properti yang dihitung lebih mudah diimplementasikan di React/MobX daripada di Angular/RxJS, tetapi RxJS mungkin menyediakan beberapa fitur FRP yang lebih berguna, yang mungkin akan dihargai nanti.

Template dan CSS
Untuk menunjukkan bagaimana templating menumpuk satu sama lain, mari gunakan komponen Posts yang menampilkan daftar posting.
sudut
@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() } }
Komponen ini hanya menghubungkan HTML, CSS, dan layanan yang disuntikkan dan juga memanggil fungsi untuk memuat posting dari API pada inisialisasi. AppService
adalah singleton yang didefinisikan dalam modul aplikasi, sedangkan PostsService
bersifat sementara, dengan instance baru dibuat pada setiap komponen yang dibuat. CSS yang direferensikan dari komponen ini dicakup ke komponen ini, yang berarti bahwa konten tidak dapat memengaruhi apa pun di luar komponen.
<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>
Dalam template HTML, kami mereferensikan sebagian besar komponen dari Angular Material. Agar tersedia, perlu untuk memasukkannya ke dalam impor app.module
(lihat di atas). *ngFor
digunakan untuk mengulang komponen md-card
untuk setiap posting.
CSS lokal:
.mat-card { margin-bottom: 1rem; }
CSS lokal hanya menambah salah satu kelas yang ada pada komponen md-card
.
CSS global:
.float-right { float: right; }
Kelas ini didefinisikan dalam file style.css
global agar tersedia untuk semua komponen. Itu dapat direferensikan dengan cara standar, class="float-right"
.
CSS yang dikompilasi:
.float-right { float: right; } .mat-card[_ngcontent-c1] { margin-bottom: 1rem; }
Dalam CSS yang dikompilasi, kita dapat melihat bahwa CSS lokal telah dicakupkan ke komponen yang dirender dengan menggunakan pemilih atribut [_ngcontent-c1]
. Setiap komponen Angular yang dirender memiliki kelas yang dihasilkan seperti ini untuk tujuan pelingkupan CSS.
Keuntungan dari mekanisme ini adalah kita dapat mereferensikan kelas secara normal, dan pelingkupan ditangani "di bawah tenda."
Reaksi
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> } }
Di React, sekali lagi, kita perlu menggunakan pendekatan Provider
untuk membuat ketergantungan PostsStore
“sementara”. Kami juga mengimpor gaya CSS, direferensikan sebagai style
dan appStyle
, untuk dapat menggunakan kelas dari file CSS tersebut di BEJ.
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> } }
Tentu saja, JSX terasa jauh lebih JavaScript-y daripada template HTML Angular, yang bisa menjadi hal yang baik atau buruk tergantung pada selera Anda. Alih-alih direktif *ngFor
, kami menggunakan konstruksi map
untuk mengulangi postingan.
Sekarang, Angular mungkin merupakan kerangka kerja yang paling menonjolkan TypeScript, tetapi sebenarnya JSX di mana TypeScript benar-benar bersinar. Dengan penambahan modul CSS (diimpor di atas), itu benar-benar mengubah pengkodean template Anda menjadi zen penyelesaian kode. Setiap hal diperiksa jenisnya. Komponen, atribut, bahkan kelas CSS ( appStyle.floatRight
dan style.messageCard
, lihat di bawah). Dan tentu saja, sifat ramping dari JSX mendorong pemisahan menjadi komponen dan fragmen sedikit lebih banyak daripada template Angular.
CSS lokal:
.messageCard { margin-bottom: 1rem; }
CSS global:
.floatRight { float: right; }
CSS yang dikompilasi:
.floatRight__qItBM { float: right; } .messageCard__1Dt_9 { margin-bottom: 1rem; }
Seperti yang Anda lihat, CSS Modules loader mem-postfix setiap kelas CSS dengan postfix acak, yang menjamin keunikan. Cara sederhana untuk menghindari konflik. Kelas kemudian direferensikan melalui objek yang diimpor webpack. Salah satu kelemahan yang mungkin dari ini adalah Anda tidak bisa begitu saja membuat CSS dengan kelas dan menambahnya, seperti yang kita lakukan pada contoh Angular. Di sisi lain, ini sebenarnya bisa menjadi hal yang baik, karena memaksa Anda untuk merangkum gaya dengan benar.
Ringkasan: Saya pribadi lebih menyukai JSX daripada templat Angular, terutama karena penyelesaian kode dan dukungan pengecekan tipe. Itu benar-benar fitur pembunuh. Angular sekarang memiliki kompiler AOT, yang juga dapat melihat beberapa hal, penyelesaian kode juga berfungsi untuk sekitar setengah dari hal-hal di sana, tetapi tidak selengkap JSX/TypeScript.
GraphQL - Memuat Data
Jadi kami memutuskan untuk menggunakan GraphQL untuk menyimpan data untuk aplikasi ini. Salah satu cara termudah untuk membuat back-end GraphQL adalah dengan menggunakan beberapa BaaS, seperti Graphcool. Jadi itulah yang kami lakukan. Pada dasarnya, Anda hanya menentukan model dan atribut, dan CRUD Anda siap digunakan.
Kode Umum
Karena beberapa kode terkait GraphQL 100% sama untuk kedua implementasi, jangan ulangi dua kali:
const PostsQuery = gql` query PostsQuery { allPosts(orderBy: createdAt_DESC, first: 5) { id, name, title, message } } `
GraphQL adalah bahasa kueri yang ditujukan untuk menyediakan serangkaian fungsionalitas yang lebih kaya dibandingkan dengan titik akhir RESTful klasik. Mari kita membedah kueri khusus ini.
-
PostsQuery
hanyalah sebuah nama untuk kueri ini untuk referensi nanti, dapat dinamai apa saja. -
allPosts
adalah bagian terpenting - ini mereferensikan fungsi untuk menanyakan semua catatan dengan model `Post`. Nama ini dibuat oleh Graphcool. -
orderBy
danfirst
adalah parameter dari fungsiallPosts
.createdAt
adalah salah satu atribut modelPost
.first: 5
berarti hanya akan mengembalikan 5 hasil kueri pertama. -
id
,name
,title
, danmessage
adalah atribut dari modelPost
yang ingin kita masukkan ke dalam hasil. Atribut lain akan disaring.
Seperti yang sudah Anda lihat, itu cukup kuat. Lihat halaman ini untuk lebih membiasakan diri Anda dengan kueri GraphQL.
interface Post { id: string name: string title: string message: string } interface PostsQueryResult { allPosts: Array<Post> }
Ya, sebagai warga TypeScript yang baik, kami membuat antarmuka untuk hasil GraphQL.
sudut
@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 }) } }
Kueri GraphQL adalah RxJS yang dapat diamati, dan kami berlangganan padanya. Ini berfungsi seperti janji, tetapi tidak cukup, jadi kami kurang beruntung menggunakan async/await
. Tentu saja, masih ada Janji, tetapi sepertinya itu bukan cara Angular. Kami menyetel fetchPolicy: 'network-only'
karena dalam kasus ini, kami tidak ingin men-cache data, tetapi mengambil ulang setiap kali.
Reaksi
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 } }
Versi React hampir identik, tetapi karena apolloClient
di sini menggunakan janji, kita dapat memanfaatkan sintaks async/await
. Ada pendekatan lain di React yang hanya "menyimpan" kueri GraphQL ke komponen tingkat tinggi, tetapi bagi saya tampaknya terlalu banyak mencampurkan data dan lapisan presentasi.
Ringkasan: Ide dari RxJS berlangganan vs. async/menunggu benar-benar sama.
GraphQL - Menyimpan Data
Kode Umum
Sekali lagi, beberapa kode terkait GraphQL:
const AddPostMutation = gql` mutation AddPostMutation($name: String!, $title: String!, $message: String!) { createPost( name: $name, title: $title, message: $message ) { id } } `
Tujuan mutasi adalah untuk membuat atau memperbarui catatan. Oleh karena itu bermanfaat untuk mendeklarasikan beberapa variabel dengan mutasi karena itu adalah cara bagaimana mengirimkan data ke dalamnya. Jadi kita memiliki variabel name
, title
, dan message
, yang diketik sebagai String
, yang harus kita isi setiap kali kita memanggil mutasi ini. Fungsi createPost
, sekali lagi, didefinisikan oleh Graphcool. Kami menentukan bahwa kunci model Post
akan memiliki nilai dari variabel mutasi keluar, dan juga bahwa kami hanya ingin id
Post yang baru dibuat untuk dikirim sebagai balasannya.
sudut
@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) }) } }
Saat memanggil apollo.mutate
, kita perlu memberikan mutasi yang kita panggil dan juga variabelnya. Kami mendapatkan hasil dalam panggilan balik subscribe
dan menggunakan router
yang disuntikkan untuk menavigasi kembali ke daftar posting.
Reaksi
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') } }
Sangat mirip dengan di atas, dengan perbedaan injeksi ketergantungan yang lebih "manual", dan penggunaan async/await
.
Ringkasan: Sekali lagi, tidak banyak perbedaan di sini. berlangganan vs. async/menunggu pada dasarnya adalah semua yang berbeda.
Formulir
Kami ingin mencapai tujuan berikut dengan formulir dalam aplikasi ini:
- Pengikatan data bidang ke model
- Pesan validasi untuk setiap bidang, beberapa aturan
- Dukungan untuk memeriksa apakah seluruh formulir valid
Reaksi
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 }) }
Jadi perpustakaan formstate berfungsi sebagai berikut: Untuk setiap bidang formulir Anda, Anda mendefinisikan FieldState
. Parameter yang dilewatkan adalah nilai awal. Properti validators
mengambil fungsi, yang mengembalikan "false" saat nilainya valid, dan pesan validasi saat nilainya tidak valid. Dengan fungsi pembantu check
dan checkRequired
, semuanya dapat terlihat deklaratif dengan baik.
Untuk mendapatkan validasi untuk seluruh formulir, sebaiknya juga membungkus bidang tersebut dengan instance FormState
, yang kemudian memberikan validitas agregat.
@inject('appStore', 'formStore') @observer export class FormComponent extends React.Component<FormComponentProps, any> { render() { const { appStore, formStore } = this.props const { postFormState } = formStore return <div> <h2> Create a new post </h2> <h3> You are now posting as {appStore.username} </h3> <Input type='text' label='Title' name='title' error={postFormState.title.error} value={postFormState.title.value} onChange={postFormState.title.onChange} /> <Input type='text' multiline={true} rows={3} label='Message' name='message' error={postFormState.message.error} value={postFormState.message.value} onChange={postFormState.message.onChange} />
FormState
menyediakan properti value
, onChange
, dan error
, yang dapat dengan mudah digunakan dengan komponen front-end apa pun.
<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.