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ルータープラグインを使用しています。 実際、これはPageJSqsのラッパーであり 、ルーティングに対する状態ベースのアプローチを実装しています。 この「ルーター」のネイティブコードは、強さから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-コンポーネントと構成



All Articles