NgrxとAngular2チュー​​トリアル:リアクティブアプリケーションの構築

公開: 2022-03-11

Angular領域でのリアクティブプログラミングについて多くのことを話します。 リアクティブプログラミングとAngular2は密接に関連しているようです。 ただし、両方のテクノロジーに精通していない人にとっては、それが何であるかを理解することは非常に困難な作業になる可能性があります。

この記事では、Ngrxを使用してリアクティブなAngular 2アプリケーションを構築することで、パターンとは何か、パターンが有用であることが証明できる場所、およびパターンを使用してより優れたAngular2アプリケーションを構築する方法を学習します。

Ngrxは、リアクティブ拡張用のAngularライブラリのグループです。 Ngrx / Storeは、Angular 2のよく知られたRxJSオブザーバブルを使用してReduxパターンを実装します。これは、アプリケーションの状態をプレーンオブジェクトに単純化し、一方向のデータフローを適用するなど、いくつかの利点を提供します。 Ngrx / Effectsライブラリを使用すると、アプリケーションは副作用をトリガーして外界と通信できます。

リアクティブプログラミングとは何ですか?

リアクティブプログラミングは最近よく耳にする用語ですが、実際にはどういう意味ですか?

リアクティブプログラミングは、アプリケーションがアプリケーション内のイベントとデータフローを処理する方法です。 リアクティブプログラミングでは、変更を要求するのではなく、それらの変更に対応するために、ソフトウェアのコンポーネントやその他の部分を設計します。 これは大きな変化になる可能性があります。

ご存知かもしれませんが、リアクティブプログラミングに最適なツールはRxJSです。

このライブラリは、受信データを変換するためのオブザーバブルと多くの演算子を提供することにより、アプリケーションでイベントを処理するのに役立ちます。 実際、オブザーバブルを使用すると、イベントを1回限りのイベントではなく、イベントのストリームとして表示できます。 これにより、たとえば、それらを組み合わせて、聞く新しいイベントを作成できます。

リアクティブプログラミングは、アプリケーションのさまざまな部分の間で通信する方法の変化です。 リアクティブプログラミングでは、データを必要とするコンポーネントまたはサービスに直接データをプッシュするのではなく、データの変更に反応するのはコンポーネントまたはサービスです。

Ngrxについての一言

このチュートリアルで作成するアプリケーションを理解するには、Reduxのコアコ​​ンセプトを簡単に理解する必要があります。

ストアはクライアント側のデータベースと見なすことができますが、さらに重要なことに、アプリケーションの状態を反映しています。 あなたはそれを唯一の正しい情報源として見ることができます。

Reduxパターンに従い、アクションをディスパッチして変更する場合は、これが唯一の変更になります。

レデューサー

レデューサーは、特定のアクションとアプリの以前の状態をどう処理するかを知っている関数です。

レデューサーはストアから以前の状態を取得し、それに純粋関数を適用します。 純粋とは、関数が同じ入力に対して常に同じ値を返し、副作用がないことを意味します。 その純粋関数の結果から、ストアに配置される新しい状態が得られます。

行動

アクションは、ストアを変更するために必要な情報を含むペイロードです。 基本的に、アクションには、リデューサー関数が状態を変更するために実行するタイプとペイロードがあります。

発車係

ディスパッチャは、アクションをディスパッチするための単なるエントリポイントです。 Ngrxには、ストアに直接ディスパッチする方法があります。

ミドルウェア

ミドルウェアは、この記事では使用しない場合でも、副作用を作成するためにディスパッチされる各アクションをインターセプトするいくつかの機能です。 これらはNgrx/Effectライブラリに実装されており、実際のアプリケーションを構築する際に必要になる可能性が高くなります。

なぜNgrxを使用するのですか?

複雑

ストアと一方向のデータフローにより、アプリケーションのパーツ間の結合が大幅に減少します。 各部分は特定の状態のみを考慮しているため、この結合の減少により、アプリケーションの複雑さが軽減されます。

ツーリング

アプリケーションの状態全体が1つの場所に保存されるため、アプリケーションの状態をグローバルに表示するのは簡単で、開発中に役立ちます。 また、Reduxには、ストアを利用して、アプリケーションの特定の状態を再現したり、タイムトラベルを作成したりするのに役立つ多くの優れた開発ツールが付属しています。

アーキテクチャのシンプルさ

Ngrxの利点の多くは、他のソリューションで実現できます。 結局のところ、Reduxはアーキテクチャパターンです。 ただし、協調編集ツールなど、Reduxパターンに最適なアプリケーションを構築する必要がある場合は、パターンに従うことで機能を簡単に追加できます。

何をしているのかを考える必要はありませんが、ディスパッチされたすべてのアクションを追跡できるため、すべてのアプリケーションに分析などを追加するのは簡単です。

小さな学習曲線

このパターンは非常に広く採用されており、シンプルであるため、チームの新しい人々があなたの行動にすばやく追いつくのは非常に簡単です。

Ngrxは、監視ダッシュボードなど、アプリケーションを変更できる外部アクターが多数ある場合に最も効果的です。 このような場合、アプリケーションにプッシュされるすべての受信データを管理することは困難であり、状態管理は困難になります。 そのため、不変の状態で単純化する必要があります。これは、Ngrxストアが提供するものの1つです。

Ngrxを使用したアプリケーションの構築

Ngrxのパワーは、リアルタイムでアプリケーションにプッシュされている外部データがある場合に最も効果的です。 それを念頭に置いて、オンラインのフリーランサーを表示し、それらをフィルタリングできる単純なフリーランサーグリッドを作成しましょう。

プロジェクトの設定

Angular CLIは、セットアッププロセスを大幅に簡素化する素晴らしいツールです。 使用したくない場合もありますが、この記事の残りの部分では使用することに注意してください。

 npm install -g @angular/cli

次に、新しいアプリケーションを作成し、すべてのNgrxライブラリをインストールします。

 ng new toptal-freelancers npm install ngrx --save

フリーランサーレデューサー

レデューサーはReduxアーキテクチャのコア部分なので、アプリケーションを構築する際に最初にレデューサーから始めてみませんか?

まず、アクションがストアにディスパッチされるたびに新しい状態を作成する責任がある「フリーランサー」レデューサーを作成します。

freelancer-grid / freelancers.reducer.ts

 import { Action } from '@ngrx/store'; export interface AppState { freelancers : Array<IFreelancer> } export interface IFreelancer { name: string, email: string, thumbnail: string } export const ACTIONS = { FREELANCERS_LOADED: 'FREELANCERS_LOADED', } export function freelancersReducer( state: Array<IFreelancer> = [], action: Action): Array<IFreelancer> { switch (action.type) { case ACTIONS.FREELANCERS_LOADED: // Return the new state with the payload as freelancers list return Array.prototype.concat(action.payload); default: return state; } }

これがフリーランサーのレデューサーです。

この関数は、ストアを通じてアクションがディスパッチされるたびに呼び出されます。 アクションがFREELANCERS_LOADEDの場合、アクションペイロードから新しい配列が作成されます。 そうでない場合は、古い状態参照が返され、何も追加されません。

ここで重要なのは、古い状態参照が返された場合、状態は変更されていないと見なされることに注意してください。 これは、 state.push(something)を呼び出した場合、状態が変更されたとは見なされないことを意味します。 レデューサー機能を実行するときは、このことを覚えておいてください。

状態は不変です。 変更するたびに新しい状態を返す必要があります。

フリーランサーグリッドコンポーネント

オンラインフリーランサーを表示するグリッドコンポーネントを作成します。 最初は、ストアにあるものだけが反映されます。

 ng generate component freelancer-grid

以下をfreelancer-grid.component.tsに入れてください

import { Component, OnInit } from '@angular/core'; import { Store } from '@ngrx/store'; import { AppState, IFreelancer, ACTIONS } from './freelancer-reducer'; import * as Rx from 'RxJS'; @Component({ selector: 'app-freelancer-grid', templateUrl: './freelancer-grid.component.html', styleUrls: ['./freelancer-grid.component.scss'], }) export class FreelancerGridComponent implements OnInit { public freelancers: Rx.Observable<Array<IFreelancer>>; constructor(private store: Store<AppState>) { this.freelancers = store.select('freelancers'); } }

そして、 freelancer-grid.component.htmlの次の:

 <span class="count">Number of freelancers online: {{(freelancers | async).length}}</span> <div class="freelancer fade thumbail" *ngFor="let freelancer of freelancers | async"> <button type="button" class="close" aria-label="Close" (click)="delete(freelancer)"><span aria-hidden="true">&times;</span></button><br> <img class="img-circle center-block" src="{{freelancer.thumbnail}}" /><br> <div class="info"><span><strong>Name: </strong>{{freelancer.name}}</span> <span><strong>Email: </strong>{{freelancer.email}}</span></div> <a class="btn btn-default">Hire {{freelancer.name}}</a> </div>

それで、あなたはただ何をしましたか?

まず、 freelancer-gridという新しいコンポーネントを作成しました。

コンポーネントには、Ngrxストアに含まれるアプリケーション状態の一部であるfreelancersという名前のプロパティが含まれています。 select演算子を使用すると、アプリケーション全体の状態のfreelancersプロパティによってのみ通知されるように選択できます。 そのため、アプリケーションの状態のfreelancersプロパティが変更されるたびに、observableに通知されます。

このソリューションの優れている点の1つは、コンポーネントの依存関係が1つしかないことです。これにより、コンポーネントの複雑さが大幅に軽減され、簡単に再利用できるようになります。

テンプレートの部分では、あまり複雑なことは何もしていません。 *ngForで非同期パイプが使用されていることに注意してください。 観察可能なfreelancersは直接反復可能ではありませんが、Angularのおかげで、非同期パイプを使用して、それをアンラップし、domをその値にバインドするツールがあります。 これにより、オブザーバブルの操作が非常に簡単になります。

フリーランサーの削除機能の追加

機能ベースができたので、アプリケーションにいくつかのアクションを追加しましょう。

州からフリーランサーを削除できるようにしたいと考えています。 Reduxの動作に応じて、最初に、影響を受ける各状態でそのアクションを定義する必要があります。

この場合、それはfreelancersのレデューサーだけです:

 export const ACTIONS = { FREELANCERS_LOADED: 'FREELANCERS_LOADED', DELETE_FREELANCER: 'DELETE_FREELANCER', } export function freelancersReducer( state: Array<IFreelancer> = [], action: Action): Array<IFreelancer> { switch (action.type) { case ACTIONS.FREELANCERS_LOADED: // Return the new state with the payload as freelancers list return Array.prototype.concat(action.payload); case ACTIONS.DELETE_FREELANCER: // Remove the element from the array state.splice(state.indexOf(action.payload), 1); // We need to create another reference return Array.prototype.concat(state); default: return state; } }

ここでは、新しい不変の状態にするために、古い配列から新しい配列を作成することが非常に重要です。

これで、このアクションをストアにディスパッチするフリーランサーの削除機能をコンポーネントに追加できます。

 delete(freelancer) { this.store.dispatch({ type: ACTIONS.DELETE_FREELANCER, payload: freelancer, }) }

シンプルに見えませんか?

これで、特定のフリーランサーを状態から削除でき、その変更はアプリケーション全体に伝播されます。

では、アプリケーションに別のコンポーネントを追加して、ストアを介してそれらがどのように相互作用できるかを確認するとどうなるでしょうか。

フィルターレデューサー

いつものように、レデューサーから始めましょう。 そのコンポーネントの場合、それは非常に単純です。 レデューサーは、ディスパッチしたプロパティのみを含む新しい状態を常に返す必要があります。 次のようになります。

 import { Action } from '@ngrx/store'; export interface IFilter { name: string, email: string, } export const ACTIONS = { UPDATE_FITLER: 'UPDATE_FITLER', CLEAR_FITLER: 'CLEAR_FITLER', } const initialState = { name: '', email: '' }; export function filterReducer( state: IFilter = initialState, action: Action): IFilter { switch (action.type) { case ACTIONS.UPDATE_FITLER: // Create a new state from payload return Object.assign({}, action.payload); case ACTIONS.CLEAR_FITLER: // Create a new state from initial state return Object.assign({}, initialState); default: return state; } }

フィルタコンポーネント

import { Component, OnInit } from '@angular/core'; import { IFilter, ACTIONS as FilterACTIONS } from './filter-reducer'; import { Store } from '@ngrx/store'; import { FormGroup, FormControl } from '@angular/forms'; import * as Rx from 'RxJS'; @Component({ selector: 'app-filter', template: '<form class="filter">'+ '<label>Name</label>'+ '<input type="text" [formControl]="name" name="name"/>'+ '<label>Email</label>'+ '<input type="text" [formControl]="email" name="email"/>'+ '<a (click)="clearFilter()" class="btn btn-default">Clear Filter</a>'+ '</form>', styleUrls: ['./filter.component.scss'], }) export class FilterComponent implements OnInit { public name = new FormControl(); public email = new FormControl(); constructor(private store: Store<any>) { store.select('filter').subscribe((filter: IFilter) => { this.name.setValue(filter.name); this.email.setValue(filter.email); }) Rx.Observable.merge(this.name.valueChanges, this.email.valueChanges).debounceTime(1000).subscribe(() => this.filter()); } ngOnInit() { } filter() { this.store.dispatch({ type: FilterACTIONS.UPDATE_FITLER, payload: { name: this.name.value, email: this.email.value, } }); } clearFilter() { this.store.dispatch({ type: FilterACTIONS.CLEAR_FITLER, }) } }

まず、状態を反映する2つのフィールド(名前と電子メール)を持つフォームを含む単純なテンプレートを作成しました。

freelancersの状態で行ったのとはかなり異なる方法で、これらのフィールドを状態と同期させます。 実際、これまで見てきたように、フィルター状態をサブスクライブし、そのたびに、 formControlに新しい値を割り当てるトリガーになります。

Angular 2の優れている点の1つは、オブザーバブルと対話するための多くのツールを提供することです。

以前に非同期パイプを見てきましたが、入力の値を監視できるようにするformControlクラスが表示されます。 これにより、フィルターコンポーネントで行ったような凝ったことが可能になります。

ご覧のとおり、 Rx.observable.mergeを使用してformControlsによって指定された2つのオブザーバブルを結合し、 filter関数をトリガーする前にその新しいオブザーバブルをデバウンスします。

簡単に言うと、名前または電子メールformControlのいずれかが変更されてから1秒待ってから、 filter関数を呼び出します。

それは素晴らしいことではありませんか?

これらはすべて、数行のコードで実行されます。 これが、RxJSを気に入る理由の1つです。 それはあなたがそうでなければもっと複雑だったであろうそれらの派手なことの多くを簡単に行うことを可能にします。

それでは、そのフィルター関数に移りましょう。 それは何をするためのものか?

名前と電子メールの値を使用してUPDATE_FILTERアクションをディスパッチするだけで、レデューサーがその情報を使用して状態を変更します。

もっと面白いことに移りましょう。

そのフィルターを以前に作成したフリーランサーグリッドとどのように相互作用させますか?

単純。 あなたは店のフィルター部分を聞く必要があるだけです。 コードがどのように見えるか見てみましょう。

 import { Component, OnInit } from '@angular/core'; import { Store } from '@ngrx/store'; import { AppState, IFreelancer, ACTIONS } from './freelancer-reducer'; import { IFilter, ACTIONS as FilterACTIONS } from './../filter/filter-reducer'; import * as Rx from 'RxJS'; @Component({ selector: 'app-freelancer-grid', templateUrl: './freelancer-grid.component', styleUrls: ['./freelancer-grid.component.scss'], }) export class FreelancerGridComponent implements OnInit { public freelancers: Rx.Observable<Array<IFreelancer>>; public filter: Rx.Observable<IFilter>; constructor(private store: Store<AppState>) { this.freelancers = Rx.Observable.combineLatest(store.select('freelancers'), store.select('filter'), this.applyFilter); } applyFilter(freelancers: Array<IFreelancer>, filter: IFilter): Array<IFreelancer> { return freelancers .filter(x => !filter.name || x.name.toLowerCase().indexOf(filter.name.toLowerCase()) !== -1) .filter(x => !filter.email || x.email.toLowerCase().indexOf(filter.email.toLowerCase()) !== -1) } ngOnInit() { } delete(freelancer) { this.store.dispatch({ type: ACTIONS.DELETE_FREELANCER, payload: freelancer, }) } }

それ以上に複雑ではありません。

ここでも、RxJSの機能を使用して、フィルターとフリーランサーの状態を組み合わせました。

実際、 combineLatestは、2つのオブザーバブルのいずれかが起動した場合に起動し、 applyFilter関数を使用して各状態を結合します。 そうする新しいオブザーバブルを返します。 他のコード行を変更する必要はありません。

コンポーネントが、フィルターの取得、変更、または保存の方法を気にしないことに注意してください。 他の状態の場合と同じように、それをリッスンするだけです。 フィルタ機能を追加しただけで、新しい依存関係は追加しませんでした。

輝かせる

リアルタイムデータを処理する必要がある場合、Ngrxの使用は本当に輝いていることを覚えていますか? その部分をアプリケーションに追加して、どのように動作するかを見てみましょう。

freelancers-serviceのご紹介。

 ng generate service freelancer

フリーランサーサービスは、データのリアルタイム操作をシミュレートし、次のようになります。

 import { Injectable } from '@angular/core'; import { Store } from '@ngrx/store'; import { AppState, IFreelancer, ACTIONS } from './freelancer-grid/freelancer-reducer'; import { Http, Response } from '@angular/http'; @Injectable() export class RealtimeFreelancersService { private USER_API_URL = 'https://randomuser.me/api/?results=' constructor(private store: Store<AppState>, private http: Http) { } private toFreelancer(value: any) { return { name: value.name.first + ' ' + value.name.last, email: value.email, thumbail: value.picture.large, } } private random(y) { return Math.floor(Math.random() * y); } public run() { this.http.get(`${this.USER_API_URL}51`).subscribe((response) => { this.store.dispatch({ type: ACTIONS.FREELANCERS_LOADED, payload: response.json().results.map(this.toFreelancer) }) }) setInterval(() => { this.store.select('freelancers').first().subscribe((freelancers: Array<IFreelancer>) => { let getDeletedIndex = () => { return this.random(freelancers.length - 1) } this.http.get(`${this.USER_API_URL}${this.random(10)}`).subscribe((response) => { this.store.dispatch({ type: ACTIONS.INCOMMING_DATA, payload: { ADD: response.json().results.map(this.toFreelancer), DELETE: new Array(this.random(6)).fill(0).map(() => getDeletedIndex()), } }); this.addFadeClassToNewElements(); }); }); }, 10000); } private addFadeClassToNewElements() { let elements = window.document.getElementsByClassName('freelancer'); for (let i = 0; i < elements.length; i++) { if (elements.item(i).className.indexOf('fade') === -1) { elements.item(i).classList.add('fade'); } } } }

このサービスは完璧ではありませんが、それが行うことを実行し、デモの目的で、いくつかのことを示すことができます。

まず、このサービスは非常に簡単です。 ユーザーAPIを照会し、結果をストアにプッシュします。 それは簡単で、データがどこに行くのかを考える必要はありません。 それは店に行きます、それはReduxをとても便利で危険なものにするものですが、後でこれに戻ります。 10秒ごとに、サービスは数人のフリーランサーを選び、それらを削除する操作を他の数人のフリーランサーに送信します。

レデューサーで処理できるようにする場合は、次のように変更する必要があります。

 import { Action } from '@ngrx/store'; export interface AppState { freelancers : Array<IFreelancer> } export interface IFreelancer { name: string, email: string, } export const ACTIONS = { LOAD_FREELANCERS: 'LOAD_FREELANCERS', INCOMMING_DATA: 'INCOMMING_DATA', DELETE_FREELANCER: 'DELETE_FREELANCER', } export function freelancersReducer( state: Array<IFreelancer> = [], action: Action): Array<IFreelancer> { switch (action.type) { case ACTIONS.INCOMMING_DATA: action.payload.DELETE.forEach((index) => { state.splice(state.indexOf(action.payload), 1); }) return Array.prototype.concat(action.payload.ADD, state); case ACTIONS.FREELANCERS_LOADED: // Return the new state with the payload as freelancers list return Array.prototype.concat(action.payload); case ACTIONS.DELETE_FREELANCER: // Remove the element from the array state.splice(state.indexOf(action.payload), 1); // We need to create another reference return Array.prototype.concat(state); default: return state; } }

これで、このような操作を処理できるようになりました。

そのサービスで示されていることの1つは、状態変更のすべてのプロセスが同期的に実行される中で、それに注意することが非常に重要であるということです。 状態の適用が非同期の場合、 this.addFadeClassToNewElements();の呼び出し。 この関数が呼び出されたときにDOM要素が作成されないため、は機能しません。

個人的には、予測可能性が向上するため、非常に便利です。

アプリケーションの構築、反応的な方法

このチュートリアルを通じて、Ngrx、RxJS、およびAngular2を使用してリアクティブアプリケーションを構築しました。

ご覧のとおり、これらは強力なツールです。 ここで構築したものは、Reduxアーキテクチャの実装と見なすこともでき、Redux自体が強力です。 ただし、いくつかの制約もあります。 Ngrxを使用している間、これらの制約は、使用するアプリケーションの一部に必然的に反映されます。

リアクティブパラダイム

上の図は、先ほど行ったアーキテクチャの大まかなものです。

一部のコンポーネントが互いに影響し合っている場合でも、それらは互いに独立していることに気付くかもしれません。 これは、このアーキテクチャの特徴です。コンポーネントは、ストアである共通の依存関係を共有します。

このアーキテクチャのもう1つの特別な点は、関数を呼び出すのではなく、アクションをディスパッチすることです。 Ngrxの代わりに、アプリケーションのオブザーバブルを使用して特定の状態を管理するサービスのみを作成し、アクションではなくそのサービスの関数を呼び出すこともできます。 このようにして、問題のある状態を分離しながら、状態の集中化と反応性を得ることができます。 このアプローチは、レデューサーを作成するオーバーヘッドを削減し、アクションをプレーンオブジェクトとして記述するのに役立ちます。

アプリケーションの状態がさまざまなソースから更新されているように感じ、それが混乱し始めた場合、Ngrxが必要です。

関連:すべての特典、面倒なし:Angular9チュートリアル