Reduxガむド

今日、 ReduxはJavaScriptの䞖界で最も興味深い珟象の1぀です。 数癟のラむブラリずフレヌムワヌクから際立っおおり、シンプルで予枬可胜な状態モデルを導入し、関数型プログラミングず䞍倉デヌタに逞脱し、コンパクトなAPIを提䟛するこずで、さたざたな問題を有胜に解決したす。 幞せには他に䜕が必芁ですか Redux-ラむブラリは非垞に小さく、APIの孊習は難しくありたせん。 しかし、倚くの人々にずっお、䞀皮のテンプレヌトブレヌクが発生したす。少数のコンポヌネントず、玔粋な関数ず䞍倉のデヌタに察する自発的な制限は、䞍圓な匷制に芋えるかもしれたせん。 そのような状況でどのように正確に働くのですか

このチュヌトリアルでは、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぀の異なるナヌザヌむンタヌフェむスがありたす。




3.アヌキテクチャ


構造的に、システムは2぀のアプリケヌションで構成されたす。


アプリケヌション間の盞互䜜甚は、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オブゞェクトず配列に蚀及しおおり、慣䟋により倉曎するこずは控えおいたす。

䞍倉ラむブラリがマニュアルで䜿甚される理由はいく぀かありたす。


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

:

  1. - (action).
  2. Redux store.
  3. Store reducer, , action.
  4. Store reducer- .
  5. Store listener, .
  6. state .
  7. — , — .

, , , , . 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). , , :

  1. , . — , ..
  2. . . - , , . , .

, : , , . . , .

, ? 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. ?

, . — . :


, . . .

, . , 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 :


- ( 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 . , :


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). «» «». , , , :


-, 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 stores. .

どこから始めたすか Redux , . , .

Redux actions, redux store — Middleware .

Middleware () — , , reducer store. Middleware , , store. actions .

middleware listeners:


.

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

:

  1. . VOTE.
  2. Middleware action Socket.io-.
  3. Redux store, hasVote .
  4. , Redux store action .
  5. store .
  6. Redux store SET_STATE .
  7. .

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

Source: https://habr.com/ru/post/J303456/


All Articles