SSRとプログレッシブエンハンスメントを備えた同形RealWorldアプリケーションの開発。 パート4-コンポーネントと構成

チュートリアルの前の部分で、同形ルーティング、ナビゲーション、フェッチ、およびデータの初期状態の問題を解決しました。 その結果、同型アプリケーションのかなり単純で簡潔な基盤であることが判明しました。これは、別のリポジトリractive-isomorphic-starterkitにも割り当てました。 このパートでは、 RealWorldアプリケーションの作成を開始しますが、最初にそれを分解します。 行こう!

画像



従来のオフトピック



興味深い結果は、 状態ベースのルーティングに関するアンケートで示されました。 このアイデアに投票した人の半数は、期待されたものに感謝しませんでした。 それでも、アイデアの半分はいまだに興味がありました。



興味深い事実は、最近、フロントエンドでYa。Subbotnikを訪れたことです。 Yandexのメンバーは、 状態ベースのルーティングのアイデアを味わったようです。 このトピックに触れたスピーカーは、アフィリエイトインターフェースのチームからのものであり、明らかに次のように見えます。



<Match strict state={{page}} params={{status: 'NEW', query: 'Yandex'}} > <App>...</App> </Match> <Switch> ... </Switch>
      
      





実際、これは原則として私がやることと同じです:



 {{#if $route.match(page) && status === 'NEW' && query === 'Yandex'}} <App>...</App> {{/if}}
      
      





豚のコンポーネントを作成する以外に、他の表現手段を持たないReactを使用するという事実がない限り、これがそのスタイルです。



レポートの終わりに私は特にこの部分に再び焦点を合わせました。 それから、スピーカーは彼のプロジェクトの詳細に深く没頭し、多くの人が彼のアイデアを完全に理解できなかったように思えました。



分解と合成





非常に簡単に言えば、フロントエンドのコンテキストでは、分解はモノリシックアプリケーションをコンポーネントに分割するプロセスであり、それぞれが問題の一部を解決します。 分解の主な目的は、経験的な複雑さを軽減し、 DRY原則を実装することです。



これは、最新のフロントエンドを支配するコンポーネントアプローチであり、全体としてモジュラーアプローチの一部です。 つまり、コードをモジュールに分割し、インターフェイスをコンポーネントに分割します。 繰り返しますが、フロントエンドのコンテキストでは、コンポーネントはUI、ビジネスロジックなどの一部をカプセル化する特定のユーザー要素です。 コンポーネントを理解する上で重要なコンポーネントは、分解が発生する原理です。



分解には多くのアプローチがあります。 たとえば、 Reactは「すべてがコンポーネントである」という原則を使用しており、他のフレームワークはSOLIDなどのコンテキストでコンポーネントを考慮しています。 しかし、一般に、それはすべて、「 高凝集疎結合 」と呼ばれるコンポーネントを作成したいという願望に帰着します。



彼らは皆これを理解しているように見えるという事実にもかかわらず、この理解は非常に頻繁に異なる方法で実装されています。 2人の開発者を連れて行く場合、高い確率で、彼らはさまざまな方法でアプリケーションを分解します。 私自身が固守する分解の原則のみを説明します。



率直に言って、コンポーネントを「粉砕」するのは正しいとは思わず、 Reactコンポーネントを粉砕する原理は私にはあまりていません。 原則として、次のルールに従ってコンポーネントを選択します。



  1. 再利用 -アプリケーションの異なる部分で同じ機能を使用します。
  2. 機能的な目的 -明確に定義された機能と、別個のライフサイクルと条件を持つビジネスプロセス
  3. 構造的機能 -構造を改善し、コードの可読性を高める


アプリケーションの一部を別のコンポーネントに割り当てる他の理由はないと思われます。 さらに、逆に過度の分解は、プロジェクトとそのサポートの理解を複雑にする可能性があります。 言い換えれば、この問題ではバランスが非常に重要です。



Reactについての完全な真実(気弱な人向けではない)
私はカルマのエントリーで嫌いをつかもうと思いますが、それでも私は純粋に主観的な意見を表明します (!) Reactのいくつかの側面について。 むしろ、 Reactが説得するアプローチとその信者が説教します。



私はJSXについて、そしてそれがどれほどひどいのかについては泣きません。これについてはすでに何千回も言われています。 そして、コードとマークアップを混合するという考えは、reactに属しているのではなく、むしろPHPに属しているので、そこからはreactに移行しました。 部分的に。



ここでは、 Reactが分解する原理と、 Reduxやその他のFluxのようなものに対する直接的な影響について説明します。 驚くかもしれませんが、これが、反応がもたらした「革命的な」アイデアのすべての理由であると断言します。 同時に、プログラミングの原則を何十年にもわたって開発してきたベストプラクティスをすべて、その道を切り開きます。



「さて、塩は何ですか? なんという塩、1 .... 反応 »



よくあることですが、 すべては素晴らしいアイデアから始まったと思います。 「すべてがコンポーネントです」 。 このアイデアは非常にシンプルで理解しやすいため、人々の心を捉えることができます。 この単純さに対する過度の熱意は、実際、 Reactが他に何もできないという事実につながりました。 コンポーネントとその構成を作成する以外の表現手段はありません(ここでは、 仮想DOMやその他のエンジンフードは、アーキテクチャの観点からはそれほど重要ではないため、ここでは特に考慮しません)。



他の最初のシンプルで理想的なアイデアと同様に、反応のアイデアにも現実がありました。 しかし、 Reactの作成者はこのアイデアに非常に熱心で、フレームワーク内ではなく(他の人がそうであるように)過度の複雑さに耐え始めましたが、アプリケーション内に残し、頭脳の見かけのシンプルさを保ちます。 また、 Reactは常に同じように答えます。アプリケーション内のすべてがコンポーネントであるため、コンポーネントを作成するだけです。



コミュニティがこれらの原則に順応し、さらにはそれらを愛していることは明らかです。 それでも、それはそのアプリケーションを過度に複雑にし始めましたが、フレームワークではなく衛星ライブラリーでした。 そのため、原則として、 Reactのみでアプリケーションを作成することはありません。 彼の背後には、あらゆる種類のアドオンと松葉杖の列車が確実に伸びています。



それとは別に、 Angularのような「オールインワン」ソリューションの大ファンでもないことに注意してください。 フレームワークの特権は、アプリケーションのアーキテクチャの問題、分解と構成の問題、コンポーネント間の通信に関連しているすべてだと思います。 しかし、http-requestなどの送信に関する質問はありません。 私にとって、 Angularは多すぎ、 Reactは少なすぎます。



しかし、分解と「別のコンポーネントを作成する」という原則に戻ります。 その結果、これらすべての素晴らしい、本質的に、アイデアは2つの主なものにつながりました。



  1. コンポーネントを作成することで問題が解決されるため、コンポーネントがたくさんありますが、それらは小さいので、マークアップとコードを混ぜても下品に見えません。
  2. アプリケーションのコンポーネントへの強力な断片化により、コンポーネントの構成が不必要に複雑になり、異なるレベルのコンポーネント間で通信することが難しくなります。 特に「一方向のデータフロー」の原則と組み合わせて。 これは、最も明らかな解決策がグローバルな状態を介したコミュニケーションであるという事実につながります。


したがって、 「すべてがコンポーネントであるという原則のman順な遵守と、 Reactが最初に制御されない分解を引き起こし、次にコンポーネントの構成を複雑にし、その後草さえ成長しなかったのは、他のツールの欠如です。 コード分​​離とマークアップの一般に受け入れられている原則を無視し、すべてがヒープ内にあるとき、それがクールであると信者に確信させることができます。 グローバル状態の使用に進むことができますが、長年にわたってこれらの状態を分離してカプセル化しようとしました。 要するに、基盤を揺るぎないものに保つために、あらゆる種類の狂気をしてください。



あなたが私の意見に同意しない場合、または何かで私を修正したい場合-コメントを歓迎します。 一般に、活気のある議論は、カルマの無言のvy騒よりもはるかに生産的なものだと思います。 前もって感謝します。 私自身も、誰かを怒らせたり怒らせたりしたくありませんでした。



まず、メインページとユーザープロファイルページを分解してみましょう。 まずそれらを実現したいです。



メインページ







ここで、対応するコンポーネントを色付きのフレームで強調表示しました。





また、メインページには、スクリーンショットに収まらない記事のリスト用のページネーションコンポーネントが埋め込まれています。



タグコンポーネントは明らかに再利用可能であることに注意してください。 同様に、お気に入りに追加するコンポーネント。



ユーザープロファイル







記事、タグ、お気に入りのリストのコンポーネントもあります。 新しいここから:





ページネーションコンポーネントもスクリーンショットに収まりませんでしたが、ユーザーの記事のリストが長くなる可能性があるため、ここで検討する価値があります。 記事リストコンポーネントも再利用可能であることが明らかになります。



このようなコンポーネントへの分離はバランスが取れており、分解の目標を達成するのに最低限必要であると思います。 同時に、コンポーネントの構成は非常にシンプルで管理しやすいままです。



コンポーネントの種類







私には、3つの主要なタイプのコンポーネントがあるように思えます:



  1. 純粋なコンポーネントは単純なコンポーネントであり、その結果は入力パラメーターに完全に依存します(「純粋な」関数のタイプによる)。 完全に再利用され、他のコンポーネントとの合成でうまく機能します。
  2. 自律コンポーネントは、ある種の分離された機能を実装し、 SOLIDの原則を実装する複雑なコンポーネントです。 原則として、このようなコンポーネントは、「純粋な」コンポーネントとの組み合わせで使用され、特定のビジネスロジックを実装し、データを収集します。
  3. ラップコンポーネントは、テンプレートの構造の改善、パラメータの受け渡しなどに最もよく使用される分離されたコンポーネントではありません。


私が何度も言ったように、現実の世界ではすべてがそれほど明確ではないため、コンポーネントにはしばしば特徴が混在しており、これは正常です。



コードを書く



ルートコンポーネント



ルートコンポーネントまたはアプリケーションコンポーネントは、. /src/app.jsで設定および作成したRactiveインスタンスです。 また、一般的なアプリケーションレイアウト(レイアウト)を実装し、画面上に常に存在する要素(ヘッダーとフッター)と、ルーティングを含むアプリケーション全体のレイアウトを含みます。



テンプレートの構造を改善し、一般的なレイアウトをより小さな部分に分割するために、前のセクションで説明したラッパーコンポーネントを使用できます。 Ractiveでは 、単純なプロパティを設定することにより、コンポーネントを非分離にできます。



 { isolated: false }
      
      





ただし、コンポーネント自体は「安価」ではありません。これらのすべてのリアクティブおよび計算されたプロパティ、オブザーバー、ライフサイクルなどが含まれているためです。 実際、 Ractiveコンポーネントは組み込み状態を持ち、何らかの機能を実装するクラスです。 ラッパーがこれをすべての形式で必要とせず、テンプレートを単純化するために設計された構造要素にすぎない場合、別の組み込み分解メカニズムであるpartialを使用するのは非常に「安く」なります。



前の記事のパーシャルで帽子と地下室をすでに取り出しました。 同様に、「すべてがコンポーネントではない」ため、コンポーネントの特性を満たさないレイアウトの他の部分を実装します。 ;-)



したがって、この段階では、ルートアプリケーションテンプレートは次のようになります。



./src/templates/app.html



 {{>navbar}} {{#with @shared.$route as $route, {delay: 500} as fadeIn, {duration: 200} as fadeOut }} <div class="page"> {{#if $route.match('/login') }} <div class="auth-page" fade-in="fadeIn" fade-out="fadeOut"> Login page </div> {{elseif $route.match('/register') }} <div class="auth-page" fade-in="fadeIn" fade-out="fadeOut"> Register page </div> {{elseif $route.match('/profile/:username/:section?') }} <div class="profile-page" fade-in="fadeIn" fade-out="fadeOut"> Profile page </div> {{elseif $route.match('/') }} <div class="home-page" fade-in="fadeIn" fade-out="fadeOut"> {{>homepage}} </div> {{else}} <div class="notfound-page" fade-in="fadeIn" fade-out="fadeOut"> {{>notfound}} </div> {{/if}} </div> {{/with}} {{>footer}}
      
      





以下、 RealWorldプロジェクトのルーティングガイドラインを使用します 。 ガイドに特定の推奨事項が含まれていない場所では、正しいと思われるアプローチを使用します。 私たちは同形のアプリケーションを書いているので、ハッシュルーティングの代わりにHistory APIルーティングも使用します。ご存知のように、URLフラグメントはサーバーに移動しません。



さらに、メインページのレイアウト用と404ページ用の2​​つのパーシャルを強調しました。



./src/templates/partials/homepage.html



 <div class="banner"> <div class="container"> <h1 class="logo-font">conduit</h1> <p>A place to share your knowledge.</p> </div> </div> <div class="container page"> <div class="row"> <div class="col-md-9"> <div class="feed-toggle"> <ul class="nav nav-pills outline-active"> <li class="nav-item"> <a href="/" class-active="$route.pathname === '/'" class="nav-link"> Global Feed </a> </li> </ul> </div> Articles list </div> <div class="col-md-3"> <div class="sidebar"> <p>Popular Tags</p> Tags list </div> </div> </div> </div>
      
      





./src/templates/partials/notfound.html



 <div class="banner"> <div class="container"> <h1 class="logo-font">conduit</h1> <p>404 - Not found</p> </div> </div>
      
      





次に、ルートコンポーネントの設定にそれらを登録する必要があります。



./src/app.js



  partials: { ... homepage: require('./templates/parsed/homepage'), notfound: require('./templates/parsed/notfound') },
      
      





完全なコード./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'), homepage: require('./templates/parsed/homepage'), notfound: require('./templates/parsed/notfound') }, transitions: { fade: require('ractive-transitions-fade'), } }; module.exports = () => new Ractive(options);
      
      







また、トランジションアニメーションの設定も少し試しました。



ページネーションコンポーネント



このコンポーネントは、純粋なコンポーネントの明るい代表です。 彼の仕事の結果は、入力パラメータ-コンポーネント属性に完全に基づいています。







視覚的には、このコンポーネントは完全に標準に見えますが、どの副作用を生成し、この副作用をどの程度正確に制御するかを決定することが重要です。 同型のアプリケーションのコンテキストでは、漸進的な改善により、この質問に対する答えは明白です-URLを変更します。



JSが無効になっていても、ページ間を移動できる必要があることを常に覚えておく必要があります。 つまり、各ページは独自のURL(リンク)で表される必要があります。 さらに、ブラウザでページをリロードするとき、選択したリストページにとどまる必要があります( SSRの完全サポート)。



ガイドラインには、ページネーションがURLにどのように反映されるべきか、それがまったくないかどうかに関する推奨事項がないため、リストにオフセットを含むURL Queryパラメーターoffsetを使用します 。 なぜページではなくオフセットですか? これはAPIでページネーションが機能する方法なので、これは簡単です。



 ?offset=20
      
      





さて、最初のRactiveコンポーネントを作成します。 これを行うために、 Ractiveコンストラクターは静的なextend()メソッドを提供します。これにより、コンストラクターを新しいプロパティで拡張し、既存のプロパティを上書きして、結果として新しいコンストラクターを取得できます。 簡単に言えば、これは継承です。



./src/components/Pagination.js



 const Ractive = require('ractive'); module.exports = Ractive.extend({ template: require('../templates/parsed/pagination'), attributes: { required: ['total'], optional: ['offset', 'limit'] }, data: () => ({ total: 0, limit: 10, offset: 0, isCurrent(page) { let limit = parseInt(this.get('limit')), offset = parseInt(this.get('offset')); return offset === ((page * limit) - limit); }, getOffset(page) { return (page - 1) * parseInt(this.get('limit')); } }), computed: { pages() { let length = Math.ceil(parseInt(this.get('total')) / parseInt(this.get('limit'))); return Array.apply(null, { length }).map((p, i) => ++i);; } } });
      
      





このコンポーネントは、属性total (リスト内の要素の総数)、 limit (ページ上の要素の数)、およびoffset (リスト内の現在のオフセット)を受け入れます。 これらのプロパティに基づいて、コンポーネントはページのリストを生成します。これは、 ページの計算されたプロパティとして実装されます 。 さらに、依存プロパティのいずれかが時間とともに変化すると、計算されたプロパティが自動的に再計算されます。 便利に。



./src/templates/pagination.html



 {{#if total > 0 && pages.length > 1}} <nav> <ul class="pagination"> {{#each pages as page}} <li class-active="isCurrent(page)" class="page-item"> <a href="?{{ @shared.$route.join('offset', getOffset(page)) }}" class="page-link"> {{ page }} </a> </li> {{/each}} </ul> </nav> {{/if}}
      
      





テンプレートでは、このリストをリンクとして表示するだけです。 ルーターでの特別なjoin()メソッドの使用に注意してください。 このメソッドは、渡されたパラメーターとその値を現在のクエリURLとマージします。その結果、既存のクエリ文字列を取得し、そこに存在するパラメーターを考慮します。 いつものように、ルーター自体がリンク処理に関するすべての作業を引き受けるため、心配する必要はありません。



結果はかなり小さくシンプルなコンポーネントであり、その唯一の副作用はURLパラメータの変更です。 これにより、任意のリストを含むコンポジションでこのコンポーネントを使用できます。 リストを実装するコンポーネントは、対応するURLパラメーターの変更をサブスクライブし、この値をAPIリクエストとデータ出力に使用します。



コンポーネントタグ



このコンポーネントも純粋です。 ただし、 ページネーションとは異なり、これには異なる前提があります。







この写真は、 Tagsコンポーネントがアプリケーションの多くの場所で実際に使用されていることを示しています。 このコンポーネントが特定のタグのリストで機能することも明らかです。 しかし、最も重要なことは、タグのリストがコンポーネントが実行されるコンテキストに依存することがすぐに明らかになることです。 メインページ-これは記事のリスト内の人気のあるタグのリストです-これらは特定の記事のタグなどです。 そのため、このコンポーネントは単純に自律的ではなく、使用されているコンテキストから記事のリストを転送する必要があります。



./src/components/Tags.js



 const Ractive = require('ractive'); module.exports = Ractive.extend({ template: require('../templates/parsed/tags'), attributes: { required: ['tags'], optional: ['skin'] }, data: () => ({ tags: [], skin: 'outline' }) });
      
      





./src/templates/tags.html



 {{#await tags}} <p>Loading...</p> {{then tags}} <ul class="tag-list"> {{#each tags as tag}} <li> <a href="/?tag={{ tag }}" class="tag-pill tag-default tag-{{~/skin}}"> {{ tag }} </a> </li> {{/each}} </ul> {{catch errors}} {{>errors}} {{else}} <p>No tags</p> {{/await}}
      
      





このコンポーネントはさらにシンプルです。 タグタグのリストと追加のパラメータスキン -スタイルタグ( outlineおよびfill)を受け入れます。



タグは、タグのリストを配列またはプロミスとして受け入れ、独立してそれをタグリストに解決できます。 Paginationと同じ副作用を生成します-クエリパラメータタグを変更します(ここでも、ガイドラインに推奨事項はありません)。 依存関係はなく、アプリケーションのどこでも使用できます。



副作用について
ページネーションコンポーネントとは異なり、このコンポーネントは変数パラメータをクエリ文字列の残りの部分にマージせず、完全に更新することに特に注意する価値があります。 実際には、タグをクリックすると、ユーザーはこのタグに一致する更新された記事のリストを見ることができます。 したがって、ページナビゲーションなど、前のリストで可能な操作はゼロにリセットする必要があります。 この場合、ページ編集コンポーネントは既存のクエリ文字列に対するパラメーターを維持するため、 ページ編集はタグの改良と連携して機能します。



メインページでこのコンポーネントを使用してみましょう。 まず、APIから人気のあるタグのリストを取得する必要があります。 このため、APIで動作するサービスには既に既製の呼び出しがあり、この要求を実装するコードを記述するだけです。また前の記事で説明したSSRおよび他の同形の部分のサポートを覚えておくことも非常に重要です。これが興味深いポイントです。ほとんどの場合、注意を払って計算されたコンポーネントのプロパティの助けを借りて、このようなデータ取得要求を実装します!







画像








なんで?ArticlesおよびProfileコンポーネントの例でこれを理解できると確信しています一言で言えば-それはナッツに行くのは素晴らしいです!



そのため、値を返すだけの単純な関数を作成しています:./src / computed /



tags.js



 const api = require('../services/api'); module.exports = function() { const key = 'tagsList', keychain = `${this.snapshot}${this.keychain()}.${key}`; let tags = this.get(keychain); if ( ! tags) { tags = api.tags.fetchAll().then(data => data.tags); this.wait(tags, key); } return tags; };
      
      





ご覧のとおり、複雑なことは何もありません。計算されたプロパティの関数は、接続先のコンポーネントのコンテキストで実行されます。ここで起こるすべては前の部分ですでに説明されましたさらに、最終的にractive-readyプラグインkeychain()メソッドを使用しましたこのメソッドは、この計算されたプロパティを接続したコンポーネントのネストレベルに応じて、データオブジェクト内の正しいパスを単に返します。次に、このプロパティをRootコンポーネントに接続し、Tagsコンポーネント接続して、このプロパティを属性として渡します。./src/app.js











 ... Ractive.defaults.snapshot = '@global.__DATA__'; ... components: { tags: require('./components/Tags'), }, computed: { tags: require('./computed/tags') }, ...
      
      





完全なコード./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.snapshot = '@global.__DATA__'; 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'), homepage: require('./templates/parsed/homepage'), notfound: require('./templates/parsed/notfound') }, transitions: { fade: require('ractive-transitions-fade'), }, components: { tags: require('./components/Tags'), }, computed: { tags: require('./computed/tags') } }; module.exports = () => new Ractive(options);
      
      







./src/templates/partials/homepage.html



 ... <div class="sidebar"> <p>Popular Tags</p> <tags tags="{{ tags }}" skin="filled" /> </div> ...
      
      





完全なコード./src/templates/partials/homepage.html
 <div class="banner"> <div class="container"> <h1 class="logo-font">conduit</h1> <p>A place to share your knowledge.</p> </div> </div> <div class="container page"> <div class="row"> <div class="col-md-9"> <div class="feed-toggle"> <ul class="nav nav-pills outline-active"> <li class="nav-item"> <a href="/" class-active="$route.pathname === '/'" class="nav-link"> Global Feed </a> </li> </ul> </div> Articles list </div> <div class="col-md-3"> <div class="sidebar"> <p>Popular Tags</p> <tags tags="{{ tags }}" skin="filled" /> </div> </div> </div> </div>
      
      







それがすべてであり、最も重要なことは、結果が目に心地よいことです。









タグはサーバーにロードされ、TagsコンポーネントはSSRの間にレンダリングされます。タグのリストには初期データ状態が含まれるため、コンポーネントはAPIへの2番目のリクエストなしでクライアント上で正常に水和されます。シャイン!



コンポーネント記事



スタンドアロンコンポーネントとクリーンコンポーネントの主な違いは、スタンドアロンコンポーネントが機能するためには、親コンポーネントにプラグインし、適切なタグをマークアップに追加するだけです。必要に応じて、属性を介していくつかの設定を渡すことができます。そのようなコンポーネントとそれに関連するすべての機能を削除または無効にする必要がある場合も同じことが機能します。テンプレートでの使用を停止するか、親コンポーネントから削除するだけです。



アプリケーションのこのようなコンポーネントの1つはArticlesコンポーネントです。このコンポーネントは、記事のリストのまったく別の機能を実装し、TagsPaginationなどの他のコンポーネントを内部的に使用します







コンポーネント記事 アプリケーションの少なくとも2ページ(メインおよびプロファイル)で使用され、転送されるパラメーターに応じて5種類の記事リストを表示できます。



  1. 記事の一般的なリスト
  2. 現在のユーザーのサブスクリプションに基づく記事の個人的なリスト
  3. 何らかのタグにフィルターされた記事のリスト
  4. 任意のユーザーが作成した記事のリスト
  5. 任意のユーザーがお気に入りに追加した記事のリスト


わあ!さらに、これらのリストタイプはすべて、ページネーションをサポートし、同形であり、JSなしで機能する必要があります。



実際、このような状況では、スタンドアロンコンポーネントが優れたソリューションです。必要なすべてのマークアップとロジックをコンポーネント内にカプセル化して、必要なインターフェイスのみを公開できます。これにより、アプリケーションのさまざまな部分でコンポーネントを使用する際の危険な副作用がなくなります。



データの取得から始めましょう:./src / computed /



articles.js



 const api = require('../services/api'); module.exports = function() { const type = this.get('type'), params = this.get('params'); const key = 'articlesList', keychain = `${this.snapshot}${this.keychain()}.${key}`; let articles = this.get(keychain); if (articles) { this.set(keychain, null); } else { articles = api.articles.fetchAll(type, params); this.wait(articles, key); } return articles; };
      
      





ご覧のとおり、計算プロパティ用の関数を再度作成しましたが、タグの場合と同じです。ネタバレの下で計算されたプロパティの利点について自発的にお読みください。



アプローチの利点
, , . ?



-, , :

 //     life cycle    - oninit () { const foo = fetch('/foo').then(res => res.json()); this.set('foo', foo); } //   ,     computed: { bar() { return fetch('/bar').then(res => res.json()); } }
      
      





-, «» :



 <!--    ,     --> {{#if baz}} {{foo}} {{/if}} <!--   ,    --> {{#if baz}} {{bar}} {{/if}}
      
      





-, :



 //  , -     oninit () { this.observe('qux', (val) => { const foo = fetch(`/foo?qux=${val}`).then(res => res.json()); this.set('foo', foo); }); } //       computed: { bar() { const qux = this.get('qux'); return fetch(`/bar?qux=${qux}`).then(res => res.json()); } }
      
      





-, ( ) . . :



 computed: { foo: require('./computed/baz'), bar: require('./computed/baz'), }
      
      





, , , . , 10 - Articles … ?



次に、コンポーネント自体について説明します:./src/ components/



Articles.js



 const Ractive = require('ractive'); module.exports = Ractive.extend({ template: require('../templates/parsed/articles'), components: { pagination: require('./Pagination'), tags: require('./Tags'), }, computed: { articles: require('../computed/articles') }, attributes: { optional: ['type', 'params'] }, data: () => ({ type: '', params: null }) });
      
      





ここで、ネストされたコンポーネント、計算されたプロパティを接続し、コンポーネントインターフェイスを決定しました。オプションの2つの属性のみを受け入れます:type(リストタイプ、空の文字列または 'feed'のいずれか)とparams(フィルターパラメーターを持つオブジェクト)。実際にはコンポーネントが小さくないため、テンプレートは少し複雑になりました:./src / templates /



articles.html



 <div class="articles-list"> {{#await articles}} <div class="article-preview"> <p>Loading articles...</p> </div> {{then data}} {{#each data.articles as article}} <div class="article-preview"> <div class="article-meta"> <a href="/profile/{{ article.author.username }}"> <img src="{{ article.author.image }}" /> </a> <div class="info"> <a href="/profile/{{ article.author.username }}" class="author"> {{ article.author.username }} </a> <span class="date">{{ formatDate(article.createdAt) }}</span> </div> </div> <a href="/article/{{ article.slug }}" class="preview-link"> <h1>{{ article.title }}</h1> <p>{{ article.description }}</p> <span>Read more...</span> <tags tags="{{ article.tagList }}"/> </a> </div> {{else}} <div class="article-preview"> <p>No articles are here... yet.</p> </div> {{/each}} <pagination total="{{ data.articlesCount }}" offset="{{ @shared.$route.query.offset || 0 }}" limit="20" /> {{catch errors}} <div class="article-preview"> {{>errors}} </div> {{else}} <div class="article-preview"> <p>No articles are here... yet.</p> </div> {{/await}} </div>
      
      





さて、メインページでそれを吐きましょう。



./src/app.js



  components: { ... articles: require('./components/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.snapshot = '@global.__DATA__'; 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'), homepage: require('./templates/parsed/homepage'), notfound: require('./templates/parsed/notfound') }, transitions: { fade: require('ractive-transitions-fade'), }, components: { tags: require('./components/Tags'), articles: require('./components/Articles'), }, computed: { tags: require('./computed/tags') } }; module.exports = () => new Ractive(options);
      
      







./src/templates/partials/homepage.html



 ... <li class="nav-item"> <a href="/" class-active="$route.pathname === '/' && ! $route.query.tag" class="nav-link"> Global Feed </a> </li> {{#if $route.query.tag }} <li class="nav-item"> <a class="nav-link active"> # {{ $route.query.tag }} </a> </li> {{/if}} ... <articles params="{{ $route.query }}"/> ...
      
      





完全なコード./src/templates/partials/homepage.html
 <div class="banner"> <div class="container"> <h1 class="logo-font">conduit</h1> <p>A place to share your knowledge.</p> </div> </div> <div class="container page"> <div class="row"> <div class="col-md-9"> <div class="feed-toggle"> <ul class="nav nav-pills outline-active"> <li class="nav-item"> <a href="/" class-active="$route.pathname === '/' && ! $route.query.tag" class="nav-link"> Global Feed </a> </li> {{#if $route.query.tag }} <li class="nav-item"> <a class="nav-link active"> # {{ $route.query.tag }} </a> </li> {{/if}} </ul> </div> <articles params="{{ $route.query }}"/> </div> <div class="col-md-3"> <div class="sidebar"> <p>Popular Tags</p> <tags tags="{{ tags }}" skin="filled" /> </div> </div> </div> </div>
      
      







フィルタリングが実装されるタグの名前を持つタブを追加する実装がどのように実装されるかに注目してください。ユーザーがタグコンポーネントからタグをクリックすると(記事または人気のタグのリストからは関係ありません)、記事のリストはこのタグによってフィルターされるだけでなく、視覚的に強調するためにタグ名のタブも追加されます。



一般に、これは次のように機能し、私の意見では、ひどく判明しませんでした:









そしてもちろん、すべてが同型であり、最初の読み込みはクライアントでの単一のajaxリクエストなしで発生します。ブラウザの履歴は完全に機能し、JSをオフにすると、すべてが正常に機能します。要するに、さらに先に進みます。



プロファイルコンポーネント



このコンポーネントは傑出したものになるでしょうが、そうではありません。これは、Articles同じスタンドアロンコンポーネントのプラスまたはマイナスであり、プラスまたはマイナスでも機能します。実際、1ページでしか使用されないため、さらに退屈です。







実際、彼はこのページです。別の言い方を知りません。



./src/components/Profile.js



 const Ractive = require('ractive'); module.exports = Ractive.extend({ template: require('../templates/parsed/profile'), components: { articles: require('./Articles') }, computed: { profile: require('../computed/profile') }, attributes: { required: ['username'], optional: ['section'] }, data: () => ({ username: '', section: '' }) });
      
      





ただし、従来の計算プロパティはまだ少し複雑です:./src / computed /



profile.js



 const api = require('../services/api'); let _profile; module.exports = function() { const username = this.get('username'); const key = 'profileData', keychain = `${this.root.snapshot}${this.keychain()}.${key}`; let profile = this.get(keychain); if (profile) { this.set(keychain, null); _profile = profile; } else if (_profile && _profile.username === username) { profile = _profile; } else if (username) { profile = api.profiles.fetch(username).then(data => (_profile = data.profile, _profile)); this.wait(profile, key); } return profile; };
      
      





お気に入りの記事のサブルートに切り替えるとき、またはその逆に切り替えるときにユーザープロファイルを再度要求したくないので、ここでクロージャーのユーザープロファイル(_profileを「キャッシュ」しています難しくはなく、高価でもありませんが、うまく機能します。たとえば、React / Reduxの実装でこの問題は解決されないため、「My Articles」と「Favorited Articles」を切り替えるたびにプロファイルが取得されます。すぐに彼らが試みなかったことは明らかです。



ここで、テンプレートでこのエコノミーをすべて使用します:./src/ templates/



profile.html



 <div class="profile"> {{#await profile}} {{then profile}} <div class="user-info"> <div class="container"> <div class="row"> <div class="col-xs-12 col-md-10 offset-md-1"> <img src="{{ profile.image }}" class="user-img" /> <h4>{{ profile.username }}</h4> <p>{{ profile.bio }}</p> </div> </div> </div> </div> {{catch errors}} {{>errors}} {{/await}} {{#if username}} <div class="container"> <div class="row"> <div class="col-xs-12 col-md-10 offset-md-1"> <div class="articles-toggle"> <ul class="nav nav-pills outline-active"> <li class="nav-item"> <a href="/profile/{{ username }}" class-active="! section" class="nav-link"> My Articles </a> </li> <li class="nav-item"> <a href="/profile/{{ username }}/favorites" class-active="section === 'favorites'" class="nav-link"> Favorited Articles </a> </li> </ul> </div> <articles params="{{ section === 'favorites' ? {favorited: username} : {author: username} }}" /> </div> </div> </div> {{/if}} </div>
      
      





その後、すべてが通常通りです- ルートコンポーネントと対応するルートに追加します。



./src/app.js



  components: { ... profile: require('./components/Profile'), },
      
      





完全なコード./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.snapshot = '@global.__DATA__'; 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'), homepage: require('./templates/parsed/homepage'), notfound: require('./templates/parsed/notfound') }, transitions: { fade: require('ractive-transitions-fade'), }, components: { tags: require('./components/Tags'), articles: require('./components/Articles'), profile: require('./components/Profile'), }, computed: { tags: require('./computed/tags') } }; module.exports = () => new Ractive(options);
      
      







./src/templates/app.html



 ... {{elseif $route.match('/profile/:username/:section?') }} <div class="profile-page" fade-in="fadeIn" fade-out="fadeOut"> <profile username="{{ $route.params.username }}" section="{{ $route.params.section }}" /> </div> ...
      
      





完全なコード./src/templates/app.html
 {{>navbar}} {{#with @shared.$route as $route, {delay: 500} as fadeIn, {duration: 200} as fadeOut }} <div class="page"> {{#if $route.match('/login') }} <div class="auth-page" fade-in="fadeIn" fade-out="fadeOut"> Login page </div> {{elseif $route.match('/register') }} <div class="auth-page" fade-in="fadeIn" fade-out="fadeOut"> Register page </div> {{elseif $route.match('/profile/:username/:section?') }} <div class="profile-page" fade-in="fadeIn" fade-out="fadeOut"> <profile username="{{ $route.params.username }}" section="{{ $route.params.section }}" /> </div> {{elseif $route.match('/') }} <div class="home-page" fade-in="fadeIn" fade-out="fadeOut"> {{>homepage}} </div> {{else}} <div class="notfound-page" fade-in="fadeIn" fade-out="fadeOut"> {{>notfound}} </div> {{/if}} </div> {{/with}} {{>footer}}
      
      







たぶん、今日はこれで十分でしょう。現在のプロジェクトの結果はこちら:



リポジトリ

デモ



次のパートでは、承認と同型フォームを段階的に強化していきます。面白いでしょう、切り替えないでください!



All Articles