非同期に初期化されたReact.jsコンポーネントのサーバー側レンダリングの戦略


114

最大の利点の一つReact.jsをすることになっているサーバー側のレンダリング。問題は、主要な機能React.renderComponentToString()が同期的であり、コンポーネント階層がサーバー上でレンダリングされるときに非同期データをロードできないことです。

コメント用の汎用コンポーネントがあり、ページのどこにでもドロップできるとしましょう。プロパティは1つだけで、なんらかの識別子(コメントが配置されている記事のIDなど)とその他すべてはコンポーネント自体(コメントの読み込み、追加、管理)によって処理されます。

Fluxアーキテクチャは、多くのことがはるかに簡単になり、そのストアがサーバーとクライアントの間で状態を共有するのに最適であるため、本当に気に入っています。コメントを含むストアを初期化したら、それをシリアル化してサーバーからクライアントに送信し、簡単に復元できます。

問題は、私のストアにデータを追加するための最良の方法は何ですか。過去数日間、私は多くのことをグググルグしていて、いくつかの戦略に出くわしましたが、Reactのこの機能がどれだけ「促進」されているかを考えると、どれも本当に良いようには見えませんでした。

  1. 私の意見では、最も簡単な方法は、実際のレンダリングが始まる前にすべてのストアにデータを取り込むことです。これは、コンポーネント階層の外側のどこかを意味します(たとえば、ルーターにフックされています)。このアプローチの問題は、ページ構造をほぼ2回定義する必要があることです。たとえば、多くの異なるコンポーネント(実際のブログ投稿、コメント、関連投稿、最新の投稿、Twitterストリーム...)を含むブログページなど、より複雑なページを考えてみます。Reactコンポーネントを使用してページ構造を設計する必要があり、それから別の場所で、この現在のページに必要な各ストアにデータを取り込むプロセスを定義する必要があります。それは私にとって良い解決策のようには思えません。残念ながら、ほとんどの同形のチュートリアルはこのように設計されています(たとえば、この素晴らしいflux-tutorial)。

  2. React-async。このアプローチは完璧です。これにより、各コンポーネントの特別な関数で状態を初期化する方法(同期か非同期かは関係ありません)を簡単に定義でき、階層がHTMLにレンダリングされるときにこれらの関数が呼び出されます。これは、状態が完全に初期化されるまでコンポーネントがレンダリングされないように機能します。問題は繊維が必要なことですこれは、私が理解している限り、標準のJavaScriptの動作を変更するNode.js拡張機能です。結果は本当に気に入っていますが、解決策を見つけるのではなく、ゲームのルールを変更したようです。そして、React.jsのこのコア機能を使用するためにこれを強制される必要はありません。このソリューションの一般的なサポートについてもわかりません。標準のNode.js Webホスティングでファイバーを使用することは可能ですか?

  3. 私は少し独りで考えていました。実装の詳細についてはあまり考えていませんが、一般的な考え方は、コンポーネントをReact-asyncと同じように拡張し、ルートコンポーネントでReact.renderComponentToString()を繰り返し呼び出すというものです。各パスの間に、拡張コールバックを収集し、パスのandでそれらを呼び出して、ストアにデータを取り込みます。現在のコンポーネント階層に必要なすべてのストアにデータが入力されるまで、この手順を繰り返します。解決しなければならないことがたくさんあり、特にパフォーマンスについてはよくわかりません。

私は何か見落としてますか?別のアプローチ/解決策はありますか?現在、react-async / fibersの方法を検討していますが、2番目のポイントで説明したように、完全にはわかりません。

GitHubに関する関連ディスカッション。どうやら、公式なアプローチもソリューションもありません。おそらく本当の問題は、Reactコンポーネントがどのように使用されることが意図されているかです。シンプルなビューレイヤー(かなり私の提案の1番目)のように、または実際の独立したスタンドアロンコンポーネントのように?


物事を取得するために:非同期呼び出しもサーバー側で発生しますか?一部の部分を空にしてビューをレンダリングし、非同期応答の結果が到着したときにビューを埋めるのではなく、この場合の利点を理解していません。おそらく何かが足りません、ごめんなさい!
phtrivier 14

JavaScriptでは、最新の投稿を取得するためのデータベースへの最も単純なクエリが非同期であることを忘れないでください。したがって、ビューをレンダリングしている場合は、データベースからデータがフェッチされるまで待つ必要があります。また、サーバー側でレンダリングすることには明らかな利点があります。たとえば、SEOです。また、ページのちらつきを防ぎます。実際、サーバー側のレンダリングは、ほとんどのWebサイトが現在も使用している標準的なアプローチです。
tobik 2014

もちろん、ページ全体をレンダリングしようとしていますか(すべての非同期dbクエリが応答した後)?その場合、私はそれを単純に1 /すべてのデータを非同期でフェッチする2 /完了時に分離し、それを「ダム」なReactビューに渡して、リクエストに応答します。または、サーバー側のレンダリングとクライアント側の両方を同じコードで実行しようとしていますか(非同期コードを反応ビューに近づける必要がありますか?)馬鹿げているように聞こえる場合は申し訳ありませんが、よくわかりませんあなたがやっていること。
phtrivier 2014

問題ありません、おそらく他の人々も理解するのに問題があります:)あなたが今説明したのは、2番目の解決策です。しかし、質問からコメントするためのコンポーネントを例にとってみましょう。一般的なクライアント側アプリケーションでは、そのコンポーネントのすべてを実行できます(コメントのロード/追加)。コンポーネントは外界から分離され、外界はこのコンポーネントを気にする必要はありません。それは完全に独立していてスタンドアロンです。しかし、サーバー側のレンダリングを紹介したい場合は、外部で非同期のものを処理する必要があります。そしてそれは全体の原則を破ります。
tobik

明確にするために、私はファイバーの使用を推奨するのではなく、すべての非同期呼び出しを実行し、それらがすべて完了したら(promiseなどを使用して)、サーバー側でコンポーネントをレンダリングします。(反応するコンポーネントは知っているだろうので、すべてのことが唯一の意見です、今。非同期なものについて)、しかし、私は実際のように完全にビューをレンダリングするためにここでしか実際にあるコンポーネントを(リアクトからサーバ通信に関連したものを除去するという考え。)そして、それがreactの背後にある哲学だと思う。とにかく、がんばって:)
phtrivier '09 / 09/29

回答:


15

react-routerを使用する場合はwillTransitionTo、コンポーネントにメソッドを定義するだけで、Transition呼び出すことができるオブジェクトが渡さ.waitれます。

Router.runすべての.waited promise が解決されるまでへのコールバックは呼び出されないため、renderToStringが同期的であるかどうかは関係ありません。そのためrenderToString、ミドルウェアで呼び出された時点で、ストアにデータを追加できました。ストアがシングルトンであっても、同期レンダリング呼び出しとコンポーネントがそれを認識する前に、ジャストインタイムでデータを一時的に設定できます。

ミドルウェアの例:

var Router = require('react-router');
var React = require("react");
var url = require("fast-url-parser");

module.exports = function(routes) {
    return function(req, res, next) {
        var path = url.parse(req.url).pathname;
        if (/^\/?api/i.test(path)) {
            return next();
        }
        Router.run(routes, path, function(Handler, state) {
            var markup = React.renderToString(<Handler routerState={state} />);
            var locals = {markup: markup};
            res.render("layouts/main", locals);
        });
    };
};

routes(ルート階層を記述する)オブジェクトは、クライアントとサーバとの逐語的に共有されています


ありがとう。問題は、私が知る限り、ルートコンポーネントのみがこのwillTransitionTo方法をサポートしているということです。つまり、私が質問で説明したような完全にスタンドアロンの再利用可能なコンポーネントを作成することはまだ不可能です。しかし、ファイバーを使用するつもりがない限り、これはおそらくサーバー側レンダリングを実装するための最善かつ最も反応する方法です。
tobik

これは面白い。willTransitionToメソッドの実装は、非同期データがロードされているように見えますか?
ハイラ

transitionオブジェクトをパラメーターとして取得するため、単にを呼び出しますtransition.wait(yourPromise)。もちろん、これは、promiseをサポートするためにAPIを実装する必要があることを意味します。このアプローチのもう1つの欠点は、クライアント側に「読み込みインジケータ」を実装する簡単な方法がないことです。すべてのpromiseが解決されるまで、遷移はルートハンドラーコンポーネントに切り替わりません。
tobik 14

しかし、実際には「ジャストインタイム」アプローチについてはよくわかりません。複数のネストされたルートハンドラーが1つのURLに一致する可能性があるため、複数のpromiseを解決する必要があります。それらがすべて同時に終了するという保証はありません。ストアがシングルトンの場合、競合が発生する可能性があります。@Esailijaあなたの答えを少し説明してもらえますか?
tobik 14

私は.waited、移行のためのすべての約束を収集する自動配管を導入しています。それらのすべてが満たされると、.runコールバックが呼び出されます。直前に.render()私は約束から一緒にすべてのデータを収集し、レンダリング呼び出しの後に次の行に、その後、singeltonストアの状態を設定し、私は戻ってシングルトンストアを初期化します。かなりハックですが、すべて自動的に行われ、コンポーネントとストアアプリケーションのコードは実質的に同じままです。
エサイリヤ2014

0

これはおそらくあなたが望んでいるものとは正確に違うかもしれませんし、それが意味をなさないかもしれませんが、両方を処理するようにコンポーネントを少し変更することでうまくいったことを覚えています:

  • サーバー側でのレンダリング、すべての初期状態はすでに取得済み、必要に応じて非同期で)
  • クライアント側でのレンダリング、必要に応じてajaxを使用

したがって、次のようなもの:

/** @jsx React.DOM */

var UserGist = React.createClass({
  getInitialState: function() {

    if (this.props.serverSide) {
       return this.props.initialState;
    } else {
      return {
        username: '',
        lastGistUrl: ''
      };
    }

  },

  componentDidMount: function() {
    if (!this.props.serverSide) {

     $.get(this.props.source, function(result) {
      var lastGist = result[0];
      if (this.isMounted()) {
        this.setState({
          username: lastGist.owner.login,
          lastGistUrl: lastGist.html_url
        });
      }
    }.bind(this));

    }

  },

  render: function() {
    return (
      <div>
        {this.state.username}'s last gist is
        <a href={this.state.lastGistUrl}>here</a>.
      </div>
    );
  }
});

// On the client side
React.renderComponent(
  <UserGist source="https://api.github.com/users/octocat/gists" />,
  mountNode
);

// On the server side
getTheInitialState().then(function (initialState) {

    var renderingOptions = {
        initialState : initialState;
        serverSide : true;
    };
    var str = Xxx.renderComponentAsString( ... renderingOptions ...)  

});

申し訳ありませんが、正確なコードが手元にないため、そのままでは機能しない可能性がありますが、ディスカッションのために投稿しています。

繰り返しになりますが、ほとんどのコンポーネントをダムビューとして扱い、コンポーネントから可能な限りデータをフェッチすることを目的としています。


1
ありがとうございました。アイデアはわかりましたが、それは本当に私が望んでいることではありません。Reactを使用して、bbc.comなどのさらに複雑なWebサイトを構築したいとします。ページを見ると、どこにでも「コンポーネント」が見られます。セクション(スポーツ、ビジネス...)は典型的なコンポーネントです。どのように実装しますか?すべてのデータをどこにプリフェッチしますか?このような複雑なサイトを設計するには、コンポーネント(原則として、小さなMVCコンテナーなど)が(おそらく唯一の)非常に良い方法です。コンポーネントアプローチは、多くの典型的なサーバー側フレームワークに共通です。問題は、そのためにReactを使用できるかどうかです。
tobik 2014

サーバーサイドでデータをプリフェッチします(この場合、おそらく「従来の」サーバーサイドテンプレートシステムに渡す前に行われます)。データの表示がモジュール式であることのメリットがあるからといって、データの計算は必然に同じ構造に従う必要があるということですか?私はここで少し悪魔の擁護者を演じています、私はomをチェックアウトするときにあなたが持っているのと同じ問題を抱えていました。そして、誰かがこれについてより多くの洞察を持っていることを私は確信しています-ワイヤの任意の側でシームレスに曲を構成することは大きな助けになります。
phtrivier 2014

1
どこで私はコードのどこを意味します。コントローラで?したがって、BBCのホームページを処理するコントローラーメソッドには、セクション1ごとに、数十の同様のクエリが含まれますか?それは地獄への道の私見です。そうです、私はないという計算が同様にモジュラーであるべきだと思います。すべてが1つのコンポーネント、1つのMVCコンテナーにパックされています。これが標準のサーバー側アプリを開発する方法であり、このアプローチが優れていると確信しています。そして、React.jsにとても興奮しているのは、このアプローチをクライアント側とサーバー側の両方で使用して素晴らしい同形のアプリを作成する大きな可能性があるためです。
tobik 2014

1
どのサイト(大/小)でも、現在のページをサーバー側でレンダリング(SSR)するだけで、初期状態が表示されます。すべてのページに初期状態は必要ありません。サーバーは初期状態を取得してレンダリングし、クライアントに渡します<script type=application/json>{initState}</script>。そうすれば、データはHTMLになります。クライアントでrenderを呼び出して、UIイベントをページに再水和/バインドします。後続のページは、クライアントのjsコード(必要に応じてデータをフェッチ)によって作成され、クライアントによってレンダリングされます。そうすれば、更新によって新しいSSRページが読み込まれ、ページをクリックすることがCSRになります。=同型でSEO対応
Federico 14

0

私は本当にこれにいじりました、そしてこれはあなたの問題に対する答えではありませんが、私はこのアプローチを使いました。React RouterではなくExpressをルーティングに使用したかったのですが、ノードでスレッドのサポートが必要ないため、ファイバーを使用したくありませんでした。

したがって、ロード時にフラックスストアにレンダリングする必要がある初期データについて、AJAXリクエストを実行して初期データをストアに渡すことを決定しました

この例ではFluxxorを使用していました。

だから私の急行ルート、この場合は/productsルート:

var request = require('superagent');
var url = 'http://myendpoint/api/product?category=FI';

request
  .get(url)
  .end(function(err, response){
    if (response.ok) {    
      render(res, response.body);        
    } else {
      render(res, 'error getting initial product data');
    }
 }.bind(this));

次に、データをストアに渡す初期化レンダリングメソッド。

var render = function (res, products) {
  var stores = { 
    productStore: new productStore({category: category, products: products }),
    categoryStore: new categoryStore()
  };

  var actions = { 
    productActions: productActions,
    categoryActions: categoryActions
  };

  var flux = new Fluxxor.Flux(stores, actions);

  var App = React.createClass({
    render: function() {
      return (
          <Product flux={flux} />
      );
    }
  });

  var ProductApp = React.createFactory(App);
  var html = React.renderToString(ProductApp());
  // using ejs for templating here, could use something else
  res.render('product-view.ejs', { app: html });

0

私はこの質問が1年前に尋ねられたことを知っていますが、同じ問題があり、レンダリングされるコンポーネントから派生したネストされたプロミスでそれを解決します。最後に、アプリのすべてのデータを取得し、そのまま送信しました。

例えば:

var App = React.createClass({

    /**
     *
     */
    statics: {
        /**
         *
         * @returns {*}
         */
        getData: function (t, user) {

            return Q.all([

                Feed.getData(t),

                Header.getData(user),

                Footer.getData()

            ]).spread(
                /**
                 *
                 * @param feedData
                 * @param headerData
                 * @param footerData
                 */
                function (feedData, headerData, footerData) {

                    return {
                        header: headerData,
                        feed: feedData,
                        footer: footerData
                    }

                });

        }
    },

    /**
     *
     * @returns {XML}
     */
    render: function () {

        return (
            <label>
                <Header data={this.props.header} />
                <Feed data={this.props.feed}/>
                <Footer data={this.props.footer} />
            </label>
        );

    }

});

そしてルーターで

var AppFactory = React.createFactory(App);

App.getData(t, user).then(
    /**
     *
     * @param data
     */
    function (data) {

        var app = React.renderToString(
            AppFactory(data)
        );       

        res.render(
            'layout',
            {
                body: app,
                someData: JSON.stringify(data)                
            }
        );

    }
).fail(
    /**
     *
     * @param error
     */
    function (error) {
        next(error);
    }
);

0

を使用したサーバー側レンダリングの私のアプローチをあなたと共有したいと思いますFlux

  1. componentストアの初期データがあるとします。

    class MyComponent extends Component {
      constructor(props) {
        super(props);
        this.state = {
          data: myStore.getData()
        };
      }
    }
  2. クラスで初期状態にプリロードされたデータが必要な場合は、ローダーを作成してみましょうMyComponent

     class MyComponentLoader {
        constructor() {
            myStore.addChangeListener(this.onFetch);
        }
        load() {
            return new Promise((resolve, reject) => {
                this.resolve = resolve;
                myActions.getInitialData(); 
            });
        }
        onFetch = () => this.resolve(data);
    }
  3. お店:

    class MyStore extends StoreBase {
        constructor() {
            switch(action => {
                case 'GET_INITIAL_DATA':
                this.yourFetchFunction()
                    .then(response => {
                        this.data = response;
                        this.emitChange();
                     });
                 break;
        }
        getData = () => this.data;
    }
  4. ルーターにデータをロードするだけです:

    on('/my-route', async () => {
        await new MyComponentLoader().load();
        return <MyComponent/>;
    });

0

短いロールアップと同じように-> GraphQLはスタックの問題を解決します...

  • GraphQLを追加
  • アポロとリアクションアポロを使用する
  • レンダリングを開始する前に「getDataFromTree」を使用してください

-> getDataFromTreeは、アプリ内の関連するすべてのクエリを自動的に検出して実行し、サーバー上のアポロキャッシュをpouplingして、完全に機能するSSRを有効にします。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.