
仿¥ã
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ã€ã®ç°ãªããŠãŒã¶ãŒã€ã³ã¿ãŒãã§ã€ã¹ããããŸãã
- æç¥šã€ã³ã¿ãŒãã§ãŒã¹ã¯ã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', () => {
ãããŠãã ãŒããŒãéåžžã®é
åã«æ¿å
¥ãããšãå€ãç¶æ
ã倿ŽãããŸãã ãã ãã代ããã«Immutableã®ãªã¹ãã䜿çšãããããåã®äŸã®æ°åãšåãã»ãã³ãã£ã¯ã¹ã䜿çšããŸãã
éåžžã®é
åã«è²Œãä»ãããšãå€ãç¶æ
ãå€ãããŸãã ããããäžå€ãªã¹ãã䜿çšããŠãããããæ°å€ã®äŸãšåãã»ãã³ãã£ã¯ã¹ãæã£ãŠããŸãã
ãã®èãæ¹ã¯ãæ¬æ Œçãªç¶æ
ããªãŒã«ãåœãŠã¯ãŸããŸãã ããªãŒã¯ããªã¹ãã飿³é
åïŒ
ããã ïŒãããã³ãã®ä»ã®ã¿ã€ãã®ã³ã¬ã¯ã·ã§ã³ã®ãã¹ãæ§é ã§ãã ããã«é©çšãããæäœã«ããã
æ°ããç¶æ
ããªãŒãäœæãããåã®
ããªãŒã¯å€æŽãããŸããã ããªãŒãã ãŒããŒã®ãªã¹ããå«ãã ãŒããŒããŒãæã€é£æ³é
åã§ããå Žåãæ°ããäœçœ®ã远å ãããšãã ãŒããŒããŒãæ°ãããªã¹ããæãæ°ããé
åãäœæããå¿
èŠããããŸãã
test/immutable_spec.js import {expect} from 'chai'; import {List, Map} from 'immutable'; describe('immutability', () => {
ããã§ã¯ããã¹ããããæ§é ã®æäœã瀺ãããã«æ¡åŒµãããã以åãšãŸã£ããåãåäœã確èªããŸãã äžå€æ§ã®æŠå¿µã¯ããã¹ãŠã®åœ¢ç¶ãšãµã€ãºã®ããŒã¿ââã«é©çšãããŸãã
ãã®ãããªãã¹ããããæ§é ã®æäœã®ããã«ã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(' ', () => {
颿°ã®å®è£
ã¯ãæŽæ°ãå€ãç¶æ
ãš
çµåïŒããŒãžïŒã ãæåã®ãšã³ããªãåå¥ã®ãªã¹ãããåé¢ããæ®ãã
entries
ãªã¹ãã®æ°ããããŒãžã§ã³ã«åé¢ã
entries
ã
src/core.js import {List, Map} from 'immutable';
4.4.3ã æç¥š
æç¥šãç¶ããšããŠãŒã¶ãŒã¯ããŸããŸãªãšã³ããªã«æç¥šããããšãã§ããŸãã ãããŠãæ°ããæç¥šããšã«ãçŸåšã®çµæãç»é¢ã«è¡šç€ºãããã¯ãã§ãã ç¹å®ã®ãšã³ããªãæ¢ã«æç¥šãããŠããå Žåããã®ã«ãŠã³ã¿ã¯å¢å ããã¯ãã§ãã
test/core_spec.js import {List, Map} from 'immutable'; import {expect} from 'chai'; import {setEntries, next, vote} from '../src/core'; describe(' ', () => {
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', () => {
å®è£
ã§ã¯ãçŸåšã®æç¥šã®åè
ãšãšã³ããªãŒãåçŽã«çµã³ä»ããŸãã ãããŠãæ°ãã
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', () => {
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
:
(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', () => {
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', () => {
.
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', () => {
, 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],
, 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', () => {
. , :
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 :
- 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:
.
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 )