ãã¥ãŒããªã¢ã«ã®
åã®éšåã§ã¯ ãå圢ã¢ããªã±ãŒã·ã§ã³ã«
ããã¯ãšã³ãAPIãªã¯ãšã¹ãããããã·ããã»ãã·ã§ã³ã䜿çšããŠåæãªã¯ãšã¹ãéã§åæç¶æ
ã転éããã¯ã©ã€ã¢ã³ãã§ããŒã¯ã¢ãããåå©çšãããªãã·ã§ã³ïŒ
hydrate ïŒã§
ãµãŒããŒåŽã¬ã³ããªã³ã°ãå®è¡ããããšãæããŸããã ãã®ããŒãã§ã¯ãååWebã¢ããªã±ãŒã·ã§ã³ã®2ã€ã®éèŠãªåé¡ã解決ããŸãã
ååã«ãŒãã£ã³ã°ãšããã²ãŒã·ã§ã³ ã
ç¹°ãè¿ããã§ãããšåæããŒã¿ç¶æ
ã§ãã ãããŠãæåéã5è¡ã®ã³ãŒãã§ãããè¡ããŸãã è¡ããïŒ

ããããŒã°
ãããã§ã¹ãã«ã€ããŠ
ãŸãã
ãããžã§ã¯ããããã§ã¹ããå°ãè£è¶³ã
ãŸã ã å®éãæšå¹Ž
ã®ããã³ããšã³ããã¬ãŒã ã¯ãŒã¯ã®
æ¯èŒãããäžåºŠèªãã ã®ã§ããããã§ã¹ãã§ãã®æ¯èŒãšäœããã®çžé¢é¢ä¿ãããç¹ã玹ä»ããŠã¿ãŸãããïŒ
æ®å¿µãªããã
Ractiveã®ããã©ãŒãã³ã¹ã«æ·±å»ãªåœ±é¿ãäžããããšã¯ã»ãšãã©ãããŸããïŒãã ããããã€ãã®æé©åãæäŸããŸãïŒã ãã ããä»ã®2ã€ã®ç¹æ§-
ãã³ãã«ã®
ãµã€ãº ãšã³ãŒãã®è¡æ° ããããžã§ã¯ããããã§ã¹ãã«è¿œå ã§ããŸãã ãããã£ãŠãæŽæ°ããããããã§ã¹ãã¯æ¬¡ã®ããã«ãªããŸãã
ãããžã§ã¯ã宣èšïŒ
- RealWorldãããžã§ã¯ãã®ä»æ§ã«æºæ ããŸãã
- ãµãŒããŒäœæ¥ãå®å
šã«ãµããŒãïŒSSRããã³ãã®ä»ãã¹ãŠïŒã
- æ¬æ ŒçãªSPAãšããŠã¯ã©ã€ã¢ã³ãã«åãçµã¿ãŸãã
- æ€çŽ¢ãšã³ãžã³ã«ãã£ãŠçŽ¢åŒä»ããããŸãã
- ã¯ã©ã€ã¢ã³ãã§JSããªãã«ããŠäœæ¥ããŸãã
- 100ïŒ
å圢ïŒäžè¬ïŒã³ãŒã;
- å®è£
ã®ããã«ããããŒãã¡ãžã£ãŒããšãã¯ã©ã³ããã䜿çšããªãã§ãã ããã
- æå€§ã®ã·ã³ãã«ã§ããç¥ãããæè¡ã¹ã¿ãã¯ã䜿çšããŸãã
- æçµçãªãã³ãã«ã®ãµã€ãºã¯ã100Kb gzipãè¶
ããªãããã«ããŠãã ããã
- ã¢ããªã±ãŒã·ã§ã³ã³ãŒãã®è¡æ°ã¯1000 locãè¶
ããŠã¯ãªããŸããã

ãã¡ãããç§ã¯äž¡æ¹ã®ææšããã®æ¯èŒãããã¹ãŠã®ãã¬ãŒã ã¯ãŒã¯ã®äžã§æé«ã§ããããšãæã¿ãŸãã ãã ãããã³ãã«ã®ãµã€ãºã®èгç¹ãã
Apprunãåé¿ããããšã¯ã§ããŸããã ããã§ãã19Kbã¯äžè¬ã«äœããã®éæ³ã§ãã
ãããã§ã¹ãã®ãã¹ãŠã®æ¡ä»¶ãæºãããåæã«ã³ãŒãã®è¡æ°ãšãã³ãã«ã®ãµã€ãºãä»ã®å®è£
ã®æå°å€ãšåçšåºŠã«ãªãã°ååã ãšæããŸãã ç°¡åã«èšãã°ãå®è£
ã¯ãã³ãã«ãµã€ãºã
React / Mobxããã³
Elmã¬ãã«ã§ãããã³ãŒãè¡æ°ã
Apprunããã³
CLJSåãã¬ãŒã ã¬ãã«ã§ããããšã
æã¿ãŸãã ãŸããä»ã®å®è£
ã宣èšããããã¹ãŠã®æ©èœãåããŠããªãããšãèãããšãäžçš®ã®ææãšãªããŸãã ããããåŸ
ã£ãŠãã ããã
ããŽã«ã€ããŠ

å¥ã®å°ããªäœè«ã
Ractiveã¯ã€ãã«
ããŽãšè²ã®ã¹ã¿ã€ã«ã倿ŽããŸããïŒ ãããã£ãŠãããã
ç§ã®æåºã§èµ·ãã£ãããšãå¬ããæããŸãã
ç§ã®ããŽãªãã·ã§ã³ãéžæãããªãã£ããšããäºå®ã«ãããããããç§ã¯ãã®ãããªä¿å®çãªã³ãã¥ããã£ãããç«ãŠãããšãã§ããããšã«ãŸã å°ãèªããæã£ãŠããŸãã ãã£ãïŒ
詳现ã«ã€ããŠ
ãã¥ãŒããªã¢ã«ã®åã®éšåã«ã¯æç¥šãå«ãŸããŠããããã®çµæã¯æå ±ã§ãã
80ïŒ
ãè¶
ããèªè
ããã®ãã¥ãŒããªã¢ã«ã®ãããã¯ãè峿·±ããšæããŠãããçŸåšã®è©³çްã¬ãã«ãæ¯æãã圢ã§å€ãã®äººãæèŠãè¿°ã¹ãŠããŸãã ãããã詳现ã«ã€ããŠã®ã¢ã³ã±ãŒããäœæããŠãçµæãç°ãªãããšãæ£çŽã«æã¿ãŸããã 誰ãããããããã¹ãŠãæç¢ºã§ããã詳现ã®ã¬ãã«ããããã£ãŠææã®éãæžããããšãã§ããŸãã ããã¯ããã§ã¯ãªãããšã倿ããŸããã
ãã®èª¿æ»ã®çµæã«ããããããããŸã å éããå¿
èŠããããŸãã å®è£
ã®å€ãã®åŽé¢ã衚é¢çã«ã®ã¿èª¬æããŸãããããªããšããã¥ãŒããªã¢ã«ãé·ãããŠãããããç§ãšããªããéå±ãããã§ãããã ããããããã¯çŽ æã®ãã®éšåã«ã¯åœãŠã¯ãŸããŸããïŒ å®éããã®ããŒãã§ã¯ãååã¢ããªã±ãŒã·ã§ã³ã®ãã¬ãŒã ã¯ãŒã¯ãäœæããããã«å¿
èŠãªäœæ¥ãå®éã«å®äºããããã§ãã
ããã«ãäœæãããã€ã³ãã©ã¹ãã©ã¯ãã£ããæäœãã
RealWorldãããžã§ã¯ãã®ä»æ§ãšãããã§ã¹ããã€ã³ããæ®µéçã«å®è£
ããŸãã ã¢ããªã±ãŒã·ã§ã³èªäœã®ã³ãŒãã®èšè¿°ãéå§ããŠããªãããšã«å床泚æãåããããšæããŸãããããã¯åé¡ã§ã¯ãªãããšãä¿èšŒããŸãã ãã®åŸãç©äºã¯èããå éããŸãã ã³ã¡ã³ãã§è©³çްãè°è«ããããšã§ããã®å éãšè©³çްã®å¿
ç¶çãªæžå°ãè£ãå¿
èŠããããšæããŸãã
ãããã ïŒ
ã«ãŒãã£ã³ã°

æåã«äž»ãªã¢ã€ãã¢ãç°¡åã«èª¬æããæ¬¡ã«å®è£
ã確èªããŸãã ããã³ããšã³ã2ã®äžçã§ã¯ãSPAã¢ããªã±ãŒã·ã§ã³å
ã®ã«ãŒãã£ã³ã°ã«å¯Ÿããäž»ãªã¢ãããŒããæ¯é
çã§ããããšã倿ããŸããã
æ§æããŒã¹ã®ã«ãŒãã£ã³ã°ïŒAngularïŒCoïŒ
ç¹å®ã®æ§æãã¡ã€ã«å
ã§ãäžçš®ã®ãããŒãžããšããŠæ©èœãããã¹ïŒã«ãŒãïŒããã³ã³ã³ããŒãã³ããžã®å¯Ÿå¿ã®ãªã¹ããæ±ºå®ããæ¹æ³ã æ¡ä»¶ä»ãã§ã次ã®ããã«ãªããŸãã
const routes = [ { path: /some pattern/, component: MyComponentConstructor, ...otherOptions }, ];
åæã«ããã³ãã¬ãŒãã«ã¯ãååãšããŠãããªã¬ãŒãããã³ã³ããŒãã³ãã衚瀺ãããã¢ã³ã«ãŒèŠçŽ ïŒã³ã³ããŒãã³ããŸãã¯ã¿ã°ã®ã¿ïŒããããŸãã
ã³ã³ããŒãã³ãããŒã¹ã®ã«ãŒãã£ã³ã°ïŒReactïŒCoïŒ
ã«ãŒãã¯ãç¹å¥ãªã«ãŒãã³ã³ããŒãã³ãã䜿çšããŠãã³ãã¬ãŒãã§çŽæ¥å®çŸ©ãããŸãããããã®ã³ã³ããŒãã³ãã¯ãããããã£ãéããŠãã«ãŒããã¿ãŒã³ããã®ä»ã®å¿
èŠãªãªãã·ã§ã³ãåããŸãã ãããã£ãŠããããŒãžãã§ããããŒã¯ã¢ããã¯ã«ãŒãã£ã³ã°ã³ã³ããŒãã³ãã®ã¿ã°å
ã«ãããæ¬¡ã®ããã«ãªããŸãã
<Route path="some pattern" ...otherOptions> <MyComponent ...someProps /> </Route>
ãããã®ã¢ãããŒããæªãã®ã¯ãªãã§ããïŒ çãã¯äœã§ããããŸããã ãã ããäž¡æ¹ã®ã¢ãããŒãã«ã¯ããã€ãã®æ¬ ç¹ããããŸãã
- æ§æããŒã¹ã®ã«ãŒãã£ã³ã° -å®åæãå€ãããã³ã³ããã¹ãããé ãããŸãã ååãšããŠãã«ãŒãã¯1ã€ã®ç¹å®ã®ã³ã³ããŒãã³ãã«è§£æ±ºãããŸãããããã¯ããŸãæè»æ§ããããŸããã
- ã³ã³ããŒãã³ãããŒã¹ã®ã«ãŒãã£ã³ã°ã¯ã³ã³ããã¹ãã«è¿ãã§ãããäœããã®çç±ã§ã³ã³ããŒãã³ãã¿ã°ãå®éã«æ¡ä»¶ã¹ããŒãã¡ã³ããšããŠäœ¿çšãããŸãã ã«ãŒãã£ã³ã°ã«å¿
èŠãªãã¹ãŠã®ãªãã·ã§ã³ãäºæž¬ããããšã¯é£ãããããåžžã«ã«ãŒãã³ã³ããŒãã³ãã®æ©èœïŒã€ãŸããåãå
¥ããããšãã§ããèšå®ïŒã«ãã£ãŠå¶éãããŸãã
ããã¯ãã¹ãŠãéåžžã«ãã§ãããããŠããå¯èœæ§ããããŸãã ãã ãããããã®äž¡æ¹ã®ã¢ãããŒãã«ã¯1ã€ã®ãã€ãã¹ãå¿
ããããŸããããã®ã«ãŒãã¯ãã®ãããªã³ã³ããŒãã³ãã§ããããã®ã«ãŒãã¯ãã®ãããªã³ã³ããŒãã³ãã§ãããªã©ãã«ãŒãã£ã³ã°ã®æè»æ§ãäœãããã§ãã
åæã«ãã»ãšãã©ã®å Žåãã¯ã©ã€ã¢ã³ãã«ãŒãã£ã³ã°ã®ããžãã¯ã¯ãçŸåšã®URLãšã®èŠåæ§ã®äžèŽã ãã«éå®ãããŸããã ã¢ããªã±ãŒã·ã§ã³ã®äžè¬çãªç¶æ
ïŒ
state ïŒããåé¢ããŠã«ãŒãã£ã³ã°ãæ€èšããããšã¯å®å
šã«æ£ãããšã¯æããŸããã ããã¯ãä»ã®ããŒã¿ãšåãç¶æ
ã®éšåã§ãã ãããã£ãŠãã¢ããªã±ãŒã·ã§ã³ã®ç¶æ
ãšUIã®ç¶æ
ïŒããžã¥ã¢ã«ã³ã³ããŒãã³ãïŒãã§ããã ãæè»ã«äœ¿çšã§ããããã«ããã«ã¯ãä»ã®ã¢ãããŒãã䜿çšããå¿
èŠããããŸãã
ç¶æ
ããŒã¹ã®ã«ãŒãã£ã³ã°
ãŸããäŸãæããŸãããŠãŒã¶ãŒã®ãã°ã€ã³ãã©ãŒã ãå«ãã¢ãŒãã«ãŠã£ã³ããŠãžã®ãªã³ã¯ããããµã€ãããããŒããããŸãã ãã¡ããããã®ãªã³ã¯ããã°ã€ã³ããŠãããŠãŒã¶ãŒã«è¡šç€ºãããªãããã«ããã«ã¯ã次ã®ããã«ããŸãã
{{#if ! loggedIn}} <a href="">Login</a> {{/if}}
ããã¯å®å
šã«æ£åžžã§ããããã§ã¯ããŠãŒã¶ãŒããã°ã€ã³ããŠãããã©ããã®çŸåšã®ã¹ããŒã¿ã¹ããã§ãã¯ããŸãã
å¥ã®èŠä»¶-ãã®ãªã³ã¯ã¯ãçŽæ¥ãªã³ã¯ãšåæ§ã«ããµã€ãã®ä»»æã®ããŒãžã§ãã°ã€ã³ãã©ãŒã ãéãå¿
èŠããããŸãã ã¢ãŒãã«ãŠã£ã³ããŠã¯çŸåšã®ããŒãžã®äžéšã§ããããããã®ã¢ãŒãã«ãŠã£ã³ããŠãéãçŽæ¥ãªã³ã¯ã«
URLãã©ã°ã¡ã³ã ïŒå
±é
ããã·ã¥å
ïŒã䜿çšããããšã¯è«ççã§ãã ãã®ãããªã«ãŒãã®ãã¿ãŒã³ã¯æ¬¡ã®ããã«ãªããŸãã
'/*#login'
察å¿ãã
ããã·ã¥ãæå®ããã ãã§ã远å ã®ã¢ã¯ã·ã§ã³ãªãã§ä»»æã®ããŒãžã§ã¢ãŒãã«ãŠã£ã³ããŠãéãããšãã§ããŸãã
{{#if ! loggedIn}} <a href="/{{currentPath}}#login">Login</a> {{/if}}
ãŸãããã©ãŠã¶ã®
ãæ»ãããã¿ã³ãŸãã¯
history.backïŒïŒãã¯ãªãã¯ããã ãã§ããã®ã¢ãŒãã«ãŠã£ã³ããŠãéããŸãã
ãã ãããã¹ãŠãæ£åžžã«æ©èœããããã«ã¯ã
ç¶æ
ã®ãã1ã€ã®éšåã
loggedInããã§ãã¯ããå¿
èŠããããŸãã äžèšã®ã«ãŒãã£ã³ã°æ¹æ³ã®ããããã䜿çšãããšã©ããªããŸããïŒ æ¿èªã確èªããå¥ã®ã³ã³ããŒãã³ãã§ã¢ãŒãã«ã³ã³ããŒãã³ããã©ããããŸããïŒ
<Route path="/*#login"> <NotAuthorized> <Modal> <form>...</form> </Modal> </NotAuthorized> </Route>
ãŸããããããã§ããã ãããããã®ãããªè¿œå ã®æ¡ä»¶ãããã€ãããå Žåã¯ã©ãã§ããããïŒ ãµã
ããã§ããããã«ã€ããŠèããŠãã¢ããªã±ãŒã·ã§ã³ã®å
šäœçãªç¶æ
ã®äžéšãšããŠã«ãŒããèæ
®ãããšã©ããªããŸããïŒ ã«ãŒããå·ã®ä»ã®éšåãšé£åããŠæ©èœããå€ãã®ã±ãŒã¹ãæãã€ãããšãã§ããŸãã ããã§ã¯ããªã远å ã®æ§æã§ãããåºå¥ããããããæ¹æ³ã§ä»ã®ç¶æ
ããåé¢ããã®ããããªã«å¿é
ãªã®ã§ããããïŒ åãããŸãã
ãªãç§ã¯ããããã¹ãŠæžããŠããã®ã§ããïŒããã¯ååãšã©ã®ããã«é¢ä¿ããŠããŸããïŒ å®éã«ã¯ãæ¹æ³ã¯ãããŸããïŒïŒïŒïŒã«ãŒãã£ã³ã°ãšããŠæ©èœããç§ã®ã³ãŒãã«åæ§ã®æ°åããªãæ§é ã衚瀺ãããŠãé©ããªãããã«ãããã ãã§ãïŒ
{{#if $route.match('/*#login') && ! loggedIn }} <modal> <form>...</form> </modal> {{/if}}
ã芧ã®ãšããããã®ã¢ãããŒãã䜿çšããå Žåãæ°ããæ§æãçºæãããã远å ã®ã³ã³ããŒãã³ããäœæããããæ§æãæ§æãããããå¿
èŠã¯ãããŸããã ã¢ããªã±ãŒã·ã§ã³ã®å
šäœçãªç¶æ
ã®äžéšãšããŠã«ãŒãã䜿çšããã ãã§ãå®éã«ã¯ãæ°è¡ã®ã³ãŒãã§ããããçš®é¡ã®ã¯ã¬ã€ãžãŒãªããšãå®è¡ã§ããŸãã
ãããŠä»ãã±ãŒã¹ã«ã å圢ã«ãŒãã£ã³ã°ã«ã¯ãéèŠãª3ã€ã®äž»èŠãªãã€ã³ãã®ã¿ããããŸãã
- ã«ãŒã¿ãŒã§ã¯ãçŸåšã®URLãæåã§èšå®ãããããã®å€æŽããã£ã¹ãããããããšãã§ããŸãã
- NodeJSç°å¢ã§äžæããªãã§ãã ããã ç°å¢åºæã®ãã®ããã®æœè±¡åã
- ã«ãŒãã£ã³ã°ã¯ããå€éšãã§ã¯ãªããã¢ããªã±ãŒã·ã§ã³ã®ãå
éšãã«ããå¿
èŠããããŸãã
å€ãã®å Žåãéçºè
ãã©ã®ããã«ã«ãŒãã£ã³ã°ããå€ã«ãåºããäžè¬çãªç¶æ
ãã³ã³ããã¹ãããé ãããã®ããçè§£ããŠããŸãã ãŸããå圢ã¢ããªã±ãŒã·ã§ã³ãäœæããããšãã人ã¯ããµãŒããŒïŒããšãã°ã
Express ïŒãšã¯ã©ã€ã¢ã³ãã«ãŒãã£ã³ã°ãå¥ã
ã«æå³çã«äœ¿çšããŠããããã§ãã æã
å
±éã®èšå®ã§ãæã«ã¯åã
ã®èšå®ã§ãã ããããæ²ããããšã«ã€ããŠã¯ååã§ãã
ç§ã®ãããžã§ã¯ãã§ã¯ã
Ractiveçš
ã®ã«ãŒã¿ãŒ
ãã©ã°ã€ã³ã䜿çšã
ãŠããŸãã å®éãããã¯
PageJSãš
qsã®ã©ãããŒã§
ãã ãã«ãŒãã£ã³ã°ã«å¯Ÿãã
ç¶æ
ããŒã¹ã®ã¢ãããŒããå®è£
ããŠããŸãã ãã®ãã«ãŒã¿ãŒãã®ãã€ãã£ãã³ãŒãã¯ã匷ããã100è¡ã®ã³ãŒããååŸããå®éã«ã«ãŒã¿ãŒã®
ç¶æ
ãã¢ã¯ãã£ããªãªã¢ã¯ãã£ã
ç¶æ
ã« ããŸãã¯ãã®éã«ãããã·ããŸãã ã«ãŒã¿ãŒã¯ããã¹ãŠã®ã³ã³ããŒãã³ãã«ã°ããŒãã«ã«é©çšãããããã«äœ¿çšå¯èœã«ãªããã³ã³ããŒãã³ãã®ç¹å®ã®ã€ã³ã¹ã¿ã³ã¹ã«åé¢ããŠé©çšãããŸãã ããã«ãããããããçš®é¡ã®ããšãã§ããŸãïŒ
{{#if $route.match('/products/:id') }} <product id="{{$route.params.id}}" cart="{{$route.state.cart}}"></product> {{#if ! loggerIn }} <a href="#login">Login to buy it</a> {{/if}} {{elseif $route.match('/products') }} <products filters="{{$route.query}}"></products> {{else}} <p>404 - Not found</p> <a href="/products">Go to search the best products</a> {{/if}} {{#if $route.match('/*#login') && ! loggerIn }} <modal> <form>...</form> </modal> {{/if}}
ãããŠãã®ãããªïŒ
ã³ãŒããæžã
ãŸãã«ãŒã¿ãŒãã¢ããªã±ãŒã·ã§ã³ã«æ¥ç¶ããå圢ã§ããããšãæããŸãããã
./src/app.js Ractive.use(require('ractive-page')({ meta: require('../config/meta.json') }));
å®å
šãªã³ãŒã./src/app.js const Ractive = require('ractive'); Ractive.DEBUG = (process.env.NODE_ENV === 'development'); Ractive.DEBUG_PROMISES = Ractive.DEBUG; Ractive.defaults.enhance = true; Ractive.defaults.lazy = true; Ractive.defaults.sanitize = true; Ractive.use(require('ractive-page')({ meta: require('../config/meta.json') })); const options = { el: '#app', template: `<div id="msg">Static text! + {{message}} + {{fullName}}</div>`, data: { message: 'Hello world', firstName: 'Habr', lastName: 'User' }, computed: { fullName() { return this.get('firstName') + ' ' + this.get('lastName'); } } }; module.exports = () => new Ractive(options);
./middleware/app.js const route = app.$page.show(req.url, null, true, false); ... const meta = route.state.meta;
å®å
šãªã³ãŒã./middleware/app.js const run = require('../src/app'); module.exports = () => (req, res, next) => { const app = run(), route = app.$page.show(req.url, null, true, false); const meta = route.state.meta, content = app.toHTML(), styles = app.toCSS(); app.teardown(); res.render('index', { meta, content, styles }); };
ããã§ãšããä»ã®ã¢ããªã±ãŒã·ã§ã³ã«ã¯å®å
šã«å圢ã®ã«ãŒãã£ã³ã°ããããŸãïŒ ãµãŒããŒã§ã¯ãçŸåšã®URLãã«ãŒã¿ãŒã«èšå®ããããããé©çšããã ãã§ããããšã«æ³šæããŠãã ããã ã«ãŒã¿ãŒãæå®ãããæ¡ä»¶ãæºãããŠããå Žåã«è¡ãå¿
èŠãããã®ã¯ããã ãã§ãã ãŸããå®å
šã«æšæºçãªåç
§ã䜿çšããŸããããã¯ãååãšæŒžé²çãªæ¹åã®ã³ã³ããã¹ãã§éåžžã«éèŠã§ãã
ããã«ãã¯ã©ã€ã¢ã³ããšãµãŒããŒã®äž¡æ¹ãåçãªã¡ã¿ã¿ã°ïŒã¿ã€ãã«ã説æãããŒã¯ãŒãïŒããµããŒãããããã«ãªããŸããããããã¯ç¹å¥ãªæ§æã§ç»é²ãããåæåæã«ã«ãŒã¿ãŒã«æ¥ç¶ãããŸãã ãã®èšå®ã¯éåžžã«ã·ã³ãã«ã«èŠãããªãã·ã§ã³ã§ãã
./config/meta.json { "/" : { "title": "Global Feed", "description": "", "keywords": "" }, ... }
ã«ãŒã¿ãŒã䜿çšããŠè€æ°ã®ããŒãžãäœæããŸãããã ãããè¡ãã«ã¯ãã¡ã€ã³ã¢ããªã±ãŒã·ã§ã³ãã³ãã¬ãŒãïŒ
app.html ïŒãšãããããŒïŒ
navbar.html ïŒããã³ããã¿ãŒïŒ
footer.html ïŒã®
ããŒã·ã£ã«ãäœæããŸãã ãããè¡ãã«ã¯ã
RealWorld仿§ãã
æ¢è£œã®ããŒã¯ã¢ãããã³ããŒããŠãããã€ãã®ãã€ããã¯ã¹ã远å ããŸãã
./src/templates/partials/navbar.html <nav class="navbar navbar-light"> <div class="container"> {{#with @shared.$route.pathname as pathname}} <a class="navbar-brand" href="/">conduit</a> <ul class="nav navbar-nav pull-xs-right"> <li class="nav-item"> <a href="/" class-active="pathname === '/'" class="nav-link"> Home </a> </li> <li class="nav-item"> <a href="/login" class-active="pathname === '/login'" class="nav-link"> Sign in </a> </li> <li class="nav-item"> <a href="/register" class-active="pathname === '/register'" class="nav-link"> Sign up </a> </li> </ul> {{/with}} </div> </nav>
./src/templates/partials/footer.html <footer> <div class="container"> <a href="/" class="logo-font">conduit</a> <span class="attribution"> An interactive learning project from <a href="https://thinkster.io">Thinkster</a>. Code & design licensed under MIT. </span> </div> </footer>
./src/templates/app.html <div id="page"> {{>navbar}} {{#with @shared.$route as $route }} {{#if $route.match('/login')}} <div fade-in-out> <div class="alert alert-info"><strong>Login</strong>. {{message}}</div> </div> {{elseif $route.match('/register')}} <div fade-in-out> <div class="alert alert-info"><strong>Register</strong>. {{message}}</div> </div> {{elseif $route.match('/')}} <div fade-in-out> <div class="alert alert-info"> <strong>Hello, {{fullName}}!</strong> You successfully read please <a href="/login" class="alert-link">login</a>. </div> </div> {{else}} <div fade-in-out> <p>404 page</p> </div> {{/if}} {{/with}} {{>footer}} </div>
ãŸãããããã®ãã³ãã¬ãŒããã¢ããªã±ãŒã·ã§ã³ã€ã³ã¹ã¿ã³ã¹ã«ç»é²ããããšãå¿ããªãã§ãã ããã
./src/app.js const options = { el: '#app', template: require('./templates/parsed/app'), partials: { navbar: require('./templates/parsed/navbar'), footer: require('./templates/parsed/footer') }, transitions: { fade: require('ractive-transitions-fade'), }, .... };
å®å
šãªã³ãŒã./src/app.js const Ractive = require('ractive'); Ractive.DEBUG = (process.env.NODE_ENV === 'development'); Ractive.DEBUG_PROMISES = Ractive.DEBUG; Ractive.defaults.enhance = true; Ractive.defaults.lazy = true; Ractive.defaults.sanitize = true; Ractive.use(require('ractive-page')({ meta: require('../config/meta.json') })); const options = { el: '#app', template: require('./templates/parsed/app'), partials: { navbar: require('./templates/parsed/navbar'), footer: require('./templates/parsed/footer') }, transitions: { fade: require('ractive-transitions-fade'), }, data: { message: 'Hello world', firstName: 'Habr', lastName: 'User' }, computed: { fullName() { return this.get('firstName') + ' ' + this.get('lastName'); } } }; module.exports = () => new Ractive(options);
æ°é
ãã®ããèªè
ã¯ããã§ã«ããã€ãã®ç¹ã«æ°ä»ããŠãããšç¢ºä¿¡ããŠããŸãã ãã¿ãã¬ã®äžã§ãããã«ã€ããŠèªçºçã«èªãã§ãã ããã
é·ç§»ã¢ãã¡ãŒã·ã§ã³Ractiveã«ã¯ãèŠçŽ ã衚瀺ãŸãã¯é衚瀺ã«ãªã£ããšãã«ãã¢ãã¡ãŒã·ã§ã³åãããé·ç§»ïŒ
transition ïŒãäœæããæ©èœããããŸãã ãããè¡ãã«ã¯ã察å¿ããç§»è¡ãã©ã°ã€ã³ãã€ã³ããŒããïŒ
ractive-transitions-fade ïŒãã°ããŒãã«ãŸãã¯ããŒã«ã«ã«ç»é²ããç¹å¥ãªãã£ã¬ã¯ãã£ãã䜿çšããŠãã©ã°ã€ã³ã䜿çšããå¿
èŠããããŸãïŒ
fade-in-out ïŒã
ãã®å Žåãããã©ã«ãèšå®ã§å¹³å¡ãª
ãã§ãŒãã䜿çšããŸããããã©ã°ã€ã³ã¯èšå®ã®èšå®ããµããŒãããŸããããšãã°ïŒ
<div fade-in="{ duration: 500 }"></div> <div fade-out="{ delay: 500 }"></div>
ãã³ãã¬ãŒãã®äºåè§£æRactiveã¯ãã³ã³ããŒãã³ãã®ãã³ãã¬ãŒããç»é²ããããã®ããã€ãã®ãªãã·ã§ã³ããµããŒãããŠããŸãã
æ¢ã«çè§£ããããã«ã
Ractiveã¯æœè±¡æ§æããªãŒïŒ
AST ïŒãå®å
šã«ãµããŒãããŠããŸãã å®éããã¹ãŠã®ãªãã·ã§ã³ã¯æçµçã«
ASTã«å€æãããããã«åºã¥ããŠ
ã©ã³ã¿ã€ã ã§äœæ¥ãé²è¡äžã§ãã ãããã£ãŠãäœæ¥ã®é床ãæé©åããããã«ã.htmlãã³ãã¬ãŒãã
ASTã§ããªã³ã³ãã€ã«ãã
ã©ã³ã¿ã€ã ã§ã¯è§£æã«ãªãœãŒã¹ãè²»ãããŸããã ããã¯ãwebpackããã«ããããåã«å®è¡ããã
npm run parseã³ãã³ãã䜿çšããŠè¡ãããŸãã
ã¯ã©ã¹ã«ã€ããŠ-*æ¡ä»¶ã«å¿ããŠã¯ã©ã¹ãç°¡åã«åãæ¿ããããšãã§ããç¹å¥ãª
Ractiveãã£ã¬ã¯ãã£ãïŒ
<a href="/login" class-active="pathname === '/login'" class="nav-link">Login</a>
ãã®å Žåããã¹ã®å€æŽã远跡ããã¢ã¯ãã£ããªã¡ãã¥ãŒé
ç®ã匷調衚瀺ããŸãã
@sharedã«ã€ããŠãã®ããšã¯ã
Ractiveã§ã³ã³ããŒãã³ãéã§ããŒã¿ãå
±æããããã«äœ¿çšãããŸããäŸïŒ
ããŒã«ã«ã³ã³ããŒãã³ãã®ç¶æ
ãšåæ§ã«ãå
±æç¶æ
ã¯ãªã¢ã¯ãã£ãã§ãããèšç®ãããããããã£ã®äŸåé¢ä¿ã§äœ¿çšããã倿Žã«ãµãã¹ã¯ã©ã€ãã§ããŸãã
{{#with}}ã«ã€ããŠwithã³ã³ã¹ãã©ã¯ãã®javascriptã®ããã«ããã®ãããã¯åŒã¯æ°ããã¹ã³ãŒãããŸãã¯ãã³ãã¬ãŒãå
ã®ã³ã³ããã¹ããäœæããŸãã ççž®ãã¹ïŒããŒãã¹ïŒãŸãã¯ããã»ãã³ãã£ãã¯ãªåœåã䜿çšãããšéåžžã«äŸ¿å©ã§ãã
{{#with foo.bar.baz.qux as qux, data as articles}} {{ qux }} {{ articles }} {{/with}}
çµæïŒæåŸã«ç§ãã¡ãæã£ãŠãããã®ïŒ- å圢ã«ãŒãã£ã³ã°ãã¯ã©ã€ã¢ã³ããšãµãŒããŒã®äž¡æ¹ã§å€æŽãªãã§æ©èœããŸãã
- å®å
šã«æ©èœãããã©ãŠã¶ãŒå±¥æŽã
- ããŒãžéã®é·ç§»ã®ã¢ãã¡ãŒã·ã§ã³ïŒãããŸã§ã®ãšãããèŠãç®ã¯ããŸãè¯ããããŸãããã調æŽããããšãã§ããŸãïŒ;
- ã¯ã©ã€ã¢ã³ããšSSRäžã®äž¡æ¹ã®å®éã®ã¡ã¿ã¿ã°ã
ããŒã¿ååŸ

次ã®ãããããå圢ã¢ããªã±ãŒã·ã§ã³ã®æãèŠçãªãããã¯ã¯ãããŒã¿ã®æäœã§ãã åé¡ã¯äœã§ããïŒ å®éããã®ãã¡ã®2ã€ããããŸãã
- ãµãŒããŒãžã®éåæããŒã¿ã®èªã¿èŸŒã¿ã
- ã¯ã©ã€ã¢ã³ãã§ããŒã¿ããªããŒãããŸãã
äžèŠãããšããããã®è³ªåã¯éåžžã«çè§£ãããããäºçްãªããšã§ãããããŸãã ããããç§ãã¡ã¯åã«è§£æ±ºçãæ¢ããŠããã ãã§ãªããçŸãã解決çãæ¢ããŠããŸããæãéèŠãªããšã¯ãæãååã®è§£æ±ºçãæ¢ããŠããŸãã ãã®ãããããšãã°ããµãŒããŒäžã®ããŒã¿ãäºåã«ïŒæ¬è³ªçã«åæçã«ïŒããŠã³ããŒããããŠããã¢ããªã±ãŒã·ã§ã³ãèµ·åïŒåæ/ããªãã§ããïŒãããåãã¯ã©ã€ã¢ã³ãäžã§éåæçã〠"æ i"ïŒéåæ/é
å»¶ïŒã«ãªã£ãå Žåãªã©ããœãªã¥ãŒã·ã§ã³ããæ£é¢ãã«é©ããŠããŸããã å€ãã®äººããããè¡ããŸãããããã¯ç§ãã¡ã®éžæè¢ã§ã¯ãããŸããã
ç§ãã¡ã¯ãããããã³ã³ããŒãã³ãå
ã®ããããã¬ãã«ã®ãã¹ãã§ããã€ã§ãã©ãã§ãããŒã¿ãåäžã«ãã§ãã§ããããã«ããããšèããŠããŸãã ã³ãŒãå
ãã³ã³ããŒãã³ãããã¯ããŸãã¯ãã®ä»ã®å Žæã ãããŠæãéèŠãªããšã¯ãæããæ lazãªããããªãã¡ ã¯ã©ã€ã¢ã³ããšãµãŒããŒã®äž¡æ¹ã§ã¢ããªã±ãŒã·ã§ã³ã®çŸåšã®ç¶æ
ã衚瀺ããããã«å¿
èŠãªããŒã¿ã®ã¿ãããŒãããã®ãçŸå®çã§ãã ãããŠãããããã¹ãŠã§ãã¯ã©ã€ã¢ã³ããšãµãŒããŒã®ããŒã¿èªã¿èŸŒã¿ã³ãŒããå
±éã«ããå¿
èŠããããŸãã ãã£ãããïŒ ããã§ãç§ãã¡ã¯äœãåŸ
ã£ãŠããŸããïŒ

é¢çœããŠéåæã§ãããããããããã¹ãŠã«ã€ããŠã¯ã©ã€ã¢ã³ãã«åé¡ã¯ãããŸããã ãµãŒããŒäžã§ãéåæã§ãããæ®å¿µãªãã
SSRã®ããã«å±ããHTTPãªã¯ãšã¹ãã¯ããã§ã¯ãããŸããã ããã¯ãããæç¹ã§ãã¢ããªã±ãŒã·ã§ã³ã®ç¶æ
ãHTMLã§ã¬ã³ããªã³ã°ããã¯ã©ã€ã¢ã³ãã«éä¿¡ããå¿
èŠãããããšãæå³ããŸãã ãããŠãäž»ãªããšã¯ããã¹ãã®ãã¹ãŠã®ã¬ãã«ã§ããã¹ãŠã®ã³ã³ããŒãã³ãã®ãã¹ãŠã®å¿
èŠãªããŒã¿ãæ¢ã«ããŒããããŠããå Žåã«ã®ã¿ãããè¡ãããšã§ãã åé¡ãšæã¯ããã«äºåã«å±ããŸãããç§ãã¡ã¯å
±éã®å©çã®ããã«èªåèªèº«ãæå¶ããŸãã
å®éãããããã¹ãŠãæŽçããæ¹æ³ã¯ãããããããšç¢ºä¿¡ããŠããŸãã ç§ãèªåã§äœ¿çšããéåžžã«äŸ¿å©ã ãšæãæ¹æ³ã«ã€ããŠã®ã¿èª¬æããŸãã ãã®ããã«ã
Ractiveçšã®å¥ã®
ãã©ã°ã€ã³ã䜿çšã
ãŸã ã ãã©ã°ã€ã³å
šäœã«ã¯ã
Ractiveã³ã³ã¹ãã©ã¯ã¿ãŒã®ãããã¿ã€ãã«3ã€ã®è¿œå ã¡ãœããã远å ããçŽ100è¡ã®ã³ãŒãã
å«ãŸããŠããŸãã
ãããã®ã¡ãœããã䜿çšããŠãåŸ
æ©ã
SSRã®éèŠãªéšåã§ããéåææäœãå€å¥ã§ããŸãã ãŸãããæåŸ
ãã«è¿œå ããããã¹ãŠã®ããŒã¿ãæœåºãããããšãä¿èšŒããããã€ã³ãïŒã³ãŒã«ããã¯é¢æ°ïŒãååŸããŸãã ãããšã¯å¥ã«ããã®ã¢ãããŒãã«ããã
SSRã«åå ããããŒã¿ãšåå ããªãããŒã¿ãæç¢ºã«æ±ºå®ã§ãããšããäºå®ã«æ³šç®ããŸãã
SSRã®æé©åã«äŸ¿å©ãªå ŽåããããŸãã ããšãã°ããµãŒããŒäžã§ã¯ã³ã³ãã³ãã®ã¡ã€ã³éšåã®ã¿ãã¬ã³ããªã³ã°ãïŒæ€çŽ¢ãšã³ãžã³çšãŸãã¯
SSRãé«éåããããïŒãã»ã«ã³ããªããŒãã¯ã¯ã©ã€ã¢ã³ãäžã§æ¢ã«ãåžãäžãããããŠããŸãã ããã«ã2çªç®ã®åé¡ã®è§£æ±ºã«åœ¹ç«ã€ã®ã¯ãããã®æ¹æ³ã§ãããæåã«ãããææ¡ããŸãããã
ããã§ãå¿
èŠãªããŒã¿ã®ããŒããæåŸ
ããHTMLãæééãã«ã¬ã³ããªã³ã°ãããããµãŒããŒã«æããŸããã ããã«ãæ¢æã®ã¬ã€ã¢ãŠããã¯ã©ã€ã¢ã³ãã«æäŸããããã¹ããŒãããª
Ractiveã¯
ããã
æ°ŽçŽ åããäºå®ã§ãïŒ
ããŒã2ãåç
§ïŒã ãµãŒããŒäžãšãŸã£ããåãã³ãŒããèµ·åãããã³ã³ããŒãã³ãã®éå±€ãã¹ãã³ã¢ãããå§ãããµãŒããŒäžã§å¿
èŠãªããŒã¿ããã§ããããã³ãŒããå®è¡ãéå§ããŸãã
ãŸãã2ã€ã®éèŠãªãã€ã³ãããããŸãããŸãããã§ãã¯ãµã ãåæããããšã¯éåžžã«éèŠã§ãã ã€ãŸããããŒã¯ã¢ãããåå©çšããã«ã¯ãããŒã¿ããµãŒããŒäžãšåãã§ããå¿
èŠããããŸãã 第äºã«ããµãŒããŒãåã³å®è¡ãããã¹ãŠã®APIãã¯ã©ã€ã¢ã³ãã«å®è¡ãããããªãã§ãããã

ããã§ã®æãããªè§£æ±ºçã¯ããµãŒããŒã§åéãããããŒã¿ãïŒã§ããã°æ£èŠåããã圢åŒã§ïŒã¯ã©ã€ã¢ã³ãã«è»¢éããããšã§ãããæãéèŠãªããš
ã¯ãããŒã¿ã®
ãªããŒããé²ãã
æ°Žåè£çµŠãå£ããªãããã«äœããã®æ¹æ³ã§ãããã®ããŒã¿ãã¯ã©ã€ã¢ã³ãã«åæ£ããããšã§ãã ã¿ã¹ã¯ã§ãããå®éã«ã¯åçŽã«è§£æ±ºãããŸãã
ã³ãŒããæžã
ãã®ãããæåã«ãã©ã°ã€ã³ãç»é²ãïŒ
ractive-ready ïŒããµãŒããŒäžã§ã¢ããªã±ãŒã·ã§ã³ãæéå
ã«ã¬ã³ããªã³ã°ããæ¹æ³ãåŠç¿ããåéããããã¹ãŠã®ããŒã¿ãæ§é åãããæ¹æ³ã§ååŸããŸãã
./src/app.js Ractive.use(require('ractive-ready')());
å®å
šãªã³ãŒã./src/app.js const Ractive = require('ractive'); Ractive.DEBUG = (process.env.NODE_ENV === 'development'); Ractive.DEBUG_PROMISES = Ractive.DEBUG; Ractive.defaults.enhance = true; Ractive.defaults.lazy = true; Ractive.defaults.sanitize = true; Ractive.use(require('ractive-ready')()); Ractive.use(require('ractive-page')({ meta: require('../config/meta.json') })); const options = { el: '#app', template: require('./templates/parsed/app'), partials: { navbar: require('./templates/parsed/navbar'), footer: require('./templates/parsed/footer') }, transitions: { fade: require('ractive-transitions-fade'), }, data: { message: 'Hello world', firstName: 'Habr', lastName: 'User' }, computed: { fullName() { return this.get('firstName') + ' ' + this.get('lastName'); } } }; module.exports = () => new Ractive(options);
./middleware/app.js app.ready((error, data) => { .... data = JSON.stringify(data || {}); error = error && error.message ? error.message : error; res.render('index', { meta, content, styles, data, error }); });
å®å
šãªã³ãŒã./middleware/app.js const run = require('../src/app'); module.exports = () => (req, res, next) => { const app = run(), route = app.$page.show(req.url, null, true, false); app.ready((error, data) => { const meta = route.state.meta, content = app.toHTML(), styles = app.toCSS(); app.teardown(); data = JSON.stringify(data || {}); error = error && error.message ? error.message : error; res.render('index', { meta, content, styles, data, error }); }); };
ãã¹ãŠã®ããã«ã Ready-Callbackã䜿çšãããšãããŒã¿ã®ããŒããåŸ
æ©ã§ããã ãã§ãªãããã®ããŒã¿ã2çªç®ã®åŒæ°ãšããŠæ§é åããã圢åŒã§åä¿¡ã§ããŸãã
NodeJSã§åãå
¥ããããŠããæåã®åŒæ°ã¯ããã®ããã»ã¹äžã«çºçããå¯èœæ§ã®ãããšã©ãŒã§ãã ããŒã¿ã¯ã³ã³ããŒãã³ãã®éå±€ã«åŸã£ãŠæ§é åãããŸããããã«ãããã¯ã©ã€ã¢ã³ãäžã®åã³ã³ããŒãã³ãã¯ãæ§é å
šäœã§ç¬èªã®ããŒã¿ãèŠã€ããããšãã§ããŸãã æ¬¡ã«ããµãŒããŒã¬ã³ããªã³ã°çšã«ãããã®å€ãããããããŠãããŒãžã«é
眮ããŸãã
./src/templates/_index.html {{#error}} <div class="alert alert-danger">{{ error }}</div> {{/error}} ... <script> window.__DATA__ = {{& data }} </script>
å®å
šãªã³ãŒã./src/templates/_index.html <!doctype html> <html lang="en" dir="ltr"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="description" content="{{ meta.description }}"> <meta name="keywords" content="{{ meta.keywords }}"/> <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0"> <title>{{ meta.title }}</title> <link rel="stylesheet" href="//code.ionicframework.com/ionicons/2.0.1/css/ionicons.min.css"> <link rel="stylesheet" href="//fonts.googleapis.com/css?family=Titillium+Web:700|Source+Serif+Pro:400,700|Merriweather+Sans:400,700|Source+Sans+Pro:400,300,600,700,300italic,400italic,600italic,700italic"> <link rel="stylesheet" href="//demo.productionready.io/main.css"> <link rel="shortcut icon" type="image/x-icon" href="/favicon.ico"> <link rel="icon" type="image/png" href="/img/favicon.png"> <link rel="apple-touch-icon" href="/img/favicon.png"> <link rel="manifest" href="/manifest.json"> <style> {{& styles }} </style> </head> <body> {{#error}} <div class="alert alert-danger">{{ error }}</div> {{/error}} <div id="app"> {{& content }} </div> <script> window.pageEl = document.getElementById('page'); </script> <script> window.__DATA__ = {{& data }} </script> </body> </html>
ããŒã¿ã
ãŠã£ã³ããŠ.__ DATA__ã«é
眮ããã ãã§ãã¯ã©ã€ã¢ã³ãã§ãããæ¢ããŸãã
ããã¯ã¹ãã³ãããã®ãšã³ãããŒãã©ã®ããã«æ©èœãããã確èªããå¿
èŠããããŸããã€ãŸããå°ãªããšã1ã€ã®éåææäœãå®è¡ããå¿
èŠããããŸãã èšäºã®ãªã¹ãã®ãã¹ããªã¯ãšã¹ããäœæããã¡ã€ã³ããŒãžã«è¡šç€ºãããšæããŸãã 1ã€ã¯ããªã¯ãšã¹ãã®ãããã·ããã¹ãããããšã§ãã
ããã«ã¯æ¬¡ã®ãã®ãå¿
èŠã§ãã
APIãµãŒãã¹./config/api.json { "backendURL": "https://conduit.productionready.io", "timeout": 3000, "https": true, "baseURL": "http://localhost:8080/api", "maxContentLength": 10000, "maxRedirects": 5, "withCredentials": true, "responseType": "json" }
./src/services/api.js const axios = require('axios'); const config = require('../../config/api.json'); const source = axios.CancelToken.source(); const api = axios.create({ baseURL: config.baseURL, timeout: config.timeout, maxRedirects: config.maxRedirects, withCredentials: config.withCredentials, responseType: config.responseType, cancelToken: source.token }); const resolve = res => JSON.parse(JSON.stringify(res.data).replace(/( |<([^>]+)>)/ig, '')); const reject = err => { throw (err.response && err.response.data && err.response.data.errors) || {message: [err.message]}; }; const auth = { current: () => api.get(`/user`).then(resolve).catch(reject), logout: () => api.delete(`/users/logout`).then(resolve).catch(reject), login: (email, password) => api.post(`/users/login`, { user: { email, password } }).then(resolve).catch(reject), register: (username, email, password) => api.post(`/users`, { user: { username, email, password } }).then(resolve).catch(reject), save: user => api.put(`/user`, { user }).then(resolve).catch(reject) }; const tags = { fetchAll: () => api.get('/tags').then(resolve).catch(reject) }; const articles = { fetchAll: (type, params) => api.get(`/articles/${type || ''}`, { params }).then(resolve).catch(reject), fetch: slug => api.get(`/articles/${slug}`).then(resolve).catch(reject), create: article => api.post(`/articles`, { article }).then(resolve).catch(reject), update: article => api.put(`/articles/${article.slug}`, { article }).then(resolve).catch(reject), delete: slug => api.delete(`/articles/${slug}`).catch(reject) }; const comments = { fetchAll: slug => api.get(`/articles/${slug}/comments`).then(resolve).catch(reject), create: (slug, comment) => api.post(`/articles/${slug}/comments`, { comment }).then(resolve).catch(reject), delete: (slug, commentId) => api.delete(`/articles/${slug}/comments/${commentId}`).catch(reject) }; const favorites = { add: slug => api.post(`/articles/${slug}/favorite`).then(resolve).catch(reject), remove: slug => api.delete(`/articles/${slug}/favorite`).then(resolve).catch(reject) }; const profiles = { fetch: username => api.get(`/profiles/${username}`).then(resolve).catch(reject), follow: username => api.post(`/profiles/${username}/follow`).then(resolve).catch(reject), unfollow: username => api.delete(`/profiles/${username}/follow`).then(resolve).catch(reject), }; const cancel = msg => source.cancel(msg); const request = api.request; module.exports = { auth, tags, articles, comments, favorites, profiles, cancel, request };
ãã®ãµãŒãã¹ã¯ãæ°ããAxiosã€ã³ã¹ã¿ã³ã¹ãäœæããŠæ§æãã仿§ã«åºã¥ããŠRealWorldããã¯ãšã³ãAPIãšå¯Ÿè©±ããããã®ã€ã³ã¿ãŒãã§ã€ã¹ããšã¯ã¹ããŒãããã ãã§ãã APIãšã©ãŒãåºåããããã®éšåç./src/templates/partials/errors.html <ul class="error-messages"> {{#errors}} {{#each this as err}} <li>{{ @key }} {{ err }}</li> {{/each}} {{/errors}} </ul>
partial , API .
æ¥ä»æžåŒãã«ããŒ./src/helpers/formatDate.js const options = { year: 'numeric', month: 'long', day: 'numeric' }; const formatter = new Intl.DateTimeFormat('en-us', options); module.exports = function (val) { return formatter.format(new Date(val)); };
ãã®ãã¹ãŠãã°ããŒãã«ã«ç»é²ããŸãïŒ./src /app.js Ractive.defaults.data.formatDate = require('./helpers/formatDate'); Ractive.defaults.data.errors = null; Ractive.partials.errors = require('./templates/parsed/errors');
å®å
šãªã³ãŒã./src/app.js const Ractive = require('ractive'); Ractive.DEBUG = (process.env.NODE_ENV === 'development'); Ractive.DEBUG_PROMISES = Ractive.DEBUG; Ractive.defaults.enhance = true; Ractive.defaults.lazy = true; Ractive.defaults.sanitize = true; Ractive.defaults.data.formatDate = require('./helpers/formatDate'); Ractive.defaults.data.errors = null; Ractive.partials.errors = require('./templates/parsed/errors'); Ractive.use(require('ractive-ready')()); Ractive.use(require('ractive-page')({ meta: require('../config/meta.json') })); const options = { el: '#app', template: require('./templates/parsed/app'), partials: { navbar: require('./templates/parsed/navbar'), footer: require('./templates/parsed/footer') }, transitions: { fade: require('ractive-transitions-fade'), }, data: { message: 'Hello world', firstName: 'Habr', lastName: 'User' }, computed: { fullName() { return this.get('firstName') + ' ' + this.get('lastName'); } } }; module.exports = () => new Ractive(options);
次ã«ããã¹ãŠã®APIãµãŒãã¹ãããã«ã€ã³ããŒãããoninitããã¯ã§èšäºã®ãªã¹ããååŸããç°¡åãªãªã¯ãšã¹ããäœæããæ³šæããŠããpromiseãããwaitãïŒLOLïŒã«è¿œå ããŸãã./src/app.js const api = require('./services/api'); const options = { ... oninit () { let articles = api.articles.fetchAll(); this.wait(articles); this.set('articles', articles); } };
å®å
šãªã³ãŒã./src/app.js const Ractive = require('ractive'); Ractive.DEBUG = (process.env.NODE_ENV === 'development'); Ractive.DEBUG_PROMISES = Ractive.DEBUG; Ractive.defaults.enhance = true; Ractive.defaults.lazy = true; Ractive.defaults.sanitize = true; Ractive.defaults.data.formatDate = require('./helpers/formatDate'); Ractive.defaults.data.errors = null; Ractive.partials.errors = require('./templates/parsed/errors'); Ractive.use(require('ractive-ready')()); Ractive.use(require('ractive-page')({ meta: require('../config/meta.json') })); const api = require('./services/api'); const options = { el: '#app', template: require('./templates/parsed/app'), partials: { navbar: require('./templates/parsed/navbar'), footer: require('./templates/parsed/footer') }, transitions: { fade: require('ractive-transitions-fade'), }, data: { message: 'Hello world', firstName: 'Habr', lastName: 'User', articles: [] }, computed: { fullName() { return this.get('firstName') + ' ' + this.get('lastName'); } }, oninit () { let articles = api.articles.fetchAll(); this.wait(articles); this.set('articles', articles); } }; module.exports = () => new Ractive(options);
ããŠãã¡ã€ã³ã®èšäºã®ãªã¹ãã衚瀺ããŸãïŒãããŸã§ã®ãšããããã¹ãŠã¯çŸãããªãããã¹ãã®ããã«ããŒãã«ãããŸãïŒïŒ./src / templates /app.html {{#await articles}} <div class="alert alert-light">Loading articles...</div> {{then data}} <div class="list-group"> {{#each data.articles as article}} <div class="list-group-item list-group-item-action flex-column align-items-start"> <div class="d-flex w-100 justify-content-between"> <h5 class="mb-1">{{ article.title }}</h5> <small>{{ formatDate(article.createdAt) }}</small> </div> </div> {{else}} <div class="list-group-item">No articles are here... yet.</div> {{/each}} </div> {{catch errors}} {{>errors}} {{/await}}
å®å
šãªã³ãŒã./src/templates/app.html <div id="page"> {{>navbar}} {{#with @shared.$route as $route }} {{#if $route.match('/login')}} <div fade-in-out> <div class="alert alert-info"><strong>Login</strong>. {{message}}</div> </div> {{elseif $route.match('/register')}} <div fade-in-out> <div class="alert alert-info"><strong>Register</strong>. {{message}}</div> </div> {{elseif $route.match('/')}} <div fade-in-out> <div class="alert alert-info"> <strong>Hello, {{fullName}}!</strong> You successfully read please <a href="/login" class="alert-link">login</a>. </div> {{#await articles}} <div class="alert alert-light">Loading articles...</div> {{then data}} <div class="list-group"> {{#each data.articles as article}} <div class="list-group-item list-group-item-action flex-column align-items-start"> <div class="d-flex w-100 justify-content-between"> <h5 class="mb-1">{{ article.title }}</h5> <small>{{ formatDate(article.createdAt) }}</small> </div> </div> {{else}} <div class="list-group-item">No articles are here... yet.</div> {{/each}} </div> {{catch errors}} {{>errors}} {{/await}} </div> {{else}} <div fade-in-out> <p>404 page</p> </div> {{/if}} {{/with}} {{>footer}} </div>
ããããšãã¡ãã£ãšåŸ
ã£ãŠãããŒã¿ã«çŽæãå
¥ããŠããã³ãã¬ãŒãã§ããã解決ããŸãããïŒããŸããã¯ããããã§ããããã§ã¯ããã«ããŒ{{formatDateïŒïŒ}}ãšéšåçãª{{> errors}}ã䜿çšããŸãããããã¯è€æ°åç§ãã¡ã«åœ¹ç«ã€ã§ãããã{{#await}}ã«ã€ããŠ( ),
Ractive .
. , « ». :
this.set('foo', fetchFoo());
{{#await foo}} <p>Loading....</p> {{then val}} <p>{{ val }}</p> {{catch err}} <p>{{ err }}</p> {{/await}}
å©çïŒ
ããã§ããŠã£ã³ããŠ.__ DATA__ãªããžã§ã¯ãã«ãé
眮ãããèšäºã®ãªã¹ããšãšãã«SSRãå®è¡ãããŸãããã ããã¯ã©ã€ã¢ã³ãã³ãŒãã¯åŒãç¶ãAPIã«å¯ŸããŠ2åç®ã®ãªã¯ãšã¹ããè¡ããŸãããããã¯è¯ããããŸãããä¿®æ£ããïŒ./src /app.js const options = { ... oninit () { const key = 'articlesList'; let articles = this.get(`@global.__DATA__.${key}`); if ( ! articles ) { articles = api.articles.fetchAll(); this.wait(articles, key); } this.set('articles', articles); } };
å®å
šãªã³ãŒã./src/app.js const Ractive = require('ractive'); Ractive.DEBUG = (process.env.NODE_ENV === 'development'); Ractive.DEBUG_PROMISES = Ractive.DEBUG; Ractive.defaults.enhance = true; Ractive.defaults.lazy = true; Ractive.defaults.sanitize = true; Ractive.defaults.data.formatDate = require('./helpers/formatDate'); Ractive.defaults.data.errors = null; Ractive.partials.errors = require('./templates/parsed/errors'); Ractive.use(require('ractive-ready')()); Ractive.use(require('ractive-page')({ meta: require('../config/meta.json') })); const options = { el: '#app', template: require('./templates/parsed/app'), partials: { navbar: require('./templates/parsed/navbar'), footer: require('./templates/parsed/footer') }, transitions: { fade: require('ractive-transitions-fade'), }, data: { message: 'Hello world', firstName: 'Habr', lastName: 'User', articles: [] }, computed: { fullName() { return this.get('firstName') + ' ' + this.get('lastName'); } }, oninit () { const key = 'articlesList'; let articles = this.get(`@global.__DATA__.${key}`); if ( ! articles ) { articles = api.articles.fetchAll(); this.wait(articles, key); } this.set('articles', articles); } }; module.exports = () => new Ractive(options);

ããããè€éãªããšã¯äœããããŸãããããŒã¿ïŒarticlesListïŒãååšããïŒãŸãã¯æ¢ã«ååšããïŒããŒãããã³ããŒã¿ãªããžã§ã¯ãå
ã®ãã¹ïŒãŠã£ã³ããŠ.__ DATA__ === @global .__ DATA__ïŒãæç€ºçã«å®çŸ©ããŸããããŒã¿ããªãå Žåã¯ãèŠæ±ãäœæããããŒã2çªç®ã®åŒæ°ãšããŠç€ºãããšãçŽæã«å
¥ããŸããããããã®ãªãã·ã§ã³ã§ãã³ã³ããŒãã³ãã«å€ãèšå®ããŸãã以äžã§ãã
@globalã®è峿·±ãã±ãŒã¹Ractive «feature rich».
@global â (
window ). ,
window .
â :
this.get('@global.foo.bar.baz');
, .
èŠããã«ãããŒã¿ã¯ãµãŒããŒã«ããŒããããSSRäžã«ã¬ã³ããªã³ã°ãããã¯ã©ã€ã¢ã³ãã«æ§é åããã圢ã§éãããäžèŠãªAPIãªã¯ãšã¹ããããŒã¯ã¢ãããã€ãã¬ãŒã·ã§ã³ãªãã§èå¥ãããåå©çšãããŸãããããã£ãïŒãšãããŒã°
ãã®ãã¥ãŒããªã¢ã«ã®3ã€ã®éšåãèŠçŽãããšãååã¢ããªã±ãŒã·ã§ã³ã®ããªãåçŽã§ç°¡æœãªåºç€ãäœæã§ããããšã«æ³šæã§ããŸãããã®ã³ãŒããæ¬åœã«ã¢ããªã±ãŒã·ã§ã³ã³ãŒãã§ã¯ãªããã©ã®ãããžã§ã¯ãã§ã䜿çšã§ããããšã確èªã§ããããã«ããã®ãã¬ãŒã ã¯ãŒã¯ãåå¥ã®ãªããžããªã«åé¢ããããšã«ããŸãããçŸåšã®ãããžã§ã¯ãã®çµæã¯ãã¡ãïŒâ
ãªããžããªâ ãã¢æ¬¡ã®ããŒãã§ã¯ãã€ãã«RealWorldã¢ããªã±ãŒã·ã§ã³ã®äœæãéå§ããŸãïŒã¢ããªã±ãŒã·ã§ã³ãã³ã³ããŒãã³ãã«åå²ãããããã®ããã€ããå®è£
ããããšããå§ããŸãããããŸããã³ã³ããŒãã³ãã®çš®é¡ããããã®éããããã³ãããããã€äœ¿çšãããã«ã€ããŠç°¡åã«èª¬æããäºå®ã§ããåãæ¿ããªãã§ãã ããïŒUPDïŒ SSRããã³Progressive Enhancementã䜿çšããå圢RealWorldã¢ããªã±ãŒã·ã§ã³ã®éçºãããŒã4-ã³ã³ããŒãã³ããšæ§æ