
このチュートリアルでは、ReduxとImmutable-jsを使用してフルスタックアプリケーションをゼロから作成する方法を説明します。 TDDアプローチを適用して、Node + ReduxバックエンドおよびReact + Reduxフロントエンドアプリケーションを構築するすべての段階を実行します。 さらに、ES6、 Babel 、 Socket.io 、 Webpack 、 Mochaなどのツールを使用します。 セットは非常に好奇心is盛で、すぐにマスターできます!
記事の内容
1.必要なもの
2.アプリケーション
3.アーキテクチャ
4.サーバーアプリケーション
4.1。 アプリケーション状態ツリーを開発する
4.2。 プロジェクトのセットアップ
4.3。 不変データの紹介
4.4。 純粋な関数を使用したアプリケーションロジックの実装
4.4.1。 レコードをダウンロードする
4.4.2。 投票開始
4.4.3。 投票
4.4.4。 次のペアに行く
4.4.5。 投票の完了
4.5。 アクションとレデューサーの使用
4.6。 味覚抑制剤組成
4.7。 Reduxストアの使用
4.8。 Socket.ioサーバーを構成する
4.9。 Reduxリスナーからのブロードキャストステータス
4.10。 Reduxリモートアクションの取得
5.クライアントアプリケーション
5.1。 クライアントプロジェクトのセットアップ
5.1.1。 単体テストのサポート
5.2。 反応および反応ホットローダー
5.3。 投票画面のインターフェースを作成する
5.4。 不変データと純粋なレンダリング
5.5。 結果画面のインターフェースの作成とルーティングの処理
5.6。 クライアントRedux-Storeの使用
5.7。 ReduxからReactに入力を渡す
5.8。 Socket.ioクライアントの構成
5.9。 サーバーからアクションを取得する
5.10。 Reactコンポーネントからアクションを渡す
5.11。 Reduxミドルウェアを使用してサーバーにアクションを送信する
6.演習
1.必要なもの
このガイドは、JavaScriptアプリケーションの作成方法をすでに知っている開発者に最も役立ちます。 すでに述べたように、Node、ES6、 React 、 Webpack 、 Babelを使用します。これらのツールに少しでも精通していれば、プロモーションに問題はありません。 慣れていない場合でも、途中で基本を理解できます。
React、Webpack、ES6を使用してWebアプリケーションを開発するための良いガイドとして、SurviveJSをお勧めします 。 ツールに関しては、NPMを備えたNodeとお好みのテキストエディターが必要です。
2.アプリケーション
パーティー、会議、会議、その他の会議でのライブ投票の申し込みを行います。 アイデアは、ユーザーに投票ポジションのコレクションが提供されるということです:映画、歌、プログラミング言語、 Horse JSからの引用など。 アプリケーションは、すべての人がお気に入りに投票できるように、要素をペアで配置します。 一連の投票の結果、1つの要素(勝者)が残ります。 ダニー・ボイルの最高の映画に投票する例:

アプリケーションには、2つの異なるユーザーインターフェイスがあります。
- 投票インターフェースは、Webブラウザーを起動する任意のデバイスで使用できます。
- 投票結果のインターフェースは、プロジェクターまたはある種の大きなスクリーンに表示できます。 投票結果はリアルタイムで更新されます。

3.アーキテクチャ
構造的に、システムは2つのアプリケーションで構成されます。
- 両方のユーザーインターフェイスを提供するReactブラウザーアプリケーション。
- 投票ロジックを含むノードサーバーアプリケーション。
アプリケーション間の相互作用は、WebSocketを使用して実行されます。 Reduxは、クライアントとサーバーのコードを整理するのに役立ちます。 そして、状態を保存するために、 不変の構造を使用します。
クライアントとサーバーの大きな類似性にもかかわらず-たとえば、両方がReduxを使用します-これは汎用/同形アプリケーションではなく、アプリケーションはコードを共有しません。 むしろ、メッセージングを介して相互に作用する2つのアプリケーションの分散システムとして説明できます。
4.サーバーアプリケーション
最初にNodeアプリケーションを作成し、次にReactを作成します。 これにより、インターフェイスに移る前に、基本的なアプリケーションロジックの実装に気を取られないようになります。 サーバーアプリケーションを作成しているので、ReduxとImmutableに精通し、それらに基づいて構築されたアプリケーションがどのように配置されるかを調べます。 Reduxは通常Reactプロジェクトに関連付けられていますが、その使用はそれらに限定されません。 特に、Reduxが他のコンテキストでどのように役立つかを調べます!
このガイドを読みながら、アプリケーションをゼロから作成することをお勧めしますが、 GitHubからソースをダウンロードできます。
4.1。 アプリケーション状態ツリーを開発する
Reduxを使用したアプリケーションの作成は、多くの場合、アプリケーション状態データ構造の構造を検討することから始まります 。 その助けを借りて、アプリケーションのあらゆる瞬間に何が起こるかを説明します。 状態とアーキテクチャには状態があります。 EmberおよびBackboneに基づくアプリケーションでは、状態はモデルに保存されます。 Angularベースのアプリケーションでは、状態はほとんどの場合ファクトリーとサービスに保存されます。 ほとんどのFluxアプリケーションでは、状態はストア(ストア)です。 これはReduxでどのように行われますか?
主な違いは、すべてのアプリケーションの状態が単一のツリー構造に保存されることです。 したがって、アプリケーションの状態について知る必要があるものはすべて、連想配列(マップ)と通常の配列の1つのデータ構造に含まれています。 すぐにわかるように、この決定には多くの結果があります。 最も重要なことの1つは、アプリケーションの状態と動作を分離できることです。 状態は純粋なデータです。 メソッドや関数は含まれておらず、他のオブジェクト内に隠されていません。 すべてが1か所にあります。 これは、特にオブジェクト指向プログラミングの経験がある場合、制限のように思えるかもしれません。 しかし、これは実際にはより大きな自由の現れです。なぜなら、データだけに集中できるからです。 十分な時間を割くと、アプリケーションの状態の設計から多くのことが論理的に流れます。
最初に常に状態ツリーを完全に開発し、次に残りのアプリケーションコンポーネントを作成する必要があるとは言いたくありません。 これは通常、並行して行われます。 しかし、コードを書き始める前に、さまざまな状況でツリーがどのように見えるべきかを最初に一般的に概説する方がより便利であるように思えます。 投票アプリケーションに状態ツリーがどのようになるか想像してみましょう。 アプリケーションの目的は、オブジェクトのペア(映画、音楽グループ)の中で投票できるようにすることです。 アプリケーションの初期状態として、投票に参加するポジションのコレクションを作成することをお勧めします。 このエントリのコレクションを呼び出します 。

投票の開始後、現在投票に参加しているポジションを何らかの形で分ける必要があります。 状態は、ユーザーがどちらかを選択する必要がある位置のペアを含む投票エンティティである場合があります。 当然、このペアはエントリコレクションから抽出する必要があります 。

また、投票結果の記録を保持する必要があります。 これは、 vote内の別の構造を使用して実行できます。

現在の投票の終わりに、負けたエントリーは破棄され、勝ったエントリーはエントリーに戻され、リストの最後に置かれます 。 後で、彼女は再び投票します。 次に、リストから次のペアが取得されます。

コレクションにエントリがある限り、これらの状態は循環的に相互に置き換えられます。 最終的に、勝者として宣言されたエントリは1つだけになり、投票は終了します。

スキームは非常に合理的であると思われるので、実装を始めましょう。 これらの要件の状態を開発するにはさまざまな方法がありますが、おそらくこのオプションは最適ではありません。 しかし、これは特に重要ではありません。 最初のアウトラインは、始めるのにちょうど良いはずです。 主なことは、アプリケーションがどのように機能するかを理解していることです。 そして、これはコードを書き始める前です!
4.2。 プロジェクトのセットアップ
袖をまくりましょう。 最初にプロジェクトフォルダーを作成し、それをNPMプロジェクトとして初期化する必要があります。
mkdir voting-server cd voting-server npm init -y
作成されたフォルダーには、とりあえず、唯一の
package.json
ファイルがあります。 ES6仕様でコードを記述します。 Nodeはバージョン4.0.0以降、多くのES6機能をサポートしていますが、必要なモジュールはまだ残っています。 したがって、ES6の全機能を使用してコードをES5に変換できるように、Babelをプロジェクトに追加する必要があります。
npm install --save-dev babel-core babel-cli babel-preset-es2015
単体テストを作成するためのライブラリも必要になります。
npm install --save-dev mocha chai
Mochaをテストフレームワークとして使用します。 テストの中で、 Chaiをライブラリとして使用して、予想される動作と条件をテストします。
mocha
を使用してテストを実行します。
./node_modules/mocha/bin/mocha --compilers js:babel-core/register --recursive
その後、Mochaはすべてのプロジェクトテストを再帰的に検索して実行します。 Babelは、ES6コードを起動する前にトランスパイルするために使用されます。 便宜上、このコマンドを
package.json
保存できます。
package.json "scripts": { "test": "mocha --compilers js:babel-core/register --recursive" },
次に、BabelでES6 / ES2015のサポートを含める必要があります。 これを行うには、既にインストールされているパッケージ
babel-preset-es2015
ます。 次に、
"babel"
セクションを
package.json
追加します。
package.json "babel": { "presets": ["es2015"] }
npm
コマンドを使用して、テストを実行できます。
npm run test
test:watch
を使用して、コードの変更を追跡し、各変更後にテストを実行するプロセスを開始できます。
package.json "scripts": { "test": "mocha --compilers js:babel-core/register --recursive", "test:watch": "npm run test -- --watch" },
Facebookが開発した不変ライブラリは、多くの有用なデータ構造を提供します。 これについては次の章で説明しますが、とりあえず、不変の構造をChaiと比較するためのサポートを追加するchai不変ライブラリとともにプロジェクトに追加します。
npm install --save immutable npm install --save-dev chai-immutable
テストを実行する前に、chai-immutableを接続する必要があります。
test_helper
ファイルを使用してこれを行うことができます。
test/test_helper.js import chai from 'chai'; import chaiImmutable from 'chai-immutable'; chai.use(chaiImmutable);
テストを実行する前に、Mochaにこのファイルをロードさせます。
package.json "scripts": { "test": "mocha --compilers js:babel-core/register --require ./test/test_helper.js --recursive", "test:watch": "npm run test -- --watch" },
これで、すべてを開始できます。
4.3。 不変データの紹介
Reduxのアーキテクチャに関連する2番目の重要な点:状態は単なるツリーではなく、 不変のツリー(不変のツリー)です。 前の章のツリーの構造は、連想配列内の要素の置換、配列からの削除など、ツリーを更新するだけでコードがアプリケーションの状態を変更することを示唆している場合があります。 しかし、Reduxでは、すべてが異なる方法で行われます。 Reduxアプリケーションの状態ツリーは、 不変のデータ構造です。 これは、ツリーが存在している間は変更されないことを意味します。 常に同じ状態を維持します。 また、別の状態への遷移は、必要な変更が加えられた別のツリーを作成することにより実行されます。 つまり、2つの連続したアプリケーション状態が2つの独立した独立したツリーに格納されます。 また、ツリー間の切り替えは、現在の状態を取得して次の状態を返す 関数を呼び出すことによって行われます 。

これはいいアイデアですか? 通常、すべての状態が1つのツリーに保存され、これらすべての安全な更新を行うと、アプリケーションの状態の履歴を簡単に保存できることがすぐに示されます。 これにより、「無料」で元に戻す/やり直しを実装できます。履歴から前または次の状態(ツリー)を設定するだけです。 また、ストーリーをシリアル化して将来のために保存したり、後で再生するためにリポジトリに保存したりすることもできます。これはデバッグに非常に役立ちます。
しかし、これらすべての追加機能に加えて、不変データを使用する主な利点はコードの簡素化であるように思えます。 純粋な関数をプログラムする必要があります 。それらはデータの受け取りと返しのみを行い、それ以上は行いません。 これらの関数は予測どおりに動作します。 何度でも呼び出すことができ、常に同じように動作します。 それらに同じ引数を与えると、同じ結果が得られます。 関数を呼び出すための「ユニバースを準備する」ためにスタブやその他の偽物を設定する必要がないため、テストは簡単になります。 入力と出力だけがあります。
不変の構造を使用してアプリケーションの状態を説明するので、作業を説明するためにいくつかの単体テストを作成して、それらを知るのに時間をかけましょう。
不変データと不変ライブラリを自信を持って使用している場合は、次のセクションに進むことができます。
不変性の概念を理解するために、まず最も単純なデータ構造について話をすることができます。 状態が数値であるカウンターアプリケーションがあるとします。 0から1、2、3のように変化するとします。 基本的に、数字はすでに不変のデータと考えています。 カウンターが増加しても、数値は変化しません。 はい、数字には「セッター」がないため、これは不可能です。
42.setValue(43)
と言うことはできません。
したがって、前の番号に1を追加して、 異なる番号を取得するだけです。 これは、純粋な関数を使用して実行できます。 彼女の引数は現在の状態になり、戻り値は次の状態として使用されます。 呼び出された関数は、現在の状態を変更しません。 彼女の例と彼女の単体テストは次のとおりです。
test/immutable_spec.js import {expect} from 'chai'; describe('immutability', () => { describe('a number', () => { function increment(currentState) { return currentState + 1; } it('is immutable', () => { let state = 42; let nextState = increment(state); expect(nextState).to.equal(43); expect(state).to.equal(42); }); }); });
数値は不変なので、
increment
呼び出されても
state
は変化しません。
ご覧のとおり、このテストはアプリケーションでは何も行いません。まだ作成していません。
テストは私たちにとって単なる学習ツールになります。 いくつかのアイデアを実行する単体テストを作成することで、新しいAPIやテクニックを学ぶことはしばしば便利です。 本「 テスト駆動開発」では、このようなテストは「トレーニングテスト」と呼ばれます。
ここで、不変性の概念を、数字だけでなく、あらゆる種類のデータ構造に拡張します。
不変リストを使用して、たとえば、状態が映画のリストであるアプリケーションを作成できます。 新しいムービーを追加する操作により、 新しいリストが作成されます。 これは、古いリストと追加する位置の組み合わせです 。 この操作の後、古い状態は変わらないことに注意することが重要です:
test/immutable_spec.js import {expect} from 'chai'; import {List} from 'immutable'; describe('immutability', () => { // ... describe('A List', () => { function addMovie(currentState, movie) { return currentState.push(movie); } it('is immutable', () => { let state = List.of('Trainspotting', '28 Days Later'); let nextState = addMovie(state, 'Sunshine'); expect(nextState).to.equal(List.of( 'Trainspotting', '28 Days Later', 'Sunshine' )); expect(state).to.equal(List.of( 'Trainspotting', '28 Days Later' )); }); }); });
そして、ムービーを通常の配列に挿入すると、古い状態が変更されます。 ただし、代わりにImmutableのリストを使用するため、前の例の数字と同じセマンティクスを使用します。
通常の配列に貼り付けると、古い状態が変わります。 しかし、不変リストを使用しているため、数値の例と同じセマンティクスを持っています。
この考え方は、本格的な状態ツリーにも当てはまります。 ツリーは、リスト、連想配列( マップ )、およびその他のタイプのコレクションのネスト構造です。 これに適用される操作により、 新しい状態ツリーが作成され、前のツリーは変更されません。 ツリーがムービーのリストを含むムービーキーを持つ連想配列である場合、新しい位置を追加すると、ムービーキーが新しいリストを指す新しい配列を作成する必要があります。
test/immutable_spec.js import {expect} from 'chai'; import {List, Map} from 'immutable'; describe('immutability', () => { // ... describe('a tree', () => { function addMovie(currentState, movie) { return currentState.set( 'movies', currentState.get('movies').push(movie) ); } it('is immutable', () => { let state = Map({ movies: List.of('Trainspotting', '28 Days Later') }); let nextState = addMovie(state, 'Sunshine'); expect(nextState).to.equal(Map({ movies: List.of( 'Trainspotting', '28 Days Later', 'Sunshine' ) })); expect(state).to.equal(Map({ movies: List.of( 'Trainspotting', '28 Days Later' ) })); }); }); });
ここでは、ネストされた構造の操作を示すために拡張された、以前とまったく同じ動作を確認します。 不変性の概念は、すべての形状とサイズのデータに適用されます。
このようなネストされた構造の操作のために、Immutableには、更新された値を取得するためにネストされたデータに簡単に「アクセス」できるようにするいくつかの補助関数があります。 簡潔にするために、 更新機能を使用できます。
test/immutable_spec.js function addMovie(currentState, movie) { return currentState.update('movies', movies => movies.push(movie)); }
アプリケーションで同様の関数を使用して、アプリケーションの状態を更新します。 Immutable APIは他の多くの機能を隠しているので、氷山の一角に注目しました。
不変データはReduxアーキテクチャの重要な側面ですが、不変ライブラリを使用するための厳密な要件はありません。 Reduxの公式ドキュメントでは、大部分が単純なJavaScriptオブジェクトと配列に言及しており、慣例により変更することは控えています。
不変ライブラリがマニュアルで使用される理由はいくつかあります。
- Immutableのデータ構造は、最初から不変であるように設計されており、それらに対して簡単に操作を実行できるAPIを提供します。
- Rich Haykiの観点を共有しますが、それには合意による不変性などはありません 。 変更可能なデータ構造を使用すると、遅かれ早かれ誰かが間違いを犯してしまいます。 特にあなたが初心者なら。 Object.freeze()のようなものは、間違いを犯さないために役立ちます。
- 不変のデータ構造は永続的です 。つまり、その内部構造は、特に大きな状態ツリーの場合、新しいバージョンの作成が時間とメモリ消費の観点から効果的な操作であるようなものです。 通常のオブジェクトと配列を使用すると、コピーが過剰になり、パフォーマンスが低下する可能性があります。
4.4。 純粋な関数を使用したアプリケーションロジックの実装
不変の状態ツリーとこれらのツリーで動作する関数の概念を理解したら、アプリケーションのロジックの作成に進むことができます。 これは、上記で説明したコンポーネント、つまりツリー構造と、このツリーの新しいバージョンを作成する一連の関数に基づいています。
4.4.1。 レコードをダウンロードする
まず、アプリケーションは投票エントリのコレクションを「ダウンロード」する必要があります。
setEntries
関数
setEntries
以前の状態とコレクションを
setEntries
そこにレコードを含めることで新しい状態を作成できます。 この関数のテストは次のとおりです。
test/core_spec.js import {List, Map} from 'immutable'; import {expect} from 'chai'; import {setEntries} from '../src/core'; describe('application logic', () => { describe('setEntries', () => { it(' ', () => { const state = Map(); const entries = List.of('Trainspotting', '28 Days Later'); const nextState = setEntries(state, entries); expect(nextState).to.equal(Map({ entries: List.of('Trainspotting', '28 Days Later') })); }); }); });
setEntries
の初期実装では、最も単純な処理のみが行われます。状態連想配列の
entries
キーには、指定されたエントリのリストが値として割り当てられます。 以前に設計された最初のツリーを取得します。
src/core.js export function setEntries(state, entries) { return state.set('entries', entries); }
便宜上、入力エントリを通常のJavaScript配列(または反復可能なもの)にします。 状態ツリーには、不変リスト(
List
)が存在する必要があります。
test/core_spec.js it(' immutable', () => { const state = Map(); const entries = ['Trainspotting', '28 Days Later']; const nextState = setEntries(state, entries); expect(nextState).to.equal(Map({ entries: List.of('Trainspotting', '28 Days Later') })); });
この要件を満たすために、リストコンストラクターにエントリを転送します。
src/core.js import {List} from 'immutable'; export function setEntries(state, entries) { return state.set('entries', List(entries)); }
4.4.2。 投票開始
すでにレコードのセットがある状態で
next
関数を呼び出すことにより、投票を開始できます。 したがって、設計されたツリーの最初のツリーから2番目のツリーへの遷移が実行されます。
この関数には追加の引数は必要ありません。 それは
vote
連想配列を作成するべきです、最初の2つのエントリーがキーペアにあります。 同時に、現在投票に参加している
entries
は、
entries
リストに含まれなくなります。
test/core_spec.js import {List, Map} from 'immutable'; import {expect} from 'chai'; import {setEntries, next} from '../src/core'; describe(' ', () => { // .. describe('', () => { it(' ', () => { const state = Map({ entries: List.of('Trainspotting', '28 Days Later', 'Sunshine') }); const nextState = next(state); expect(nextState).to.equal(Map({ vote: Map({ pair: List.of('Trainspotting', '28 Days Later') }), entries: List.of('Sunshine') })); }); }); });
関数の実装は、更新を古い状態と結合(マージ)し 、最初のエントリを個別のリストから分離し、残りを
entries
リストの新しいバージョンに分離し
entries
。
src/core.js import {List, Map} from 'immutable'; // ... export function next(state) { const entries = state.get('entries'); return state.merge({ vote: Map({pair: entries.take(2)}), entries: entries.skip(2) }); }
4.4.3。 投票
投票が続くと、ユーザーはさまざまなエントリに投票することができます。 そして、新しい投票ごとに、現在の結果が画面に表示されるはずです。 特定のエントリが既に投票されている場合、そのカウンタは増加するはずです。
test/core_spec.js import {List, Map} from 'immutable'; import {expect} from 'chai'; import {setEntries, next, vote} from '../src/core'; describe(' ', () => { // ... describe('vote', () => { it(' ', () => { const state = Map({ vote: Map({ pair: List.of('Trainspotting', '28 Days Later') }), entries: List() }); const nextState = vote(state, 'Trainspotting'); expect(nextState).to.equal(Map({ vote: Map({ pair: List.of('Trainspotting', '28 Days Later'), tally: Map({ 'Trainspotting': 1 }) }), entries: List() })); }); it(' ', () => { const state = Map({ vote: Map({ pair: List.of('Trainspotting', '28 Days Later'), tally: Map({ 'Trainspotting': 3, '28 Days Later': 2 }) }), entries: List() }); const nextState = vote(state, 'Trainspotting'); expect(nextState).to.equal(Map({ vote: Map({ pair: List.of('Trainspotting', '28 Days Later'), tally: Map({ 'Trainspotting': 4, '28 Days Later': 2 }) }), entries: List() })); }); }); });
ImmutableのfromJS関数を使用すると、これらすべてのネストされたスキーマとリストをより簡潔に作成できます。
テストを実行します。
src/core.js export function vote(state, entry) { return state.updateIn( ['vote', 'tally', entry], 0, tally => tally + 1 ); }
updateInを使用すると、ツリーに思考を広めないようにすることができます。 このコードは、「ネストされたデータ構造[
'vote'
、
'tally'
、
'Trainspotting'
]のパスを取得し、この関数を適用します。 欠落しているキーがある場合は、代わりに新しい配列(
Map
)を作成します。 最後に値がない場合は、ゼロで初期化してください。 不変のデータ構造での作業を楽しむことができるのはこの種のコードなので、時間をかけて練習する必要があります。
4.4.4。 次のペアに行く
現在のペアの投票が終了したら、次のペアに進みます。 勝者を保存し、エントリのリストの最後に追加して、後で再び投票に参加できるようにする必要があります。 負けた記録は単に捨てられます。 同点の場合、両方のエントリが保持されます。
next
の既存
next
実装にこのロジックを追加します。
test/core_spec.js describe('next', () => { // ... it(' ', () => { const state = Map({ vote: Map({ pair: List.of('Trainspotting', '28 Days Later'), tally: Map({ 'Trainspotting': 4, '28 Days Later': 2 }) }), entries: List.of('Sunshine', 'Millions', '127 Hours') }); const nextState = next(state); expect(nextState).to.equal(Map({ vote: Map({ pair: List.of('Sunshine', 'Millions') }), entries: List.of('127 Hours', 'Trainspotting') })); }); it(' ', () => { const state = Map({ vote: Map({ pair: List.of('Trainspotting', '28 Days Later'), tally: Map({ 'Trainspotting': 3, '28 Days Later': 3 }) }), entries: List.of('Sunshine', 'Millions', '127 Hours') }); const nextState = next(state); expect(nextState).to.equal(Map({ vote: Map({ pair: List.of('Sunshine', 'Millions') }), entries: List.of('127 Hours', 'Trainspotting', '28 Days Later') })); }); });
実装では、現在の投票の勝者とエントリーを単純に結び付けます。 そして、新しい
getWinners
関数を使用してこれらの勝者を見つけることができます。
src/core.js function getWinners(vote) { if (!vote) return []; const [a, b] = vote.get('pair'); const aVotes = vote.getIn(['tally', a], 0); const bVotes = vote.getIn(['tally', b], 0); if (aVotes > bVotes) return [a]; else if (aVotes < bVotes) return [b]; else return [a, b]; } export function next(state) { const entries = state.get('entries') .concat(getWinners(state.get('vote'))); return state.merge({ vote: Map({pair: entries.take(2)}), entries: entries.skip(2) }); }
4.4.5。 投票の完了
ある時点で、記録が1つしか残っていません-勝者、そして投票が終了します。 そして、新しい投票を生成する代わりに、このエントリを現在の状態の勝者として明示的に指定します。 投票の終わり。
test/core_spec.js describe('next', () => { // ... it(' , ', () => { const state = Map({ vote: Map({ pair: List.of('Trainspotting', '28 Days Later'), tally: Map({ 'Trainspotting': 4, '28 Days Later': 2 }) }), entries: List() }); const nextState = next(state); expect(nextState).to.equal(Map({ winner: 'Trainspotting' })); }); });
next
実装では、次の投票の完了後、エントリのリストに位置が1つしか残っていない場合に、状況の処理を提供する必要があります。
src/core.js export function next(state) { const entries = state.get('entries') .concat(getWinners(state.get('vote'))); if (entries.size === 1) { return state.remove('vote') .remove('entries') .set('winner', entries.first()); } else { return state.merge({ vote: Map({pair: entries.take(2)}), entries: entries.skip(2) }); } }
ここでは単に
Map({winner: entries.first()})
返すことができます。 ただし、代わりに、古い状態を再び使用して、
vote
および
entries
キーを明示的に削除し
entries
。 これは将来を見据えて行われます。現在の状態では、この関数を使用して変更せずに転送する必要があるサードパーティのデータが表示されることがあります。 一般に、状態変換の機能の基礎は良い考えです-新しい状態をゼロから作成するのではなく、常に古い状態を新しい状態に変換します。
これで、アプリケーションのメインロジックの完全に受け入れ可能なバージョンが作成され、いくつかの関数として表されました。 ユニットテストも作成しましたが、これは非常に簡単に提供されました。プリセットやスタブはありません。 これは、純粋な機能の美しさの現れです。 単純に呼び出して、戻り値を確認できます。
Reduxもまだインストールしていないことに注意してください。 同時に、彼らはこのタスクに「フレームワーク」を関与させることなく、アプリケーションロジックの開発に冷静に従事していました。 それについて気の利いた何かがあります。
4.5。 アクションとレデューサーの使用
したがって、主要な機能はありますが、Reduxで直接呼び出すことはありません。 関数と外部の世界の間には、間接的なアドレッシングの層があります:
Actions
。
これらは、アプリケーションの状態で発生する必要がある変更を記述する単純なデータ構造です。 本質的に、これは小さなオブジェクトにパッケージ化された関数呼び出しの説明です。 ,
type
, , . . , :
{type: 'SET_ENTRIES', entries: ['Trainspotting', '28 Days Later']} {type: 'NEXT'} {type: 'VOTE', entry: 'Trainspotting'}
.
VOTE
:
// action let voteAction = {type: 'VOTE', entry: 'Trainspotting'} // : return vote(state, voteAction.entry);
(generic function), — — . (
reducer
):
src/reducer.js export default function reducer(state, action) { // , , }
, reducer :
test/reducer_spec.js import {Map, fromJS} from 'immutable'; import {expect} from 'chai'; import reducer from '../src/reducer'; describe('reducer', () => { it('handles SET_ENTRIES', () => { const initialState = Map(); const action = {type: 'SET_ENTRIES', entries: ['Trainspotting']}; const nextState = reducer(initialState, action); expect(nextState).to.equal(fromJS({ entries: ['Trainspotting'] })); }); it('handles NEXT', () => { const initialState = fromJS({ entries: ['Trainspotting', '28 Days Later'] }); const action = {type: 'NEXT'}; const nextState = reducer(initialState, action); expect(nextState).to.equal(fromJS({ vote: { pair: ['Trainspotting', '28 Days Later'] }, entries: [] })); }); it('handles VOTE', () => { const initialState = fromJS({ vote: { pair: ['Trainspotting', '28 Days Later'] }, entries: [] }); const action = {type: 'VOTE', entry: 'Trainspotting'}; const nextState = reducer(initialState, action); expect(nextState).to.equal(fromJS({ vote: { pair: ['Trainspotting', '28 Days Later'], tally: {Trainspotting: 1} }, entries: [] })); }); });
reducer . , :
src/reducer.js import {setEntries, next, vote} from './core'; export default function reducer(state, action) { switch (action.type) { case 'SET_ENTRIES': return setEntries(state, action.entries); case 'NEXT': return next(state); case 'VOTE': return vote(state, action.entry) } return state; }
, reducer , .
reducer- : , , . . ,
undefined
, :
test/reducer_spec.js describe('reducer', () => { // ... it('has an initial state', () => { const action = {type: 'SET_ENTRIES', entries: ['Trainspotting']}; const nextState = reducer(undefined, action); expect(nextState).to.equal(fromJS({ entries: ['Trainspotting'] })); }); });
core.js
, :
src/core.js export const INITIAL_STATE = Map();
reducer- :
src/reducer.js import {setEntries, next, vote, INITIAL_STATE} from './core'; export default function reducer(state = INITIAL_STATE, action) { switch (action.type) { case 'SET_ENTRIES': return setEntries(state, action.entries); case 'NEXT': return next(state); case 'VOTE': return vote(state, action.entry) } return state; }
, reducer . , , . : callback-a.
test/reducer_spec.js it(' reduce', () => { const actions = [ {type: 'SET_ENTRIES', entries: ['Trainspotting', '28 Days Later']}, {type: 'NEXT'}, {type: 'VOTE', entry: 'Trainspotting'}, {type: 'VOTE', entry: '28 Days Later'}, {type: 'VOTE', entry: 'Trainspotting'}, {type: 'NEXT'} ]; const finalState = actions.reduce(reducer, Map()); expect(finalState).to.equal(fromJS({ winner: 'Trainspotting' })); });
/ action/reducer, . actions — , JSON, , , Web Worker, reducer-a. , .
, actions , Immutable. Redux.
4.6。 Reducer-
, .
, . , . .
( ). : - , .
, . - :
vote
,
vote
. . unit
vote
:
test/core_spec.js describe('vote', () => { it(' ', () => { const state = Map({ pair: List.of('Trainspotting', '28 Days Later') }); const nextState = vote(state, 'Trainspotting') expect(nextState).to.equal(Map({ pair: List.of('Trainspotting', '28 Days Later'), tally: Map({ 'Trainspotting': 1 }) })); }); it(' ', () => { const state = Map({ pair: List.of('Trainspotting', '28 Days Later'), tally: Map({ 'Trainspotting': 3, '28 Days Later': 2 }) }); const nextState = vote(state, 'Trainspotting'); expect(nextState).to.equal(Map({ pair: List.of('Trainspotting', '28 Days Later'), tally: Map({ 'Trainspotting': 4, '28 Days Later': 2 }) })); }); });
, , !
vote
:
src/core.js export function vote(voteState, entry) { return voteState.updateIn( ['tally', entry], 0, tally => tally + 1 ); }
reducer
vote
.
src/reducer.js export default function reducer(state = INITIAL_STATE, action) { switch (action.type) { case 'SET_ENTRIES': return setEntries(state, action.entries); case 'NEXT': return next(state); case 'VOTE': return state.update('vote', voteState => vote(voteState, action.entry)); } return state; }
, : -reducer reducer- . .
reducer- Redux . , reducer-.
4.7。 Redux Store
, reducer, , Redux.
, , ,
reduce
. , . , : , , .
— Redux Store . , , .
reducer-, :
import {createStore} from 'redux'; const store = createStore(reducer);
(dispatch) store, reducer- . , Redux-Store.
store.dispatch({type: 'NEXT'});
:
store.getState();
Redux Store
store.js
. : , , action :
test/store_spec.js import {Map, fromJS} from 'immutable'; import {expect} from 'chai'; import makeStore from '../src/store'; describe('store', () => { it(' ', () => { const store = makeStore(); expect(store.getState()).to.equal(Map()); store.dispatch({ type: 'SET_ENTRIES', entries: ['Trainspotting', '28 Days Later'] }); expect(store.getState()).to.equal(fromJS({ entries: ['Trainspotting', '28 Days Later'] })); }); });
Store Redux :
npm install --save redux
store.js
,
createStore
reducer-:
src/store.js import {createStore} from 'redux'; import reducer from './reducer'; export default function makeStore() { return createStore(reducer); }
, Redux Store , — , actions, , reducer.
: Redux- ?
: . .
. , . - ?
. — , . — .
, Redux. , reducer-, Redux . , , !
—
index.js
, Store:
index.js import makeStore from './src/store'; export const store = makeStore();
, Node REPL (,
babel-node
),
index.js
Store.
4.8. Socket.io
, . , .
, . WebSocket'. , Socket.io , WebSocket'. , WebSocket'.
Socket.io :
npm install --save socket.io
server.js
, Socket.io:
src/server.js import Server from 'socket.io'; export default function startServer() { const io = new Server().attach(8090); }
Socket.io, 8090 HTTP-. , , .
index.js
, :
index.js import makeStore from './src/store'; import startServer from './src/server'; export const store = makeStore(); startServer();
,
start
package.json
:
package.json "scripts": { "start": "babel-node index.js", "test": "mocha --compilers js:babel-core/register --require ./test/test_helper.js --recursive", "test:watch": "npm run test -- --watch" },
Redux-Store:
npm run start
babel-node
babel-cli . Node- Babel-. , , . .
4.9. Store Redux Listener
Socket.io Redux , . .
(, « ?», « ?», « ?»). Socket.io .
, - ? Redux store, , action, . , callback store.
startServer
, Redux store :
index.js import makeStore from './src/store'; import {startServer} from './src/server'; export const store = makeStore(); startServer(store);
(listener) . , JavaScript- Socket.io
state
. JSON- , Socket.io.
src/server.js import Server from 'socket.io'; export function startServer(store) { const io = new Server().attach(8090); store.subscribe( () => io.emit('state', store.getState().toJS()) ); }
. . (, , , ..). .
, . .
Socket.io
connection
, . :
src/server.js import Server from 'socket.io'; export function startServer(store) { const io = new Server().attach(8090); store.subscribe( () => io.emit('state', store.getState().toJS()) ); io.on('connection', (socket) => { socket.emit('state', store.getState().toJS()); }); }
4.10. Remote Redux Actions
, : ,
NEXT
. Redux store
action
, .
src/server.js import Server from 'socket.io'; export function startServer(store) { const io = new Server().attach(8090); store.subscribe( () => io.emit('state', store.getState().toJS()) ); io.on('connection', (socket) => { socket.emit('state', store.getState().toJS()); socket.on('action', store.dispatch.bind(store)); }); }
« Redux», store (remote) actions. Redux : JavaScript-, , , . !
, , , Socket.io, Redux store. - , Vert.x Event Bus Bridge . .
:
- - (action).
- Redux store.
- Store reducer, , action.
- Store reducer- .
- Store listener, .
-
state
. - — , — .
, , , , .
entries.json
. .
entries.json [ "Shallow Grave", "Trainspotting", "A Life Less Ordinary", "The Beach", "28 Days Later", "Millions", "Sunshine", "Slumdog Millionaire", "127 Hours", "Trance", "Steve Jobs" ]
index.js
,
NEXT
:
index.js import makeStore from './src/store'; import {startServer} from './src/server'; export const store = makeStore(); startServer(store); store.dispatch({ type: 'SET_ENTRIES', entries: require('./entries.json') }); store.dispatch({type: 'NEXT'});
.
5.
React-, . Redux. , : React-. , , React . , GitHub .
5.1。
NPM-, .
mkdir voting-client cd voting-client npm init –y
HTML-.
dist/index.html
:
dist/index.html <!DOCTYPE html> <html> <body> <div id="app"></div> <script src="bundle.js"></script> </body> </html>
<div>
ID
app
, .
bundle.js
.
JavaScript-, . :
src/index.js console.log('I am alive!');
Webpack , :
npm install --save-dev webpack webpack-dev-server
, , :
npm install -g webpack webpack-dev-server
.
Webpack, , :
webpack.config.js module.exports = { entry: [ './src/index.js' ], output: { path: __dirname + '/dist', publicPath: '/', filename: 'bundle.js' }, devServer: { contentBase: './dist' } };
index.js
dist/bundle.js
.
dist
.
webpack
bundle.js
:
webpack
, localhost:8080 (
index.js
).
webpack-dev-server
React JSX ES6, . Babel , Webpack-:
npm install --save-dev babel-core babel-loader babel-preset-es2015 babel-preset-react
package.json
Babel' ES6/ES2015 React JSX, :
package.json "babel": { "presets": ["es2015", "react"] }
Webpack,
.jsx
.js
Babel:
webpack.config.js module.exports = { entry: [ './src/index.js' ], module: { loaders: [{ test: /\.jsx?$/, exclude: /node_modules/, loader: 'babel' }] }, resolve: { extensions: ['', '.js', '.jsx'] }, output: { path: __dirname + '/dist', publicPath: '/', filename: 'bundle.js' }, devServer: { contentBase: './dist' } };
CSS. , . . CSS- Webpack- ( ), , .
5.1.1.
. — Mocha Chai:
npm install --save-dev mocha chai
React-, DOM. - Karma . , jsdom , DOM JavaScript Node:
npm install --save-dev jsdom
jsdom io.js Node.js 4.0.0. Node, jsdom:
npm install --save-dev jsdom@3
jsdom React. , jsdom-
document
window
, . , React ,
document
window
. :
test/test_helper.js import jsdom from 'jsdom'; const doc = jsdom.jsdom('<!doctype html><html><body></body></html>'); const win = doc.defaultView; global.document = doc; global.window = win;
, , jsdom-
window
(,
navigator
), global Node.js. ,
window
window.
, . React:
test/test_helper.js import jsdom from 'jsdom'; const doc = jsdom.jsdom('<!doctype html><html><body></body></html>'); const win = doc.defaultView; global.document = doc; global.window = win; Object.keys(window).forEach((key) => { if (!(key in global)) { global[key] = window[key]; } });
Immutable , , , Chai. — immutable chai-immutable:
npm install --save immutable npm install --save-dev chai-immutable
:
test/test_helper.js import jsdom from 'jsdom'; import chai from 'chai'; import chaiImmutable from 'chai-immutable'; const doc = jsdom.jsdom('<!doctype html><html><body></body></html>'); const win = doc.defaultView; global.document = doc; global.window = win; Object.keys(window).forEach((key) => { if (!(key in global)) { global[key] = window[key]; } }); chai.use(chaiImmutable);
:
package.json
:
package.json "scripts": { "test": "mocha --compilers js:babel-core/register --require ./test/test_helper.js \"test/**/*@(.js|.jsx)\"" },
package.json
. :
--recursive
,
.jsx
-.
.js
,
.jsx
- glob .
.
test:watch
, :
package.json "scripts": { "test": "mocha --compilers js:babel-core/register --require ./test/test_helper.js 'test/**/*.@(js|jsx)'", "test:watch": "npm run test -- --watch" },
5.2。 React react-hot-loader
Webpack Babel , React!
React- Redux Immutable (Pure Components, Dumb Components). , , :
- , . — , ..
- . . - , , . , .
, : , , . . , .
, ? Redux store! — . React- .
. React :
npm install --save react react-dom
react-hot-loader . .
npm install --save-dev react-hot-loader
react-hot-loader, . , Redux react-hot-loader — !
webpack.config.js
. 起こったことは次のとおりです。
webpack.config.js var webpack = require('webpack'); module.exports = { entry: [ 'webpack-dev-server/client?http://localhost:8080', 'webpack/hot/only-dev-server', './src/index.js' ], module: { loaders: [{ test: /\.jsx?$/, exclude: /node_modules/, loader: 'react-hot!babel' }] }, resolve: { extensions: ['', '.js', '.jsx'] }, output: { path: __dirname + '/dist', publicPath: '/', filename: 'bundle.js' }, devServer: { contentBase: './dist', hot: true }, plugins: [ new webpack.HotModuleReplacementPlugin() ] };
entry
: Webpack (hot module loader) Webpack. Webpack . ,
plugins
devServer
.
loaders
react-hot
, Babel .js .jsx.
(Hot Module Replacement).
5.3。
: , , . .

, React- : , . , Webpack react-hot-loader , . , , .
,
Voting
. div
#app
,
index.html
.
index.js
index.jsx
, JSX-:
src/index.jsx import React from 'react'; import ReactDOM from 'react-dom'; import Voting from './components/Voting'; const pair = ['Trainspotting', '28 Days Later']; ReactDOM.render( <Voting pair={pair} />, document.getElementById('app') );
Voting
. , . , , .
webpack.config.js
:
webpack.config.js entry: [ 'webpack-dev-server/client?http://localhost:8080', 'webpack/hot/only-dev-server', './src/index.jsx' ],
webpack-dev-server
Voting
. :
src/components/Voting.jsx import React from 'react'; export default React.createClass({ getPair: function() { return this.props.pair || []; }, render: function() { return <div className="voting"> {this.getPair().map(entry => <button key={entry}> <h1>{entry}</h1> </button> )} </div>; } });
, . - , . . .
, , webpack-dev-server, .
.
Voting_spec.jsx
:
test/components/Voting_spec.jsx import Voting from '../../src/components/Voting'; describe('Voting', () => { });
pair
, . renderIntoDocument React, :
npm install --save react-addons-test-utils
test/components/Voting_spec.jsx import React from 'react'; import ReactDOM from 'react-dom'; import { renderIntoDocument } from 'react-addons-test-utils'; import Voting from '../../src/components/Voting'; describe('Voting', () => { it('renders a pair of buttons', () => { const component = renderIntoDocument( <Voting pair={["Trainspotting", "28 Days Later"]} /> ); }); });
React — scryRenderedDOMComponentsWithTag . , .
test/components/Voting_spec.jsx import React from 'react'; import ReactDOM from 'react-dom'; import { renderIntoDocument, scryRenderedDOMComponentsWithTag } from 'react-addons-test-utils'; import Voting from '../../src/components/Voting'; import {expect} from 'chai'; describe('Voting', () => { it('renders a pair of buttons', () => { const component = renderIntoDocument( <Voting pair={["Trainspotting", "28 Days Later"]} /> ); const buttons = scryRenderedDOMComponentsWithTag(component, 'button'); expect(buttons.length).to.equal(2); expect(buttons[0].textContent).to.equal('Trainspotting'); expect(buttons[1].textContent).to.equal('28 Days Later'); }); });
:
npm run test
callback-. , . . Simulate React:
test/components/Voting_spec.jsx import React from 'react'; import ReactDOM from 'react-dom'; import { renderIntoDocument, scryRenderedDOMComponentsWithTag, Simulate } from 'react-addons-test-utils'; import Voting from '../../src/components/Voting'; import {expect} from 'chai'; describe('Voting', () => { // ... it('invokes callback when a button is clicked', () => { let votedWith; const vote = (entry) => votedWith = entry; const component = renderIntoDocument( <Voting pair={["Trainspotting", "28 Days Later"]} vote={vote}/> ); const buttons = scryRenderedDOMComponentsWithTag(component, 'button'); Simulate.click(buttons[0]); expect(votedWith).to.equal('Trainspotting'); }); });
.
onClick
,
vote
:
src/components/Voting.jsx import React from 'react'; export default React.createClass({ getPair: function() { return this.props.pair || []; }, render: function() { return <div className="voting"> {this.getPair().map(entry => <button key={entry} onClick={() => this.props.vote(entry)}> <h1>{entry}</h1> </button> )} </div>; } });
: actions, callback-.
. , , .
- , . , , .
hasVoted
, :
src/index.jsx import React from 'react'; import ReactDOM from 'react-dom'; import Voting from './components/Voting'; const pair = ['Trainspotting', '28 Days Later']; ReactDOM.render( <Voting pair={pair} hasVoted="Trainspotting" />, document.getElementById('app') );
:
src/components/Voting.jsx import React from 'react'; export default React.createClass({ getPair: function() { return this.props.pair || []; }, isDisabled: function() { return !!this.props.hasVoted; }, render: function() { return <div className="voting"> {this.getPair().map(entry => <button key={entry} disabled={this.isDisabled()} onClick={() => this.props.vote(entry)}> <h1>{entry}</h1> </button> )} </div>; } });
label ,
hasVoted
.
hasVotedFor
, , :
src/components/Voting.jsx import React from 'react'; export default React.createClass({ getPair: function() { return this.props.pair || []; }, isDisabled: function() { return !!this.props.hasVoted; }, hasVotedFor: function(entry) { return this.props.hasVoted === entry; }, render: function() { return <div className="voting"> {this.getPair().map(entry => <button key={entry} disabled={this.isDisabled()} onClick={() => this.props.vote(entry)}> <h1>{entry}</h1> {this.hasVotedFor(entry) ? <div className="label">Voted</div> : null} </button> )} </div>; } });
, . , :
src/index.jsx import React from 'react'; import ReactDOM from 'react-dom'; import Voting from './components/Voting'; const pair = ['Trainspotting', '28 Days Later']; ReactDOM.render( <Voting pair={pair} winner="Trainspotting" />, document.getElementById('app') );
, div winner:
src/components/Voting.jsx import React from 'react'; export default React.createClass({ getPair: function() { return this.props.pair || []; }, isDisabled: function() { return !!this.props.hasVoted; }, hasVotedFor: function(entry) { return this.props.hasVoted === entry; }, render: function() { return <div className="voting"> {this.props.winner ? <div ref="winner">Winner is {this.props.winner}!</div> : this.getPair().map(entry => <button key={entry} disabled={this.isDisabled()} onClick={() => this.props.vote(entry)}> <h1>{entry}</h1> {this.hasVotedFor(entry) ? <div className="label">Voted</div> : null} </button> )} </div>; } });
, . , (vote screen) (winner), (vote). winner div:
src/components/Winner.jsx import React from 'react'; export default React.createClass({ render: function() { return <div className="winner"> Winner is {this.props.winner}! </div>; } });
, , :
src/components/Vote.jsx import React from 'react'; export default React.createClass({ getPair: function() { return this.props.pair || []; }, isDisabled: function() { return !!this.props.hasVoted; }, hasVotedFor: function(entry) { return this.props.hasVoted === entry; }, render: function() { return <div className="voting"> {this.getPair().map(entry => <button key={entry} disabled={this.isDisabled()} onClick={() => this.props.vote(entry)}> <h1>{entry}</h1> {this.hasVotedFor(entry) ? <div className="label">Voted</div> : null} </button> )} </div>; } });
, :
src/components/Voting.jsx import React from 'react'; import Winner from './Winner'; import Vote from './Vote'; export default React.createClass({ render: function() { return <div> {this.props.winner ? <Winner ref="winner" winner={this.props.winner} /> : <Vote {...this.props} />} </div>; } });
, ref . DOM-.
! , : , , callback-. . , Redux store.
.
hasVoted
:
test/components/Voting_spec.jsx it(' , ', () => { const component = renderIntoDocument( <Voting pair={["Trainspotting", "28 Days Later"]} hasVoted="Trainspotting" /> ); const buttons = scryRenderedDOMComponentsWithTag(component, 'button'); expect(buttons.length).to.equal(2); expect(buttons[0].hasAttribute('disabled')).to.equal(true); expect(buttons[1].hasAttribute('disabled')).to.equal(true); });
Label
Voted
,
hasVoted
:
test/components/Voting_spec.jsx it(' label , ', () => { const component = renderIntoDocument( <Voting pair={["Trainspotting", "28 Days Later"]} hasVoted="Trainspotting" /> ); const buttons = scryRenderedDOMComponentsWithTag(component, 'button'); expect(buttons[0].textContent).to.contain('Voted'); });
, , ref' :
test/components/Voting_spec.jsx it(' ', () => { const component = renderIntoDocument( <Voting winner="Trainspotting" /> ); const buttons = scryRenderedDOMComponentsWithTag(component, 'button'); expect(buttons.length).to.equal(0); const winner = ReactDOM.findDOMNode(component.refs.winner); expect(winner).to.be.ok; expect(winner.textContent).to.contain('Trainspotting'); });
, , «». , , , .
5.4。 (Pure Rendering)
, , , React. , , React .
PureRenderMixin add-on- . mixin , React - ( ) . , , .
, immutable . , , !
. , , , - , :
test/components/Voting_spec.jsx it(' ', () => { const pair = ['Trainspotting', '28 Days Later']; const container = document.createElement('div'); let component = ReactDOM.render( <Voting pair={pair} />, container ); let firstButton = scryRenderedDOMComponentsWithTag(component, 'button')[0]; expect(firstButton.textContent).to.equal('Trainspotting'); pair[0] = 'Sunshine'; component = ReactDOM.render( <Voting pair={pair} />, container ); firstButton = scryRenderedDOMComponentsWithTag(component, 'button')[0]; expect(firstButton.textContent).to.equal('Trainspotting'); });
renderIntoDocument
<div>
, .
, :
test/components/Voting_spec.jsx import React from 'react'; import ReactDOM from 'react-dom'; import { renderIntoDocument, scryRenderedDOMComponentsWithTag, Simulate } from 'react-addons-test-utils'; import {List} from 'immutable'; import Voting from '../../src/components/Voting'; import {expect} from 'chai'; describe('Voting', () => { // ... it(' DOM ', () => { const pair = List.of('Trainspotting', '28 Days Later'); const container = document.createElement('div'); let component = ReactDOM.render( <Voting pair={pair} />, container ); let firstButton = scryRenderedDOMComponentsWithTag(component, 'button')[0]; expect(firstButton.textContent).to.equal('Trainspotting'); const newPair = pair.set(0, 'Sunshine'); component = ReactDOM.render( <Voting pair={newPair} />, container ); firstButton = scryRenderedDOMComponentsWithTag(component, 'button')[0]; expect(firstButton.textContent).to.equal('Sunshine'); }); });
, PureRenderMixin. . , , : . , .
, PureRenderMixin . :
npm install --save react-addons-pure-render-mixin
:
src/components/Voting.jsx import React from 'react'; import PureRenderMixin from 'react-addons-pure-render-mixin'; import Winner from './Winner'; import Vote from './Vote'; export default React.createClass({ mixins: [PureRenderMixin], // ... }); src/components/Vote.jsx import React from 'react'; import PureRenderMixin from 'react-addons-pure-render-mixin'; export default React.createClass({ mixins: [PureRenderMixin], // ... }); src/components/Winner.jsx import React from 'react'; import PureRenderMixin from 'react-addons-pure-render-mixin'; export default React.createClass({ mixins: [PureRenderMixin], // ... });
, PureRenderMixin , . , React Voting, .
PureRenderMixin . -, , -, .
5.5。 (Routing Handling)
, : .
, , . , .
, , . URL'.
#/
,
#/results
— .
react-router , . :
npm install --save react-router@2.0.0
. (Router) React-
Route
, . :
src/index.jsx import React from 'react'; import ReactDOM from 'react-dom'; import {Route} from 'react-router'; import App from './components/App'; import Voting from './components/Voting'; const pair = ['Trainspotting', '28 Days Later']; const routes = <Route component={App}> <Route path="/" component={Voting} /> </Route>; ReactDOM.render( <Voting pair={pair} />, document.getElementById('app') );
, Voting. , .
App
, .
.
App
:
src/components/App.jsx import React from 'react'; import {List} from 'immutable'; const pair = List.of('Trainspotting', '28 Days Later'); export default React.createClass({ render: function() { return React.cloneElement(this.props.children, {pair: pair}); } });
,
children
. react-router , .
Voting
,
Voting
.
,
pair
index.jsx
App.jsx
.
pair
API cloneElement . , .
, PureRenderMixin . App: - React . , .
index.js
, , :
src/index.jsx import React from 'react'; import ReactDOM from 'react-dom'; import {Router, Route, hashHistory} from 'react-router'; import App from './components/App'; import Voting from './components/Voting'; const routes = <Route component={App}> <Route path="/" component={Voting} /> </Route>; ReactDOM.render( <Router history={hashHistory}>{routes}</Router>, document.getElementById('app') );
Router
react-router
,
#hash
( API HTML 5). .
:
Voting
. React, . ,
Results
:
src/index.jsx import React from 'react'; import ReactDOM from 'react-dom'; import {Router, Route, hashHistory} from 'react-router'; import App from './components/App'; import Voting from './components/Voting'; import Results from './components/Results'; const routes = <Route component={App}> <Route path="/results" component={Results} /> <Route path="/" component={Voting} /> </Route>; ReactDOM.render( <Router history={hashHistory}>{routes}</Router>, document.getElementById('app') );
<Route>
/results
results
. Voting.
Results
:
src/components/Results.jsx import React from 'react'; import PureRenderMixin from 'react-addons-pure-render-mixin'; export default React.createClass({ mixins: [PureRenderMixin], render: function() { return <div>Hello from results!</div> } });
localhost :8080/#/results, Results. . «» «» , . , !
React. , .
, Results, - . , Voting:
src/components/Results.jsx import React from 'react'; import PureRenderMixin from 'react-addons-pure-render-mixin'; export default React.createClass({ mixins: [PureRenderMixin], getPair: function() { return this.props.pair || []; }, render: function() { return <div className="results"> {this.getPair().map(entry => <div key={entry} className="entry"> <h1>{entry}</h1> </div> )} </div>; } });
, , . App Map:
src/components/App.jsx import React from 'react'; import {List, Map} from 'immutable'; const pair = List.of('Trainspotting', '28 Days Later'); const tally = Map({'Trainspotting': 5, '28 Days Later': 4}); export default React.createClass({ render: function() { return React.cloneElement(this.props.children, { pair: pair, tally: tally }); } });
Results :
src/components/Results.jsx import React from 'react'; import PureRenderMixin from 'react-addons-pure-render-mixin'; export default React.createClass({ mixins: [PureRenderMixin], getPair: function() { return this.props.pair || []; }, getVotes: function(entry) { if (this.props.tally && this.props.tally.has(entry)) { return this.props.tally.get(entry); } return 0; }, render: function() { return <div className="results"> {this.getPair().map(entry => <div key={entry} className="entry"> <h1>{entry}</h1> <div className="voteCount"> {this.getVotes(entry)} </div> </div> )} </div>; } });
Results, , . div' , . , :
test/components/Results_spec.jsx import React from 'react'; import { renderIntoDocument, scryRenderedDOMComponentsWithClass } from 'react-addons-test-utils'; import {List, Map} from 'immutable'; import Results from '../../src/components/Results'; import {expect} from 'chai'; describe('Results', () => { it('renders entries with vote counts or zero', () => { const pair = List.of('Trainspotting', '28 Days Later'); const tally = Map({'Trainspotting': 5}); const component = renderIntoDocument( <Results pair={pair} tally={tally} /> ); const entries = scryRenderedDOMComponentsWithClass(component, 'entry'); const [train, days] = entries.map(e => e.textContent); expect(entries.length).to.equal(2); expect(train).to.contain('Trainspotting'); expect(train).to.contain('5'); expect(days).to.contain('28 Days Later'); expect(days).to.contain('0'); }); });
«Next», . , callback-. , «Next». , , :
test/components/Results_spec.jsx import React from 'react'; import ReactDOM from 'react-dom'; import { renderIntoDocument, scryRenderedDOMComponentsWithClass, Simulate } from 'react-addons-test-utils'; import {List, Map} from 'immutable'; import Results from '../../src/components/Results'; import {expect} from 'chai'; describe('Results', () => { // ... it(' callback Next', () => { let nextInvoked = false; const next = () => nextInvoked = true; const pair = List.of('Trainspotting', '28 Days Later'); const component = renderIntoDocument( <Results pair={pair} tally={Map()} next={next}/> ); Simulate.click(ReactDOM.findDOMNode(component.refs.next)); expect(nextInvoked).to.equal(true); }); });
. , :
src/components/Results.jsx import React from 'react'; import PureRenderMixin from 'react-addons-pure-render-mixin'; export default React.createClass({ mixins: [PureRenderMixin], getPair: function() { return this.props.pair || []; }, getVotes: function(entry) { if (this.props.tally && this.props.tally.has(entry)) { return this.props.tally.get(entry); } return 0; }, render: function() { return <div className="results"> <div className="tally"> {this.getPair().map(entry => <div key={entry} className="entry"> <h1>{entry}</h1> <div class="voteCount"> {this.getVotes(entry)} </div> </div> )} </div> <div className="management"> <button ref="next" className="next" onClick={this.props.next}> Next </button> </div> </div>; } });
, :
test/components/Results_spec.jsx it(' ', () => { const component = renderIntoDocument( <Results winner="Trainspotting" pair={["Trainspotting", "28 Days Later"]} tally={Map()} /> ); const winner = ReactDOM.findDOMNode(component.refs.winner); expect(winner).to.be.ok; expect(winner.textContent).to.contain('Trainspotting'); });
Winner, . , :
src/components/Results.jsx import React from 'react'; import PureRenderMixin from 'react-addons-pure-render-mixin'; import Winner from './Winner'; export default React.createClass({ mixins: [PureRenderMixin], getPair: function() { return this.props.pair || []; }, getVotes: function(entry) { if (this.props.tally && this.props.tally.has(entry)) { return this.props.tally.get(entry); } return 0; }, render: function() { return this.props.winner ? <Winner ref="winner" winner={this.props.winner} /> : <div className="results"> <div className="tally"> {this.getPair().map(entry => <div key={entry} className="entry"> <h1>{entry}</h1> <div className="voteCount"> {this.getVotes(entry)} </div> </div> )} </div> <div className="management"> <button ref="next" className="next" onClick={this.props.next}> Next </button> </div> </div>; } });
. , Tally . , !
, . , . , . , .
, , , Redux store .
5.6。 Redux Store
Redux , . . Redux , ! , React-.
, . , .
. , . vote :

, .

(Voting) , . :

, :

, ,
hasVoted
. , (actions) (reducers), Redux store. ?
, . — . :
- .
- “Next” .
, . . .
, . ,
state
, . , . reducer-a, action, . action :
{ type: 'SET_STATE', state: { vote: {...} } }
, . , , reducer :
test/reducer_spec.js import {List, Map, fromJS} from 'immutable'; import {expect} from 'chai'; import reducer from '../src/reducer'; describe('reducer', () => { it('handles SET_STATE', () => { const initialState = Map(); const action = { type: 'SET_STATE', state: Map({ vote: Map({ pair: List.of('Trainspotting', '28 Days Later'), tally: Map({Trainspotting: 1}) }) }) }; const nextState = reducer(initialState, action); expect(nextState).to.equal(fromJS({ vote: { pair: ['Trainspotting', '28 Days Later'], tally: {Trainspotting: 1} } })); }); });
Reducer JS- . :
test/reducer_spec.js it(' SET_STATE JS-', () => { const initialState = Map(); const action = { type: 'SET_STATE', state: { vote: { pair: ['Trainspotting', '28 Days Later'], tally: {Trainspotting: 1} } } }; const nextState = reducer(initialState, action); expect(nextState).to.equal(fromJS({ vote: { pair: ['Trainspotting', '28 Days Later'], tally: {Trainspotting: 1} } })); });
undefined
reducer- :
test/reducer_spec.js it(' SET_STATE ', () => { const action = { type: 'SET_STATE', state: { vote: { pair: ['Trainspotting', '28 Days Later'], tally: {Trainspotting: 1} } } }; const nextState = reducer(undefined, action); expect(nextState).to.equal(fromJS({ vote: { pair: ['Trainspotting', '28 Days Later'], tally: {Trainspotting: 1} } })); });
. , . -reducer, reducer-:
src/reducer.js import {Map} from 'immutable'; export default function(state = Map(), action) { return state; }
Reducer action
SET_STATE
. merge Map -. !
src/reducer.js import {Map} from 'immutable'; function setState(state, newState) { return state.merge(newState); } export default function(state = Map(), action) { switch (action.type) { case 'SET_STATE': return setState(state, action.state); } return state; }
, «» , reducer-. , , . , . , .
, : «Next». , , .
Redux :
npm install --save redux
store
index.jsx
. - ,
SET_STATE
( , ):
src/index.jsx import React from 'react'; import ReactDOM from 'react-dom'; import {Router, Route, hashHistory} from 'react-router'; import {createStore} from 'redux'; import reducer from './reducer'; import App from './components/App'; import Voting from './components/Voting'; import Results from './components/Results'; const store = createStore(reducer); store.dispatch({ type: 'SET_STATE', state: { vote: { pair: ['Sunshine', '28 Days Later'], tally: {Sunshine: 2} } } }); const routes = <Route component={App}> <Route path="/results" component={Results} /> <Route path="/" component={Voting} /> </Route>; ReactDOM.render( <Router history={hashHistory}>{routes}</Router>, document.getElementById('app') );
Store . React-?
5.7。 Redux React
Redux Store . React-, . store , . React , PureRenderMixin , , .
, Redux React react-redux :
npm install --save react-redux
react-redux Redux store :
- store .
- actions callback- .
- ( Provider ) react-redux. Redux Store, store c .
-. .
src/index.jsx import React from 'react'; import ReactDOM from 'react-dom'; import {Router, Route, hashHistory} from 'react-router'; import {createStore} from 'redux'; import {Provider} from 'react-redux'; import reducer from './reducer'; import App from './components/App'; import {VotingContainer} from './components/Voting'; import Results from './components/Results'; const store = createStore(reducer); store.dispatch({ type: 'SET_STATE', state: { vote: { pair: ['Sunshine', '28 Days Later'], tally: {Sunshine: 2} } } }); const routes = <Route component={App}> <Route path="/results" component={Results} /> <Route path="/" component={VotingContainer} /> </Route>; ReactDOM.render( <Provider store={store}> <Router history={hashHistory}>{routes}</Router> </Provider>, document.getElementById('app') ); });
, «», store . , :
-
App
, . -
Vote
Winner
, . . - , :
Voting
Results
.App
. - store.
Voting
. react-redux connect , . , , React-:
connect(mapStateToProps)(SomeComponent);
- Redux Store . . Voting
pair
winner
Store:
src/components/Voting.jsx import React from 'react'; import PureRenderMixin from 'react-addons-pure-render-mixin' import {connect} from 'react-redux'; import Winner from './Winner'; import Vote from './Vote'; const Voting = React.createClass({ mixins: [PureRenderMixin], render: function() { return <div> {this.props.winner ? <Winner ref="winner" winner={this.props.winner} /> : <Vote {...this.props} />} </div>; } }); function mapStateToProps(state) { return { pair: state.getIn(['vote', 'pair']), winner: state.get('winner') }; } connect(mapStateToProps)(Voting); export default Voting;
. ,
connect
Voting
. , .
connect
Voting
. , .
VotingContainer
:
src/components/Voting.jsx import React from 'react'; import PureRenderMixin from 'react-addons-pure-render-mixin'; import {connect} from 'react-redux'; import Winner from './Winner'; import Vote from './Vote'; export const Voting = React.createClass({ mixins: [PureRenderMixin], render: function() { return <div> {this.props.winner ? <Winner ref="winner" winner={this.props.winner} /> : <Vote {...this.props} />} </div>; } }); function mapStateToProps(state) { return { pair: state.getIn(['vote', 'pair']), winner: state.get('winner') }; } export const VotingContainer = connect(mapStateToProps)(Voting);
Voting
VotingContainer
. react-redux «» (dumb) , — «» (smart). «» «». , , , :
- / . , .
- / - , Redux store. react-redux.
-,
Voting
VotingContainer
. , Redux-.
src/index.jsx import React from 'react'; import ReactDOM from 'react-dom'; import {Router, Route, hashHistory} from 'react-router'; import {createStore} from 'redux'; import {Provider} from 'react-redux'; import reducer from './reducer'; import App from './components/App'; import {VotingContainer} from './components/Voting'; import Results from './components/Results'; const store = createStore(reducer); store.dispatch({ type: 'SET_STATE', state: { vote: { pair: ['Sunshine', '28 Days Later'], tally: {Sunshine: 2} } } }); const routes = <Route component={App}> <Route path="/results" component={Results} /> <Route path="/" component={VotingContainer} /> </Route>; ReactDOM.render( <Provider store={store}> <Router history={hashHistory}>{routes}</Router> </Provider>, document.getElementById('app') );
Voting
, Voting :
test/components/Voting_spec.jsx import React from 'react'; import ReactDOM from 'react-dom'; import { renderIntoDocument, scryRenderedDOMComponentsWithTag, Simulate } from 'react-addons-test-utils'; import {List} from 'immutable'; import {Voting} from '../../src/components/Voting'; import {expect} from 'chai';
. Voting, . , store.
,
pair
winner
. ,
tally
:
src/components/Results.jsx import React from 'react'; import PureRenderMixin from 'react-addons-pure-render-mixin'; import {connect} from 'react-redux'; import Winner from './Winner'; export const Results = React.createClass({ mixins: [PureRenderMixin], getPair: function() { return this.props.pair || []; }, getVotes: function(entry) { if (this.props.tally && this.props.tally.has(entry)) { return this.props.tally.get(entry); } return 0; }, render: function() { return this.props.winner ? <Winner ref="winner" winner={this.props.winner} /> : <div className="results"> <div className="tally"> {this.getPair().map(entry => <div key={entry} className="entry"> <h1>{entry}</h1> <div className="voteCount"> {this.getVotes(entry)} </div> </div> )} </div> <div className="management"> <button ref="next" className="next" onClick={this.props.next}> Next </button> </div> </div>; } }); function mapStateToProps(state) { return { pair: state.getIn(['vote', 'pair']), tally: state.getIn(['vote', 'tally']), winner: state.get('winner') } } export const ResultsContainer = connect(mapStateToProps)(Results);
index.jsx
,
Results
ResultsContainer
:
src/index.jsx import React from 'react'; import ReactDOM from 'react-dom'; import {Router, Route, hashHistory} from 'react-router'; import {createStore} from 'redux'; import {Provider} from 'react-redux'; import reducer from './reducer'; import App from './components/App'; import {VotingContainer} from './components/Voting'; import {ResultsContainer} from './components/Results'; const store = createStore(reducer); store.dispatch({ type: 'SET_STATE', state: { vote: { pair: ['Sunshine', '28 Days Later'], tally: {Sunshine: 2} } } }); const routes = <Route component={App}> <Route path="/results" component={ResultsContainer} /> <Route path="/" component={VotingContainer} /> </Route>; ReactDOM.render( <Provider store={store}> <Router history={hashHistory}>{routes}</Router> </Provider>, document.getElementById('app') );
,
Results
:
test/components/Results_spec.jsx import React from 'react'; import ReactDOM from 'react-dom'; import { renderIntoDocument, scryRenderedDOMComponentsWithClass, Simulate } from 'react-addons-test-utils'; import {List, Map} from 'immutable'; import {Results} from '../../src/components/Results'; import {expect} from 'chai';
React- Redux-, .
, , . .
, , . , . , , , . «».
, Redux.
App.jsx
, :
src/components/App.jsx import React from 'react'; export default React.createClass({ render: function() { return this.props.children; } });
5.8. Socket.io
Redux-, Redux-. , .
socket- . Redux-, . .
. Socket.io- . socket.io-client , , :
npm install --save socket.io-client
io
, Socket.io. 8090 ( ):
src/index.jsx import React from 'react'; import ReactDOM from 'react-dom'; import {Router, Route, hashHistory} from 'react-router'; import {createStore} from 'redux'; import {Provider} from 'react-redux'; import io from 'socket.io-client'; import reducer from './reducer'; import App from './components/App'; import {VotingContainer} from './components/Voting'; import {ResultsContainer} from './components/Results'; const store = createStore(reducer); store.dispatch({ type: 'SET_STATE', state: { vote: { pair: ['Sunshine', '28 Days Later'], tally: {Sunshine: 2} } } }); const socket = io(`${location.protocol}//${location.hostname}:8090`); const routes = <Route component={App}> <Route path="/results" component={ResultsContainer} /> <Route path="/" component={VotingContainer} /> </Route>; ReactDOM.render( <Provider store={store}> <Router history={hashHistory}>{routes}</Router> </Provider>, document.getElementById('app') );
, , . WebSocket-, Socket.io.
Socket.io-: , Webpack-.
5.9. actions
Socket.io .
state
, .
SET_STATE
. reducer:
src/index.jsx import React from 'react'; import ReactDOM from 'react-dom'; import {Router, Route, hashHistory} from 'react-router'; import {createStore} from 'redux'; import {Provider} from 'react-redux'; import io from 'socket.io-client'; import reducer from './reducer'; import App from './components/App'; import {VotingContainer} from './components/Voting'; import {ResultsContainer} from './components/Results'; const store = createStore(reducer); const socket = io(`${location.protocol}//${location.hostname}:8090`); socket.on('state', state => store.dispatch({type: 'SET_STATE', state}) ); const routes = <Route component={App}> <Route path="/results" component={ResultsContainer} /> <Route path="/" component={VotingContainer} /> </Route>; ReactDOM.render( <Provider store={store}> <Router history={hashHistory}>{routes}</Router> </Provider>, document.getElementById('app') );
,
SET_STATE
. , .
— : , . !
5.10. actions React-
, Redux store. .
. ,
Voting
vote
, callback-. , . , .
, - ? , . , :
hasVoted
, - .
SET_STATE
Redux action —
VOTE
.
hasVoted
:
test/reducer_spec.js it(' VOTE hasVoted', () => { const state = fromJS({ vote: { pair: ['Trainspotting', '28 Days Later'], tally: {Trainspotting: 1} } }); const action = {type: 'VOTE', entry: 'Trainspotting'}; const nextState = reducer(state, action); expect(nextState).to.equal(fromJS({ vote: { pair: ['Trainspotting', '28 Days Later'], tally: {Trainspotting: 1} }, hasVoted: 'Trainspotting' })); });
, VOTE - , :
test/reducer_spec.js it(' hasVoted VOTE', () => { const state = fromJS({ vote: { pair: ['Trainspotting', '28 Days Later'], tally: {Trainspotting: 1} } }); const action = {type: 'VOTE', entry: 'Sunshine'}; const nextState = reducer(state, action); expect(nextState).to.equal(fromJS({ vote: { pair: ['Trainspotting', '28 Days Later'], tally: {Trainspotting: 1} } })); });
reducer-a :
src/reducer.js import {Map} from 'immutable'; function setState(state, newState) { return state.merge(newState); } function vote(state, entry) { const currentPair = state.getIn(['vote', 'pair']); if (currentPair && currentPair.includes(entry)) { return state.set('hasVoted', entry); } else { return state; } } export default function(state = Map(), action) { switch (action.type) { case 'SET_STATE': return setState(state, action.state); case 'VOTE': return vote(state, action.entry); } return state; }
hasVoted
. , .
SET_STATE
, , , . ,
hasVoted
:
test/reducer_spec.js it(' , hasVoted SET_STATE', () => { const initialState = fromJS({ vote: { pair: ['Trainspotting', '28 Days Later'], tally: {Trainspotting: 1} }, hasVoted: 'Trainspotting' }); const action = { type: 'SET_STATE', state: { vote: { pair: ['Sunshine', 'Slumdog Millionaire'] } } }; const nextState = reducer(initialState, action); expect(nextState).to.equal(fromJS({ vote: { pair: ['Sunshine', 'Slumdog Millionaire'] } })); });
resetVote
SET_STATE
:
src/reducer.js import {List, Map} from 'immutable'; function setState(state, newState) { return state.merge(newState); } function vote(state, entry) { const currentPair = state.getIn(['vote', 'pair']); if (currentPair && currentPair.includes(entry)) { return state.set('hasVoted', entry); } else { return state; } } function resetVote(state) { const hasVoted = state.get('hasVoted'); const currentPair = state.getIn(['vote', 'pair'], List()); if (hasVoted && !currentPair.includes(hasVoted)) { return state.remove('hasVoted'); } else { return state; } } export default function(state = Map(), action) { switch (action.type) { case 'SET_STATE': return resetVote(setState(state, action.state)); case 'VOTE': return vote(state, action.entry); } return state; }
hasVoted
. .
hasVoted
Voting
:
src/components/Voting.jsx function mapStateToProps(state) { return { pair: state.getIn(['vote', 'pair']), hasVoted: state.get('hasVoted'), winner: state.get('winner') }; }
-
Voting vote
callback, .
Voting
actions Redux,
connect
react-redux.
react-redux , . Redux: (Action creators) .
, Redux , ( )
type
. . :
function vote(entry) { return {type: 'VOTE', entry}; }
« ». , . , . , . , .
, :
src/action_creators.js export function setState(state) { return { type: 'SET_STATE', state }; } export function vote(entry) { return { type: 'VOTE', entry }; }
. , . , .
index.jsx
Socket.io-
setState
:
src/index.jsx import React from 'react'; import ReactDOM from 'react-dom'; import {Router, Route, hashHistory} from 'react-router'; import {createStore} from 'redux'; import {Provider} from 'react-redux'; import io from 'socket.io-client'; import reducer from './reducer'; import {setState} from './action_creators'; import App from './components/App'; import {VotingContainer} from './components/Voting'; import {ResultsContainer} from './components/Results'; const store = createStore(reducer); const socket = io(`${location.protocol}//${location.hostname}:8090`); socket.on('state', state => store.dispatch(setState(state)) ); const routes = <Route component={App}> <Route path="/results" component={ResultsContainer} /> <Route path="/" component={VotingContainer} /> </Route>; ReactDOM.render( <Provider store={store}> <Router history={hashHistory}>{routes}</Router> </Provider>, document.getElementById('app') );
, react-redux React-. callback-
vote
Voting
vote. , : , , .
connect
react-redux :
src/components/Voting.jsx import React from 'react'; import PureRenderMixin from 'react-addons-pure-render-mixin'; import {connect} from 'react-redux'; import Winner from './Winner'; import Vote from './Vote'; import * as actionCreators from '../action_creators'; export const Voting = React.createClass({ mixins: [PureRenderMixin], render: function() { return <div> {this.props.winner ? <Winner ref="winner" winner={this.props.winner} /> : <Vote {...this.props} />} </div>; } }); function mapStateToProps(state) { return { pair: state.getIn(['vote', 'pair']), hasVoted: state.get('hasVoted'), winner: state.get('winner') }; } export const VotingContainer = connect( mapStateToProps, actionCreators )(Voting);
vote
Voting
. , vote, Redux Store. ! : .
5.11. Redux Middleware
— . “Next” .
. ?
- ,
VOTE
Redux store. -
VOTE
reducer-hasVoted
. - actions Socket.io-
action
. Redux Store. -
VOTE
reducer- .
, , .
VOTE
, Redux stores. .
どこから始めますか? Redux , . , .
Redux actions, redux store — Middleware .
Middleware () — , , reducer store. Middleware , , store. actions .
middleware listeners:
- , store, .
- , .
.
remote action middleware, Socket.io- store, .
middleware. , Redux store , callback «next». , Redux action. middleware:
src/remote_action_middleware.js export default store => next => action => { }
, :
export default function(store) { return function(next) { return function(action) { } } }
. : (
function(store, next, action) { }
), . , «»,
store
.
next
. callback, middleware ,
action
store ( middleware):
src/remote_action_middleware.js export default store => next => action => { return next(action); }
next
, . reducer store.
- middleware, , :
src/remote_action_middleware.js export default store => next => action => { console.log('in middleware', action); return next(action); }
middleware Redux store, . middleware Redux
applyMiddleware
. middleware, , , , ,
createStore
. store middleware:
src/components/index.jsx import React from 'react'; import ReactDOM from 'react-dom'; import {Router, Route, hashHistory} from 'react-router'; import {createStore, applyMiddleware} from 'redux'; import {Provider} from 'react-redux'; import io from 'socket.io-client'; import reducer from './reducer'; import {setState} from './action_creators'; import remoteActionMiddleware from './remote_action_middleware'; import App from './components/App'; import {VotingContainer} from './components/Voting'; import {ResultsContainer} from './components/Results'; const createStoreWithMiddleware = applyMiddleware( remoteActionMiddleware )(createStore); const store = createStoreWithMiddleware(reducer); const socket = io(`${location.protocol}//${location.hostname}:8090`); socket.on('state', state => store.dispatch(setState(state)) ); const routes = <Route component={App}> <Route path="/results" component={ResultsContainer} /> <Route path="/" component={VotingContainer} /> </Route>; ReactDOM.render( <Provider store={store}> <Router history={hashHistory}>{routes}</Router> </Provider>, document.getElementById('app') );
. API Redux.
, , , middleware actions:
SET_STATE
, —
VOTE
.
Middleware Socket.io- middleware. .
index.jsx
, middleware . middleware. Socket.io:
src/remote_action_middleware.js export default socket => store => next => action => { console.log('in middleware', action); return next(action); }
src/index.jsx const socket = io(`${location.protocol}//${location.hostname}:8090`); socket.on('state', state => store.dispatch(setState(state)) ); const createStoreWithMiddleware = applyMiddleware( remoteActionMiddleware(socket) )(createStore); const store = createStoreWithMiddleware(reducer);
, store: , store.
, middleware
action
:
src/remote_action_middleware.js export default socket => store => next => action => { socket.emit('action', action); return next(action); }
以上です! . , . !
:
SET_STATE
, . , ,
SET_STATE
. .
Middleware action . , SET_STATE, , . ,
{meta: {remote: true}}
:
( rafScheduler middleware )
src/remote_action_middleware.js export default socket => store => next => action => { if (action.meta && action.meta.remote) { socket.emit('action', action); } return next(action); }
VOTE
,
SET_STATE
:
src/action_creators.js export function setState(state) { return { type: 'SET_STATE', state }; } export function vote(entry) { return { meta: {remote: true}, type: 'VOTE', entry }; }
:
- . VOTE.
- Middleware action Socket.io-.
- Redux store,
hasVote
. - , Redux store action .
- store .
- Redux store
SET_STATE
. - .
“Next”. , . .
NEXT
:
src/action_creator.js export function setState(state) { return { type: 'SET_STATE', state }; } export function vote(entry) { return { meta: {remote: true}, type: 'VOTE', entry }; } export function next() { return { meta: {remote: true}, type: 'NEXT' }; }
ResultsContainer
:
src/components/Results.jsx import React from 'react'; import PureRenderMixin from 'react-addons-pure-render-mixin'; import {connect} from 'react-redux'; import Winner from './Winner'; import * as actionCreators from '../action_creators'; export const Results = React.createClass({ mixins: [PureRenderMixin], getPair: function() { return this.props.pair || []; }, getVotes: function(entry) { if (this.props.tally && this.props.tally.has(entry)) { return this.props.tally.get(entry); } return 0; }, render: function() { return this.props.winner ? <Winner ref="winner" winner={this.props.winner} /> : <div className="results"> <div className="tally"> {this.getPair().map(entry => <div key={entry} className="entry"> <h1>{entry}</h1> <div className="voteCount"> {this.getVotes(entry)} </div> </div> )} </div> <div className="management"> <button ref="next" className="next" onClick={this.props.next}> Next </button> </div> </div>; } }); function mapStateToProps(state) { return { pair: state.getIn(['vote', 'pair']), tally: state.getIn(['vote', 'tally']), winner: state.get('winner') } } export const ResultsContainer = connect( mapStateToProps, actionCreators )(Results);
… ! . . , . . «Next» , .
6.
, Redux, . .
1.
, . , .
.
2.
, , hasVoted. : , , . , .
, , .
: . , . , .
.
3.
. , . .
: , , . , . , .
.
4.
, .
: , .
.
5.
Socket.io . , .
: Socket.io , Redux- .
.
: (Peer to Peer)
, , . , reducer- reducer , . , .
, , ? ? P2P WebRTC? ( Socket.io P2P )