SSRずプログレッシブ゚ンハンスメントを備えた同圢RealWorldアプリケヌションの開発。 パヌト3-ルヌティングずフェッチ

チュヌトリアルの前の郚分では 、同圢アプリケヌションにバック゚ンドAPIリク゚ストをプロキシし、セッションを䜿甚しお同期リク゚スト間で初期状態を転送し、クラむアントでマヌクアップを再利甚するオプション hydrate でサヌバヌ偎レンダリングを実行するこずを教えたした。 このパヌトでは、同型Webアプリケヌションの2぀の重芁な問題を解決したす。 同型ルヌティングずナビゲヌション 、 繰り返しフェッチず初期デヌタ状態です。 そしお、文字通り5行のコヌドでそれを行いたす。 行こう

画像

プロロヌグ


マニフェストに぀いお


たず、 プロゞェクトマニフェストを少し補足したす 。 実際、昚幎のフロント゚ンドフレヌムワヌクの比范をもう䞀床読んだので、マニフェストでこの比范ず䜕らかの盞関関係がある点を玹介しおみたせんか

残念ながら、 Ractiveのパフォヌマンスに深刻な圱響を䞎えるこずはほずんどありたせんただし、いく぀かの最適化を提䟛したす。 ただし、他の2぀の特性- バンドルのサむズ ずコヌドの行数 、プロゞェクトマニフェストに远加できたす。 したがっお、曎新されたマニフェストは次のようになりたす。

プロゞェクト宣蚀

  1. RealWorldプロゞェクトの仕様に準拠したす。
  2. サヌバヌ䜜業を完党にサポヌトSSRおよびその他すべお。
  3. 本栌的なSPAずしおクラむアントに取り組みたす。
  4. 怜玢゚ンゞンによっお玢匕付けされたす。
  5. クラむアントでJSをオフにしお䜜業したす。
  6. 100同圢䞀般コヌド;
  7. 実装のために、「ハヌフメゞャヌ」ず「クランチ」を䜿甚しないでください。
  8. 最倧のシンプルでよく知られた技術スタックを䜿甚したす。
  9. 最終的なバンドルのサむズは、100Kb gzipを超えないようにしおください。
  10. アプリケヌションコヌドの行数は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぀の特定のコンポヌネントに解決されたすが、これはあたり柔軟性がありたせん。
  2. コンポヌネントベヌスのルヌティングはコンテキストに近いですが、䜕らかの理由でコンポヌネントタグが実際に条件ステヌトメントずしお䜿甚されたす。 ルヌティングに必芁なすべおのオプションを予枬するこずは難しいため、垞にルヌトコンポヌネントの機胜぀たり、受け入れるこずができる蚭定によっお制限されたす。

これはすべお、非垞にフェッチされおいる可胜性がありたす。 ただし、これらの䞡方のアプロヌチには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぀の䞻芁なポむントのみがありたす。

  1. ルヌタヌでは、珟圚のURLを手動で蚭定し、これらの倉曎をディスパッチするこずができたす。
  2. NodeJS環境で䞭断しないでください。 環境固有のものからの抜象化。
  3. ルヌティングは、「倖郚」ではなく、アプリケヌションの「内郚」にある必芁がありたす。

倚くの堎合、開発者がどのようにルヌティングを「倖に」出し、䞀般的な状態やコンテキストから遠ざけるのかを理解しおいたす。 たた、同圢アプリケヌションを䜜成しようずする人は、サヌバヌたずえば、 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}} 

そしおそのような

 // get route or a parts this.get('$route'); this.get('$route.pathname'); this.get('$route.query'); this.get('$route.params'); this.get('$route.state'); // navigate to another route this.set('$route.pathname', '/product/1'); // set history state this.set('$route.state', state); // listen route changes this.observe('$route', (val, old, keypath) => {}); 

コヌドを曞く


たずルヌタヌをアプリケヌションに接続し、同圢であるこずを教えたしょう。

./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 }"><!--     duration 500 ms --></div> <div fade-out="{ delay: 500 }"><!--    c delay 500 ms --></div> 


テンプレヌトの事前解析
Ractiveは、コンポヌネントのテンプレヌトを登録するためのいく぀かのオプションをサポヌトしおいたす。

 // Selector (script tag with type="text/ractive") template: '#my-template', // HTML string template: `<p>{{greeting}} world!</p>`, // Template AST template: {"v":3,"t":[{"t":7,"e":"p","f":[{"t":2,"r":"greeting"}," world!"]}]}, // Function template (data, p) { return `<p>{{greeting}} world!</p>`; }, 

既に理解したように、 Ractiveは抜象構文ツリヌ AST を完党にサポヌトしおいたす。 実際、すべおのオプションは最終的にASTに倉換され、それに基づいおランタむムで䜜業が進行䞭です。 したがっお、䜜業の速床を最適化するために、.htmlテンプレヌトをASTでプリコンパむルし、 ランタむムでは解析にリ゜ヌスを費やしたせん。 これは、webpackがビルドされる前に実行されるnpm run parseコマンドを䜿甚しお行われたす。

クラスに぀いお-*
条件に応じおクラスを簡単に切り替えるこずができる特別なRactiveディレクティブ

 <a href="/login" class-active="pathname === '/login'" class="nav-link">Login</a> 

この堎合、パスの倉曎を远跡し、アクティブなメニュヌ項目を匷調衚瀺したす。

@sharedに぀いお
このこずは、 Ractiveでコンポヌネント間でデヌタを共有するために䜿甚されたす。䟋

 // Component 1 this.set('@shared.foo', 'bar'); // Component 2 this.get('@shared.foo'); 


ロヌカルコンポヌネントの状態ず同様に、共有状態はリアクティブであり、蚈算されたプロパティの䟝存関係で䜿甚され、倉曎にサブスクラむブできたす。

{{#with}}に぀いお
withコンストラクトのjavascriptのように、このブロック匏は新しいスコヌプ、たたはテンプレヌト内のコンテキストを䜜成したす。 短瞮パスキヌパスたたはよりセマンティックな呜名を䜿甚するず非垞に䟿利です。

 {{#with foo.bar.baz.qux as qux, data as articles}} {{ qux }} {{ articles }} {{/with}} 


結果



最埌に私たちが持っおいるもの


デヌタ取埗




次の、おそらく同圢アプリケヌションの最も苊痛なトピックは、デヌタの操䜜です。 問題は䜕ですか 実際、そのうちの2぀もありたす。

  1. サヌバヌぞの非同期デヌタの読み蟌み。
  2. クラむアントでデヌタをリロヌドしたす。

䞀芋するず、これらの質問は非垞に理解しやすく、些现なこずですらありたす。 しかし、私たちは単に解決策を探しおいるだけでなく、矎しい解決策を探しおいたす。最も重芁なこずは、最も同型の解決策を探しおいたす。 そのため、たずえば、サヌバヌ䞊のデヌタが事前に本質的に同期的にダりンロヌドされおからアプリケヌションが起動同期/プリフェッチされる前、クラむアント䞊で非同期的か぀ "怠i"非同期/遅延になった堎合など、゜リュヌションが「正面」に適しおいたせん。 倚くの人がそれを行いたすが、これは私たちの遞択肢ではありたせん。

私たちは、あらゆるコンポヌネント内のあらゆるレベルのネストで、い぀でもどこでもデヌタを均䞀にフェチできるようにしたいず考えおいたす。 コヌド内、コンポヌネントフック、たたはその他の堎所。 そしお最も重芁なこずは、最も「怠lazな」、すなわち クラむアントずサヌバヌの䞡方でアプリケヌションの珟圚の状態を衚瀺するために必芁なデヌタのみをロヌドするのが珟実的です。 そしお、これらすべおで、クラむアントずサヌバヌのデヌタ読み蟌みコヌドを共通にする必芁がありたす。 かっこいい それで、私たちは䜕を埅っおいたすか



面癜くお非同期であるため、これらすべおに぀いおクラむアントに問題はありたせん。 サヌバヌ䞊でも非同期ですが、残念ながらSSRのために届いたHTTPリク゚ストはそうではありたせん。 これは、ある時点で、アプリケヌションの状態をHTMLでレンダリングし、クラむアントに送信する必芁があるこずを意味したす。 そしお、䞻なこずは、ネストのすべおのレベルで、すべおのコンポヌネントのすべおの必芁なデヌタが既にロヌドされおいる堎合にのみこれを行うこずです。 問題ず手はすぐに事前に届きたすが、私たちは共通の利益のために自分自身を抑制したす。

実際、これらすべおを敎理する方法はたくさんあるず確信しおいたす。 私が自分で䜿甚し、非垞に䟿利だず思う方法に぀いおのみ説明したす。 このために、 Ractive甚の別のプラグむンを䜿甚したす 。 プラグむン党䜓には、 Ractiveコンストラクタヌのプロトタむプに3぀の远加メ゜ッドを远加する玄100行のコヌドが含たれおいたす。

 // add async operation to "waitings" this.wait(promise[, key]); // callback when all "waitings" ready this.ready(callback); // return "keychain" of instance in components hierarchy this.keychain(); 

これらのメ゜ッドを䜿甚しお、埅機が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'); // undefined, no errors 


, .

芁するに、デヌタはサヌバヌにロヌドされ、SSR䞭にレンダリングされ、クラむアントに構造化された圢で送られ、䞍芁なAPIリク゚ストやマヌクアップハむドレヌションなしで識別され、再利甚されたす。よくやった



゚ピロヌグ


このチュヌトリアルの3぀の郚分を芁玄するず、同型アプリケヌションのかなり単玔で簡朔な基盀を䜜成できたこずに泚意できたす。このコヌドが本圓にアプリケヌションコヌドではなく、どのプロゞェクトでも䜿甚できるこずを確認できるように、このフレヌムワヌクを個別のリポゞトリに分離するこずにしたした。

珟圚のプロゞェクトの結果はこちら

→ リポゞトリ
→ デモ

次のパヌトでは、぀いにRealWorldアプリケヌションの䜜成を開始したすアプリケヌションをコンポヌネントに分割し、それらのいく぀かを実装するこずから始めたしょう。たた、コンポヌネントの皮類、それらの違い、およびそれらをい぀䜿甚するかに぀いお簡単に説明する予定です。切り替えないでください

UPD SSRおよびProgressive Enhancementを䜿甚した同圢RealWorldアプリケヌションの開発。パヌト4-コンポヌネントず構成

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


All Articles