ReactとAngularのどちらがWeb開発に適しているかを議論する記事は無数にあります。もう1つ必要ですか?
私がこの記事を書いた理由は インクルード 記事 公開 すでに(優れた洞察が含まれていますが)、実用的なフロントエンド開発者がニーズに合ったものを決定するのに十分な深さを持っています。
この記事では、AngularとReactの両方が、哲学が大きく異なるものの、同様のフロントエンドの問題を解決することをどのように目指しているか、そしてどちらを選択するかは単に個人的な好みの問題であるかどうかを学びます。それらを比較するために、同じアプリケーションを2回ビルドします。1回はAngularを使用し、もう1回はReactを使用します。
2年前、私はについての記事を書きました Reactエコシステム 。とりわけ、この記事は、Angularが「事前発表による死」の犠牲者になったと主張しました。当時、Angularと他のほとんどすべてのもののどちらを選択するかは、プロジェクトを廃止されたフレームワークで実行したくない人にとっては簡単なものでした。 Angular 1は廃止され、Angular2はアルファ版でも利用できませんでした。
後から考えると、恐れは多かれ少なかれ正当化されました。 Angular 2は劇的に変化し、最終リリースの直前に大幅な書き直しも行われました。
2年後、Angular 4が登場し、今後は比較的安定することが期待されます。
それで?
ReactとAngularを比較することは、リンゴとオレンジを比較するようなものだと言う人もいます。 1つはビューを処理するライブラリですが、もう1つは本格的なフレームワークです。
もちろん、ほとんど React開発者 Reactにいくつかのライブラリを追加して、完全なフレームワークに変換します。繰り返しになりますが、このスタックの結果のワークフローは、Angularとはまだ非常に異なることが多いため、比較可能性は依然として制限されています。
最大の違いは、状態管理にあります。 Angularにはデータバインディングがバンドルされていますが、今日のReactは通常、単方向のデータフローを提供し、不変のデータを処理するためにReduxによって拡張されています。これらはそれ自体が反対のアプローチであり、可変/データバインディングが不変/単方向よりも優れているか悪いかについて、無数の議論が行われています。
Reactはハッキングが簡単なことで有名なので、この比較の目的で、Angularを適度に厳密にミラーリングしてコードスニペットを並べて比較できるReactセットアップを構築することにしました。
目立つがデフォルトではReactにない特定のAngular機能は次のとおりです。
特徴 | Angularパッケージ | Reactライブラリ |
---|---|---|
データバインディング、依存性注入(DI) | @ angular / core | MobX |
計算されたプロパティ | rxjs | MobX |
コンポーネントベースのルーティング | @ angular / router | Reactルーターv4 |
マテリアルデザインコンポーネント | @角度/素材 | React Toolbox |
コンポーネントを対象としたCSS | @ angular / core | CSSモジュール |
フォームの検証 | @ angular / forms | FormState |
プロジェクトジェネレータ | @ angular / cli | React Scripts TS |
データバインディングは、一方向のアプローチよりも開始するのが間違いなく簡単です。もちろん、完全に反対方向に進んで使用することは可能です 戻ってきた または mobx-state-tree Reactで、そして ngrx Angularで。しかし、それは別の投稿のトピックになります。
パフォーマンスが懸念されますが、Angularのプレーンゲッターは、レンダリングごとに呼び出されるため、問題外です。使用可能です BehaviorSubject から RsJS 、それは仕事をします。
Reactを使用すると、使用することが可能です @computed 同じ目的を達成するMobXから、おそらく少し優れたAPIを使用します。
依存性注入は、関数型プログラミングと不変性の現在のReactパラダイムに反するため、物議を醸すものです。結局のところ、ある種の依存性注入は、データバインディング環境ではほとんど不可欠です。これは、個別のデータレイヤーアーキテクチャがない場合のデカップリング(したがって、モックとテスト)に役立つためです。
DI(Angularでサポート)のもう1つの利点は、さまざまなストアのさまざまなライフサイクルを持つことができることです。現在のほとんどのReactパラダイムは、さまざまなコンポーネントにマップするある種のグローバルアプリ状態を使用していますが、私の経験から、コンポーネントのアンマウント時にグローバル状態をクリーンアップするときにバグを導入するのは非常に簡単です。
コンポーネントマウントで作成される(そしてこのコンポーネントの子がシームレスに利用できる)ストアを持つことは非常に便利であり、見過ごされがちな概念のようです。
Angularの箱から出してすぐに使用できますが、MobXでも非常に簡単に再現できます。
コンポーネントベースのルーティングにより、コンポーネントは1つの大きなグローバルルーター構成を使用する代わりに、独自のサブルートを管理できます。このアプローチはついにreact-router
に到達しましたバージョン4では。
いくつかの高レベルのコンポーネントから始めるのはいつでもいいことです。マテリアルデザインは、Google以外のプロジェクトでも、広く受け入れられているデフォルトの選択肢のようなものになっています。
私は意図的に選択しました React Toolbox 通常推奨される以上 マテリアルUI 、マテリアルUIが深刻な自白をしているため パフォーマンスの問題 インラインCSSアプローチで、次のバージョンで解決する予定です。
その上、 PostCSS / cssnext ReactToolboxで使用されているものはとにかくSass / LESSに取って代わり始めています。
CSSクラスはグローバル変数のようなものです。競合を防ぐためにCSSを編成する方法は多数あります( 良い )が、CSSを処理するのに役立つライブラリを使用して、これらの競合を防止するために、 フロントエンドの開発者 精巧なCSSネーミングシステムを考案する。
フォームの検証は重要で、非常に広く使用されている機能です。コードの繰り返しやバグを防ぐために、ライブラリでカバーしておくとよいでしょう。
プロジェクト用のCLIジェネレーターがあると、GitHubからボイラープレートのクローンを作成するよりも少し便利です。
したがって、ReactとAngularで同じアプリケーションを作成します。見事なものは何もありません。誰でも共通のページにメッセージを投稿できるShoutboardだけです。
ここでアプリケーションを試すことができます:
ビジュアルデザイナーとは
ソースコード全体が必要な場合は、GitHubから入手できます。
ReactアプリにもTypeScriptを使用していることに気付くでしょう。 TypeScriptでの型チェックの利点は明らかです。そして今、インポートのより良い処理、async / awaitとrestspreadがついにTypeScript2に到着したので、それはBabel / ES7 /を去ります フロー ほこりの中で。
また、追加しましょう Apolloクライアント GraphQLを使用したいので、両方に。つまり、RESTは素晴らしいのですが、10年ほど経つと、古くなります。
まず、両方のアプリケーションのエントリポイントを見てみましょう。
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( , document.getElementById('root') )
コンポーネントは、MobXでの依存性注入に使用されます。ストアをコンテキストに保存して、Reactコンポーネントが後でそれらを注入できるようにします。はい、Reactコンテキストは(おそらく)使用できます 安全に 。
モジュール宣言がないため、Reactバージョンは少し短くなっています。通常はインポートするだけですぐに使用できます。この種のハードな依存関係が望ましくない場合があるため(テスト)、グローバルなシングルトンストアでは、この数十年前のものを使用する必要がありました。 GoF パターン :
export class AppStore { static instance: AppStore static getInstance() @observable username = 'Mr. User' }
Angularのルーターは注入可能であるため、コンポーネントだけでなく、どこからでも使用できます。反応で同じことを達成するために、私たちは mobx-react-router パッケージ化してrouterStore
を注入します。
概要: 両方のアプリケーションのブートストラップは非常に簡単です。 Reactには、モジュールの代わりにインポートだけを使用するという、よりシンプルなエッジがありますが、後で説明するように、これらのモジュールは非常に便利です。シングルトンを手動で作成するのは少し面倒です。ルーティング宣言の構文に関しては、JSONとJSXは好みの問題です。
したがって、ルートを切り替えるには2つのケースがあります。
を使用した宣言型要素、および必須であり、ルーティング(したがってロケーション)APIを直接呼び出します。
Angular
Home Posts {this.props.children}
React Routerは、activeClassName
を使用してアクティブリンクのクラスを設定することもできます。
ここでは、クラス名を直接指定することはできません。これは、CSSモジュールコンパイラによって一意にされているため、style
を使用する必要があるためです。ヘルパー。これについては後で詳しく説明します。
上で見たように、Reactルーターは要素内の要素を使用します。要素は現在のルートをラップしてマウントするだけなので、現在のコンポーネントのサブルートはちょうど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
に対して、このコンポーネントでのみ使用できるようにします。現在はシングルトンではありませんが、コンポーネントの各インスタンスは、コンポーネントのマウントで新しいコピーを受け取ります。これは、以前の使用による古いデータがないことを意味します。
対照的に、AppService
app.module
に登録されています(上記を参照)、したがって、それはシングルトンであり、アプリケーションの寿命はありますが、すべてのコンポーネントで同じままです。コンポーネントからサービスのライフサイクルを制御できることは、非常に便利ですが、十分に評価されていない概念です。
DIは、TypeScriptタイプで識別されるコンポーネントのコンストラクターにサービスインスタンスを割り当てることで機能します。さらに、public
キーワードはパラメータをthis
に自動割り当てするため、退屈なthis.homeService = homeService
を記述する必要はありません。もう行。
Dashboard
Clicks since last visit: {{homeService.counter}} Click!
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 { homeStore: HomeStore componentWillMount() { this.homeStore = new HomeStore() } render() { return } }
ライフサイクルを正しく管理するには、Angularの例よりも少し多くの作業を行う必要があります。 HomeComponent
をラップしますProvider
の新しいインスタンスを受け取るHomeStore
の内部各マウントに。
interface HomeComponentProps { appStore?: AppStore, homeStore?: HomeStore } @inject('appStore', 'homeStore') @observer export class HomeComponent extends React.Component { render() { const { homeStore, appStore } = this.props return Dashboard
Clicks since last visit: {homeStore.counter} Click! } }
HomeComponent
@observer
を使用します@observable
の変更をリッスンするデコレータプロパティ。
この内部メカニズムは非常に興味深いので、ここで簡単に説明します。 @observable
デコレータは、オブジェクトのプロパティをゲッターとセッターに置き換えます。これにより、呼び出しをインターセプトできます。 @observer
のレンダリング機能の場合拡張コンポーネントが呼び出され、それらのプロパティゲッターが呼び出され、それらを呼び出したコンポーネントへの参照が保持されます。
次に、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
の場合にのみ再計算されます変化します。
{homeStore.counterMessage} Click!
次に、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の例よりも少し冗長であり、サブスクリプションを手動で管理する必要があることです(ここではコンストラクターのように)。
{homeService.counterMessage } Click!
| async
を使用してRxJSサブジェクトを参照する方法に注意してください。パイプ。これはいい感じで、コンポーネントをサブスクライブする必要があるよりもはるかに短いです。 input
コンポーネントは[(ngModel)]
によって駆動されます指令。奇妙に見えますが、実際には非常にエレガントです。 appService.username
への値のデータバインディング、およびユーザー入力イベントからの値の自動割り当てのための単なる構文糖衣。
概要: 計算されたプロパティは、Angular / RxJSよりもReact / MobXで実装する方が簡単ですが、RxJSは、後で理解できる、より便利なFRP機能を提供する場合があります。
テンプレートが互いにどのようにスタックするかを示すために、投稿のリストを表示するPostsコンポーネントを使用してみましょう。
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は、このコンポーネントにスコープされます。つまり、コンテンツはコンポーネントの外部に影響を与えることはできません。
add Hello {{appService.username}}
{{post.title}} {{post.name}} {{post.message}}
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 { postsStore: PostsStore componentWillMount() { this.postsStore = new PostsStore() this.postsStore.initializePosts() } render() { return } }
Reactでも、Provider
を使用する必要がありますPostsStore
を作成するためのアプローチ依存関係「一時的」。 style
として参照されるCSSスタイルもインポートしますおよびappStyle
、JSXでこれらのCSSファイルのクラスを使用できるようにします。
interface PostsComponentProps { appStore?: AppStore, postsStore?: PostsStore } @inject('appStore', 'postsStore') @observer export class PostsComponent extends React.Component { render() { const { postsStore, appStore } = this.props return Hello {appStore.username}
{postsStore.posts.map(post => {post.message} )} } }
当然のことながら、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モジュールローダーは各CSSクラスにランダムな接尾辞を付けて、一意性を保証します。競合を回避する簡単な方法。クラスは、webpackでインポートされたオブジェクトを介して参照されます。これの考えられる欠点の1つは、Angularの例で行ったように、クラスを使用してCSSを作成して拡張することができないことです。一方、これは実際には良いことです。スタイルを適切にカプセル化する必要があるからです。
概要: 個人的には、特にコード補完と型チェックのサポートのために、AngularテンプレートよりもJSXの方が少し好きです。それは本当にキラー機能です。 AngularにはAOTコンパイラが搭載されており、これもいくつかの点を見つけることができます。コード補完もそこにあるものの約半分で機能しますが、JSX / TypeScriptほど完全ではありません。
そのため、このアプリケーションのデータを保存するために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によって作成されました。orderBy
およびfirst
allPosts
のパラメータです関数。 createdAt
Post
の1つですモデルの属性。 first: 5
これは、クエリの最初の5つの結果のみを返すことを意味します。id
、name
、title
、およびmessage
Post
の属性です結果に含めたいモデル。その他の属性は除外されます。すでにご覧のとおり、非常に強力です。チェックアウト このページ GraphQLクエリに慣れるため。
interface Post { id: string name: string title: string message: string } interface PostsQueryResult { allPosts: Array }
はい、TypeScriptの良き市民として、GraphQLの結果のためのインターフェースを作成します。
Angular
@Injectable() export class PostsService { posts = [] constructor(private apollo: Apollo) { } initializePosts() { this.apollo.query({ query: PostsQuery, fetchPolicy: 'network-only' }).subscribe(({ data }) => { this.posts = data.allPosts }) } }
GraphQLクエリはRxJSオブザーバブルであり、サブスクライブします。それは約束のように少し機能しますが、完全ではないので、async/await
を使用するのは運が悪いです。もちろん、まだあります お約束します 、しかしそれはとにかく角度の方法ではないようです。 fetchPolicy: 'network-only'
を設定しますこの場合、データをキャッシュしたくないので、毎回再フェッチします。
React
export class PostsStore { appStore: AppStore @observable posts: Array = [] constructor() { this.appStore = AppStore.getInstance() } async initializePosts() { const result = await this.appStore.apolloClient.query({ query: PostsQuery, fetchPolicy: 'network-only' }) this.posts = result.data.allPosts } }
Reactのバージョンはほとんど同じですが、apolloClient
と同じです。ここではpromiseを使用しているので、async/await
を利用できます。構文。 Reactには、GraphQLクエリを「テープ」するだけのアプローチが他にもあります。 高次コンポーネント 、しかし、データとプレゼンテーション層を少し混ぜすぎているように私には思えました。
概要: RxJSサブスクライブとasync / awaitの考え方はまったく同じです。
共通コード
繰り返しますが、いくつかのGraphQL関連コード:
const AddPostMutation = gql` mutation AddPostMutation($name: String!, $title: String!, $message: String!) { createPost( name: $name, title: $title, message: $message ) { id } } `
ミューテーションの目的は、レコードを作成または更新することです。したがって、いくつかの変数をミューテーションで宣言することは有益です。なぜなら、それらはデータをそれに渡す方法だからです。つまり、name
、title
、およびmessage
があります。 String
として入力された変数。これは、このミューテーションを呼び出すたびに入力する必要があります。 createPost
この場合も、関数はGraphcoolによって定義されます。 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 { render() { const { appStore, formStore } = this.props const { postFormState } = formStore return Create a new post
You are now posting as {appStore.username}
FormState
インスタンスはvalue
、onChange
、およびerror
を提供しますプロパティ。フロントエンドコンポーネントで簡単に使用できます。
} }
form.hasError
の場合true
の場合、ボタンは無効のままにします。送信ボタンは、前に示したGraphQLミューテーションにフォームを送信します。
Angular
Angularでは、FormService
を使用しますおよびFormBuilder
は、@angular/forms
の一部です。パッケージ。
@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' } }
まず、検証メッセージを定義しましょう。
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)] ], }) }
FormBuilder
を使用すると、Reactの例よりもさらに簡潔に、フォーム構造を非常に簡単に作成できます。
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 }
バインド可能な検証メッセージを適切な場所に取得するには、いくつかの処理を行う必要があります。このコードは、いくつかの小さな変更を加えて、公式ドキュメントから取得されています。基本的に、FormServiceでは、フィールドはバリデーター名で識別されるアクティブなエラーのみを参照するため、影響を受けるフィールドに必要なメッセージを手動でペアリングする必要があります。これは完全に欠点ではありません。たとえば、国際化に適しています。
onSubmit({ value, valid }) { if (!valid) { return } this.formService.addPost(value) } onCancel() { this.router.navigate(['/posts']) } }
この場合も、フォームが有効な場合、データをGraphQLミューテーションに送信できます。
Create a new post
You are now posting as {{appService.username}}
{{validationErrors['title']}}
{{validationErrors['message']}}
Cancel Submit
最も重要なことは、FormBuilderで作成したformGroupを参照することです。これは[formGroup]='postForm'
です。割り当て。フォーム内のフィールドは、formControlName
を介してフォームモデルにバインドされます。プロパティ。ここでも、フォームが無効な場合は「送信」ボタンを無効にします。ここでは、ダーティでないフォームがまだ無効である可能性があるため、ダーティチェックも追加する必要があります。ただし、ボタンの初期状態を「有効」にする必要があります。
概要: ReactとAngularのフォームに対するこのアプローチは、検証とテンプレートの両方の面でまったく異なります。 Angularアプローチは、単純なバインディングではなく、もう少し「魔法」を伴いますが、一方で、より完全で徹底的です。
ああ、もう一つ。プロダクションはJSバンドルサイズを縮小し、アプリケーションジェネレーターからのデフォルト設定を使用しました。特に、ReactでのTreeShakingとAngularでのAOTコンパイルです。
さて、ここではそれほど驚くことではありません。 Angularは常にかさばるものでした。
gzipを使用すると、サイズはそれぞれ275kbと127kbになります。
これは基本的にすべてのベンダーライブラリであることを覚えておいてください。比較すると、実際のアプリケーションコードの量は最小限ですが、実際のアプリケーションには当てはまりません。そこでは、比率はおそらく1:4よりも1:2のようになります。また、Reactに多くのサードパーティライブラリを含め始めると、バンドルサイズもかなり急速に大きくなる傾向があります。
そのため、AngularとReactのどちらがWeb開発に適しているかについて、(再び!)明確な答えを出すことができなかったようです。
ReactとAngularの開発ワークフローは、Reactの使用を選択したライブラリに応じて、非常に類似している可能性があることがわかりました。次に、それは主に個人的な好みの問題です。
既製のスタック、強力な依存性注入が好きで、いくつかのRxJSグッズを使用する予定がある場合は、Angularを選択してください。
自分でスタックをいじって構築したい場合は、JSXの単純さが好きで、より単純な計算可能なプロパティを好む場合は、React / MobXを選択してください。
繰り返しになりますが、この記事からアプリケーションの完全なソースコードを入手できます。 ここに そして ここに 。
または、より大きなRealWorldの例が必要な場合は、次のようにします。
React / MobXを使用したプログラミングは、実際にはReact / Reduxを使用した場合よりもAngularに似ています。テンプレートと依存関係の管理にはいくつかの顕著な違いがありますが、それらは同じです 可変/データバインディング パラダイム。
React / Redux with its 不変/一方向 パラダイムは完全に異なる獣です。
Reduxライブラリの小さなフットプリントにだまされないでください。小さいかもしれませんが、それでもフレームワークです。今日のReduxのベストプラクティスのほとんどは、次のようなredux互換ライブラリの使用に焦点を合わせています。 Redux Saga 非同期コードとデータフェッチの場合、 Reduxフォーム フォーム管理用、 再選択 記憶されたセレクター(Reduxの計算値)用。そして 再構成 とりわけ、よりきめ細かいライフサイクル管理のために。また、Reduxコミュニティは Immutable.js に ラムダ または lodash / fp 、プレーンJSオブジェクトを変換する代わりに処理します。
現代のReduxの良い例はよく知られています ボイラープレートに反応する 。これは手ごわい開発スタックですが、見てみると、これまでこの投稿で見たものとは非常に大きく異なります。
Angularは、JavaScriptコミュニティのより声高な部分から少し不公平な扱いを受けていると感じています。それに不満を表明する多くの人々は、おそらく古いAngularJSと今日のAngularの間で起こった大きな変化を理解していません。私の意見では、これは非常にクリーンで生産的なフレームワークであり、1〜2年前に登場した場合に世界を席巻するでしょう。
それでも、Angularは、特に企業の世界で、大きなチームと標準化と長期的なサポートの必要性により、確固たる足場を築いています。言い換えれば、Angularは、Googleのエンジニアが、ウェブ開発を行うべきだと考える方法です。
MobXについても、同様の評価が適用されます。本当に素晴らしいですが、過小評価されています。
結論:ReactとAngularのどちらかを選択する前に、まずプログラミングパラダイムを選択してください。
可変/データバインディング または 不変/一方向 、それは…本当の問題のようです。
Reactは、ユーザーインターフェイスを構築するためのJavaScriptライブラリです。ビューを処理し、フロントエンドアーキテクチャの残りの部分を選択できます。ただし、強力なライブラリエコシステムがその周りに開発されており、いくつかのライブラリを追加することで、Reactの周りに完全なフレームワークを構築できます。
計算されたプロパティの基礎となるすべての値をBehaviorSubject(RxJS経由で利用可能)として定義し、プロパティが依存する各サブジェクトを手動でサブスクライブします。