Angular vs. React:Web開発にはどちらが良いですか?

公開: 2022-03-11

ReactとAngularのどちらがWeb開発に適しているかを議論する記事は無数にあります。 もう1つ必要ですか?

私がこの記事を書いた理由は、すでに公開されている記事はどれも、優れた洞察を含んでいますが、実際のフロントエンド開発者がニーズに合うものを決定するのに十分な深さではないためです。

この記事では、AngularとReactの両方が、哲学が大きく異なるものの、同様のフロントエンドの問題を解決することをどのように目指しているか、そしてどちらを選択するかは単に個人的な好みの問題であるかどうかを学びます。 それらを比較するために、同じアプリケーションを2回ビルドします。1回はAngularを使用し、もう1回はReactを使用します。

Angularのタイムリーでない発表

2年前、私はReactエコシステムについての記事を書きました。 とりわけ、この記事は、Angularが「事前発表による死」の犠牲者になったと主張しました。 当時、Angularと他のほとんどすべてのもののどちらを選択するかは、プロジェクトを廃止されたフレームワークで実行したくない人にとっては簡単な選択でした。 Angular 1は廃止され、Angular2はアルファ版でも利用できませんでした。

後から考えると、恐れは多かれ少なかれ正当化されました。 Angular 2は劇的に変化し、最終リリースの直前に大幅な書き直しも行われました。

2年後、Angular 4が登場し、今後は比較的安定することが期待されます。

それで?

Angular vs. React:リンゴとオレンジの比較

ReactとAngularを比較することは、リンゴとオレンジを比較するようなものだと言う人もいます。 1つはビューを処理するライブラリですが、もう1つは本格的なフレームワークです。

もちろん、ほとんどのReact開発者は、Reactにいくつかのライブラリを追加して、完全なフレームワークに変換します。 繰り返しになりますが、このスタックの結果のワークフローは、Angularとはまだ大きく異なることが多いため、比較可能性は依然として制限されています。

最大の違いは、状態管理にあります。 Angularにはデータバインディングがバンドルされていますが、今日のReactは通常、一方向のデータフローを提供し、不変のデータを処理するためにReduxによって拡張されています。 これらはそれ自体が反対のアプローチであり、可変/データバインディングが不変/単方向よりも優れているか悪いかについて、数え切れないほどの議論が行われています。

平等な運動場

Reactはハッキングが簡単なことで有名なので、この比較の目的で、Angularを適度に忠実にミラーリングしてコードスニペットを並べて比較できるReactセットアップを構築することにしました。

目立つがデフォルトではReactにない特定のAngular機能は次のとおりです。

特徴AngularパッケージReactライブラリ
データバインディング、依存性注入(DI) @ angle / core MobX
計算されたプロパティrxjs MobX
コンポーネントベースのルーティング@ angle / router Reactルーターv4
マテリアルデザインコンポーネント@ angle /material ReactToolbox
コンポーネントを対象としたCSS @ angle / core CSSモジュール
フォームの検証@ angle / forms FormState
プロジェクトジェネレータ@ angle / cli React Scripts TS

データバインディング

データバインディングは、一方向のアプローチよりも開始するのがほぼ間違いなく簡単です。 もちろん、完全に反対の方向に進み、ReactでReduxまたはmobx-state-treeを使用し、Angularでngrxを使用することは可能です。 しかし、それは別の投稿のトピックになります。

計算されたプロパティ

パフォーマンスに関しては、Angularのプレーンゲッターは、レンダリングごとに呼び出されるため、問題外です。 その仕事をするRsJSからBehaviorSubjectを使用することが可能です。

Reactを使用すると、MobXの@computedを使用できます。これは、ほぼ間違いなく、もう少し優れたAPIで同じ目的を達成します。

依存性注入

依存性注入は、関数型プログラミングと不変性の現在のReactパラダイムに反するため、物議を醸すものです。 結局のところ、データバインディング環境では、個別のデータレイヤーアーキテクチャがない場合のデカップリング(したがってモックとテスト)に役立つため、ある種の依存性注入はほぼ不可欠です。

DI(Angularでサポート)のもう1つの利点は、さまざまなストアのさまざまなライフサイクルを持つことができることです。 現在のほとんどのReactパラダイムは、さまざまなコンポーネントにマップするある種のグローバルアプリ状態を使用していますが、私の経験から、コンポーネントのアンマウント時にグローバル状態をクリーンアップするときにバグを導入するのは非常に簡単です。

コンポーネントマウントで作成される(そしてこのコンポーネントの子がシームレスに利用できる)ストアを持つことは非常に便利であり、見過ごされがちな概念です。

Angularの箱から出してすぐに使用できますが、MobXでも非常に簡単に再現できます。

ルーティング

コンポーネントベースのルーティングにより、コンポーネントは1つの大きなグローバルルーター構成を使用する代わりに、独自のサブルートを管理できます。 このアプローチにより、バージョン4 react-routerがついに実現しました。

マテリアルデザイン

いくつかの高レベルのコンポーネントから始めるのは常に良いことであり、マテリアルデザインは、Google以外のプロジェクトでも、広く受け入れられているデフォルトの選択肢のようなものになっています。

Material UIには、次のバージョンで解決する予定のインラインCSSアプローチで深刻な自白のパフォーマンスの問題があるため、通常推奨されるMaterialUIよりもReactToolboxを意図的に選択しました。

その上、ReactToolboxで使用されるPostCSS/ cssnextは、とにかくSass/LESSに取って代わり始めています。

スコープ付きCSS

CSSクラスはグローバル変数のようなものです。 競合を防ぐためにCSSを編成する方法は多数ありますが(BEMを含む)、フロントエンド開発者が複雑なCSSネーミングシステムを考案することなく、CSSを処理して競合を防ぐのに役立つライブラリを使用する傾向があります。

フォームの検証

フォームの検証は重要で、非常に広く使用されている機能です。 コードの繰り返しやバグを防ぐために、それらをライブラリでカバーしておくとよいでしょう。

プロジェクトジェネレータ

プロジェクト用のCLIジェネレーターを使用すると、GitHubからボイラープレートのクローンを作成するよりも少し便利です。

同じアプリケーション、2回ビルド

したがって、ReactとAngularで同じアプリケーションを作成します。 見事なものは何もありません。誰でも共通のページにメッセージを投稿できるShoutboardだけです。

ここでアプリケーションを試すことができます:

  • シャウトボードアンギュラー
  • シャウトボード反応

シャウトボードアプリケーション

ソースコード全体が必要な場合は、GitHubから入手できます。

  • ShoutboardAngularソース
  • ShoutboardReactソース

ReactアプリにもTypeScriptを使用していることに気付くでしょう。 TypeScriptでの型チェックの利点は明らかです。 そして今、インポートのより良い処理、非同期/待機および残りのスプレッドがついにTypeScript 2に到着したので、それはBabel / ES7/Flowをほこりの中に残します。

また、GraphQLを使用したいので、両方にApolloClientを追加しましょう。 つまり、RESTは素晴らしいのですが、10年ほど経つと、RESTは古くなります。

ブートストラップとルーティング

まず、両方のアプリケーションのエントリポイントを見てみましょう。

Angular

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

基本的に、アプリケーションで使用するすべてのコンポーネントは、宣言に移動する必要があります。 インポートするすべてのサードパーティライブラリ、およびプロバイダーへのすべてのグローバルストア。 子コンポーネントはこれらすべてにアクセスでき、ローカルのものを追加する機会があります。

React

 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/>コンポーネントは、MobXでの依存性注入に使用されます。 ストアをコンテキストに保存して、Reactコンポーネントが後でそれらを注入できるようにします。 はい、Reactコンテキストは(おそらく)安全に使用できます。

モジュール宣言がないため、Reactバージョンは少し短くなっています。通常は、インポートするだけですぐに使用できます。 この種のハード依存関係が望ましくない場合(テスト)があるため、グローバルシングルトンストアでは、この数十年前のGoFパターンを使用する必要がありました。

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

Angularのルーターは注入可能であるため、コンポーネントだけでなく、どこからでも使用できます。 同じことをreactで実現するために、mobx-react-routerパッケージを使用してrouterStoreを注入します。

概要:両方のアプリケーションのブートストラップは非常に簡単です。 Reactには、モジュールの代わりにインポートだけを使用するという、よりシンプルなエッジがありますが、後で説明するように、これらのモジュールは非常に便利です。 シングルトンを手動で作成するのは少し面倒です。 ルーティング宣言の構文に関しては、JSONとJSXは好みの問題です。

リンクと命令型ナビゲーション

したがって、ルートを切り替えるには2つのケースがあります。 <a href...>要素を使用する宣言型、およびルーティング(したがってロケーション)APIを直接呼び出す命令型。

Angular

 <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は、アクティブなrouterLinkを自動的に検出し、適切なrouterLinkActiveクラスを配置して、スタイルを設定できるようにします。

ルーターは、特別な<router-outlet>要素を使用して、現在のパスで指定されているものをレンダリングします。 アプリケーションのサブコンポーネントを深く掘り下げると、多くの<router-outlet>を持つことができます。

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

ルーターモジュールは任意のサービスに注入でき(半魔法的にはTypeScriptタイプによって)、 private宣言は明示的な割り当てを必要とせずにインスタンスに格納します。 navigateメソッドを使用してURLを切り替えます。

React

 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は、 activeClassNameを使用してアクティブリンクのクラスを設定することもできます。

ここでは、CSSモジュールコンパイラによって一意にされているため、クラス名を直接指定することはできません。また、 styleヘルパーを使用する必要があります。 これについては後で詳しく説明します。

上記のように、ReactRouterは<App>要素内の<Switch>要素を使用します。 <Switch>要素は現在のルートをラップしてマウントするだけなので、現在のコンポーネントのサブルートはthis.props.childrenだけであることを意味します。 だからそれも構成可能です。

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

mobx-router-storeパッケージを使用すると、インジェクションとナビゲーションも簡単になります。

要約:ルーティングへの両方のアプローチは非常に類似しています。 Angularはより直感的であるように見えますが、ReactRouterはもう少し簡単な構成が可能です。

依存性注入

データ層をプレゼンテーション層から分離することはすでに有益であることが証明されています。 ここでDIを使用して達成しようとしているのは、データレイヤーのコンポーネント(ここではモデル/ストア/サービスと呼ばれます)をビジュアルコンポーネントのライフサイクルに従わせることです。これにより、グローバルに触れることなく、そのようなコンポーネントの1つまたは複数のインスタンスを作成できます。州。 また、互換性のあるデータと視覚化レイヤーを組み合わせて組み合わせることが可能である必要があります。

この記事の例は非常に単純なので、すべてのDIはやり過ぎのように見えるかもしれませんが、アプリケーションが大きくなるにつれて便利になります。

Angular

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

したがって、任意のクラスを@injectableにし、そのプロパティとメソッドをコンポーネントで使用できるようにすることができます。

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

HomeServiceをコンポーネントのprovidersに登録することにより、このコンポーネントでのみ使用できるようになります。 現在はシングルトンではありませんが、コンポーネントの各インスタンスは、コンポーネントのマウントで新しいコピーを受け取ります。 これは、以前の使用からの古いデータがないことを意味します。

対照的に、 AppServiceapp.module (上記を参照)に登録されているため、シングルトンであり、アプリケーションの存続期間中はすべてのコンポーネントで同じままです。 コンポーネントからサービスのライフサイクルを制御できることは、非常に便利ですが、十分に評価されていない概念です。

DIは、TypeScriptタイプによって識別されるコンポーネントのコンストラクターにサービスインスタンスを割り当てることによって機能します。 さらに、 publicキーワードはパラメーターをthisに自動割り当てするため、これ以上退屈な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>

Angularのテンプレート構文は、間違いなく非常にエレガントです。 私は[()]ショートカットが好きです。これは、双方向のデータバインディングのように機能しますが、内部的には、実際には属性バインディング+イベントです。 サービスのライフサイクルに応じて、 homeService.counter/homeから移動するたびにリセットされますが、 appService.usernameはそのままで、どこからでもアクセスできます。

React

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

MobXでは、監視可能にしたいプロパティに@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> } }

ライフサイクルを正しく管理するには、Angularの例よりも少し多くの作業を行う必要があります。 HomeComponentProvider内にラップします。プロバイダーは、マウントごとにHomeStoreの新しいインスタンスを受け取ります。

 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は、 @observerデコレータを使用して@observableプロパティの変更をリッスンします。

この内部メカニズムは非常に興味深いので、ここで簡単に説明します。 @observableデコレータは、オブジェクトのプロパティをgetterとsetterに置き換えます。これにより、呼び出しをインターセプトできます。 @observer拡張コンポーネントのrender関数が呼び出されると、それらのプロパティgetterが呼び出され、それらを呼び出したコンポーネントへの参照が保持されます。

次に、setterが呼び出されて値が変更されると、最後のレンダリングでプロパティを使用したコンポーネントのレンダリング関数が呼び出されます。 これで、どのプロパティがどこで使用されているかに関するデータが更新され、サイクル全体を最初からやり直すことができます。

非常にシンプルなメカニズムであり、パフォーマンスも非常に優れています。 ここでより詳細な説明。

@injectデコレータは、 appStoreおよびhomeStoreインスタンスをHomeComponentの小道具に注入するために使用されます。 この時点で、これらのストアはそれぞれ異なるライフサイクルを持っています。 appStoreはアプリケーションの存続期間中同じですが、 homeStoreは「/home」ルートへのナビゲーションごとに新たに作成されます。

これの利点は、すべてのストアがグローバルである場合のように、プロパティを手動でクリーンアップする必要がないことです。これは、ルートが毎回完全に異なるデータを含む「詳細」ページである場合に苦痛です。

概要: AngularのDIに固有の機能におけるプロバイダーのライフサイクル管理として、もちろん、そこでそれを実現する方が簡単です。 Reactバージョンも使用可能ですが、より多くのボイラープレートが含まれます。

計算されたプロパティ

React

これについてはReactから始めましょう。より簡単な解決策があります。

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

したがって、 counterにバインドし、適切に複数形のメッセージを返す計算プロパティがあります。 counterMessageの結果はキャッシュされ、 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>

次に、JSXテンプレートからプロパティ(およびincrementメソッド)を参照します。 入力フィールドは、値にバインドし、 appStoreのメソッドにユーザーイベントを処理させることによって駆動されます。

Angular

Angularで同じ効果を達成するには、もう少し独創的である必要があります。

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

計算されたプロパティの基礎となるすべての値をBehaviorSubjectとして定義する必要があります。 計算されたプロパティ自体もBehaviorSubjectです。これは、計算されたプロパティが別の計算されたプロパティの入力として機能する可能性があるためです。

もちろん、 RxJSはこれ以上のことを行うことができますが、それはまったく別の記事のトピックになります。 マイナーな欠点は、計算されたプロパティだけにRxJSを使用することは、reactの例よりも少し冗長であり、サブスクリプションを手動で管理する必要があることです(ここではコンストラクターのように)。

 <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を使用してRxJSサブジェクトを参照する方法に注意してください。 | asyncパイプ。 これはいい感じで、コンポーネントをサブスクライブする必要があるよりもはるかに短いです。 inputコンポーネントは、 [(ngModel)]ディレクティブによって駆動されます。 奇妙に見えますが、実際にはかなりエレガントです。 appService.usernameへの値のデータバインディング、およびユーザー入力イベントからの値の自動割り当てのための単なる構文糖衣。

概要:計算されたプロパティは、Angular/RxJSよりもReact/MobXで実装する方が簡単ですが、RxJSは、後で理解できる、より便利なFRP機能を提供する場合があります。

テンプレートとCSS

テンプレートが互いにどのようにスタックするかを示すために、投稿のリストを表示する投稿コンポーネントを使用してみましょう。

Angular

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

このコンポーネントは、HTML、CSS、および挿入されたサービスを相互に接続し、初期化時にAPIから投稿を読み込む関数を呼び出します。 AppServiceはアプリケーションモジュールで定義されたシングルトンですが、 PostsServiceは一時的なものであり、コンポーネントが作成されるたびに新しいインスタンスが作成されます。 このコンポーネントから参照されるCSSは、このコンポーネントにスコープされます。つまり、コンテンツはコンポーネントの外部に影響を与えることはできません。

 <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テンプレートでは、主にAngularMaterialのコンポーネントを参照しています。 それらを利用可能にするには、それらをapp.moduleインポートに含める必要がありました(上記を参照)。 *ngForディレクティブは、投稿ごとにmd-cardコンポーネントを繰り返すために使用されます。

ローカルCSS:

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

ローカルCSSは、 md-cardコンポーネントに存在するクラスの1つを拡張するだけです。

グローバルCSS:

 .float-right { float: right; }

このクラスは、すべてのコンポーネントで使用できるようにするために、グローバルなstyle.cssファイルで定義されています。 標準的な方法であるclass="float-right"で参照できます。

コンパイルされたCSS:

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

コンパイルされたCSSでは、 [_ngcontent-c1]属性セレクターを使用して、ローカルCSSがレンダリングされたコンポーネントにスコープされていることがわかります。 レンダリングされたすべてのAngularコンポーネントには、CSSスコープの目的でこのような生成されたクラスがあります。

このメカニズムの利点は、クラスを正常に参照できることと、スコープが「内部」で処理されることです。

React

 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でも、 PostsStore依存関係を「一時的」にするためにProviderアプローチを使用する必要があります。 また、 styleおよびappStyleとして参照されるCSSスタイルをインポートして、JSXでそれらのCSSファイルのクラスを使用できるようにします。

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

当然のことながら、JSXはAngularのHTMLテンプレートよりもはるかにJavaScriptのように感じます。これは、好みに応じて良いことも悪いこともあります。 *ngForディレクティブの代わりに、 map構文を使用して投稿を反復処理します。

さて、AngularはTypeScriptを最も宣伝するフレームワークかもしれませんが、実際にはTypeScriptが本当に輝いているのはJSXです。 CSSモジュール(上記でインポート)を追加すると、テンプレートのコーディングがコード補完の禅に変わります。 すべてのものがタイプチェックされます。 コンポーネント、属性、さらにはCSSクラス( appStyle.floatRightおよびstyle.messageCard 、以下を参照)。 そしてもちろん、JSXの無駄のない性質により、Angularのテンプレートよりも少し多くのコンポーネントとフラグメントに分割することができます。

ローカルCSS:

 .messageCard { margin-bottom: 1rem; }

グローバルCSS:

 .floatRight { float: right; }

コンパイルされたCSS:

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

ご覧のとおり、CSS Modulesローダーは各CSSクラスにランダムな接尾辞を付けて、一意性を保証します。 競合を回避する簡単な方法。 その後、クラスはwebpackのインポートされたオブジェクトを介して参照されます。 これの考えられる欠点の1つは、Angularの例で行ったように、クラスを使用してCSSを作成して拡張することができないことです。 一方、これは実際には良いことです。スタイルを適切にカプセル化する必要があるからです。

要約:私は個人的に、特にコード補完と型チェックのサポートのために、AngularテンプレートよりもJSXの方が少し好きです。 それは本当にキラー機能です。 Angularには現在AOTコンパイラがあり、これもいくつかのことを見つけることができます。コード補完もそこにあるものの約半分で機能しますが、JSX/TypeScriptほど完全ではありません。

GraphQL-データの読み込み

そのため、このアプリケーションのデータを格納するためにGraphQLを使用することにしました。 GraphQLバックエンドを作成する最も簡単な方法の1つは、GraphcoolなどのBaaSを使用することです。 それが私たちがしたことです。 基本的には、モデルと属性を定義するだけで、CRUDを使用できます。

共通コード

GraphQL関連のコードの一部は両方の実装で100%同じであるため、2回繰り返さないでください。

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

GraphQLは、従来のRESTfulエンドポイントと比較してより豊富な機能セットを提供することを目的としたクエリ言語です。 この特定のクエリを分析してみましょう。

  • PostsQueryは、後で参照するこのクエリの単なる名前であり、任意の名前を付けることができます。
  • allPostsは最も重要な部分です。これは、`Post`モデルを使用してすべてのレコードをクエリする関数を参照します。 この名前はGraphcoolによって作成されました。
  • orderByfirstは、 allPosts関数のパラメーターです。 createdAtは、 Postモデルの属性の1つです。 first: 5は、クエリの最初の5つの結果のみを返すことを意味します。
  • idnametitle 、およびmessageは、結果に含めるPostモデルの属性です。 その他の属性は除外されます。

すでにお分かりのように、それはかなり強力です。 このページをチェックして、GraphQLクエリについて詳しく理解してください。

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

はい、TypeScriptの良き市民として、GraphQLの結果のためのインターフェースを作成します。

Angular

 @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クエリはRxJSで監視可能であり、サブスクライブします。 それは約束のように少し機能しますが、完全ではないので、 async/awaitを使用するのは運が悪いです。 もちろん、toPromiseはまだありますが、とにかくAngularの方法ではないようです。 この場合、データをキャッシュしたくないので、 fetchPolicy: 'network-only'を設定しますが、毎回再フェッチします。

React

 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のバージョンはほとんど同じですが、ここでのapolloClientはpromiseを使用しているため、 async/await構文を利用できます。 Reactには、GraphQLクエリを高次のコンポーネントに「テープ」するだけのアプローチが他にもありますが、データとプレゼンテーション層を少し混ぜすぎているように見えました。

概要: RxJSサブスクライブとasync/awaitの考え方はまったく同じです。

GraphQL-データの保存

共通コード

繰り返しますが、いくつかのGraphQL関連のコード:

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

ミューテーションの目的は、レコードを作成または更新することです。 したがって、データを渡す方法であるため、ミューテーションを使用していくつかの変数を宣言することは有益です。 したがって、 nametitle 、およびmessage変数があり、 Stringとして入力されます。これらは、このミューテーションを呼び出すたびに入力する必要があります。 createPost関数も、Graphcoolによって定義されています。 Postモデルのキーには、ミューテーション変数からの値が含まれるように指定します。また、新しく作成されたPostのidのみを返送するように指定します。

Angular

 @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を呼び出すときは、呼び出すミューテーションと変数も指定する必要があります。 subscribeコールバックで結果を取得し、挿入されたrouterを使用して投稿リストに戻ります。

React

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

上記と非常によく似ていますが、より「手動」の依存性注入とasync/awaitの使用法が異なります。

要約:ここでも、大きな違いはありません。 subscribeとasync/awaitの違いは基本的にすべてです。

フォーム

このアプリケーションのフォームを使用して、次の目標を達成したいと考えています。

  • モデルへのフィールドのデータバインディング
  • 各フィールドの検証メッセージ、複数のルール
  • フォーム全体が有効かどうかを確認するためのサポート

React

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

したがって、formstateライブラリは次のように機能します。フォームの各フィールドに対して、 FieldStateを定義します。 渡されたパラメータは初期値です。 validatorsプロパティは、値が有効な場合は「false」を返し、値が無効な場合は検証メッセージを返す関数を取ります。 checkおよびcheckRequiredヘルパー関数を使用すると、すべてがうまく宣言的に見える可能性があります。

フォーム全体の検証を行うには、これらのフィールドをFormStateインスタンスでラップすることも有益です。これにより、集計の有効性が提供されます。

 @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インスタンスは、 valueonChange 、およびerrorプロパティを提供します。これらは、任意のフロントエンドコンポーネントで簡単に使用できます。

 <Button label='Cancel' onClick={formStore.goBack} raised accent /> &nbsp; <Button label='Submit' onClick={formStore.submit} raised disabled={postFormState.form.hasError} primary /> </div> } }

form.hasErrortrue 、ボタンは無効のままにします。 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.

バンドルサイズ

ああ、もう一つ。 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.