异步初始化的React.js组件的服务器端呈现策略

时间:2014-09-22 20:58:07

标签: node.js reactjs flux reactjs-flux

React.js 的一大优势应该是服务器端呈现。问题是密钥函数React.renderComponentToString()是同步的,这使得在服务器上呈现组件层次结构时无法加载任何异步数据。

假设我有一个用于评论的通用组件,我可以在页面的任何位置删除它。它只有一个属性,某种标识符(例如文章的id在其下面放置注释),其他所有内容都由组件本身处理(加载,添加,管理注释)。

我非常喜欢 Flux 架构,因为它使很多事情变得更容易,而且它的商店非常适合在服务器和客户端之间共享状态。一旦我的包含注释的商店被初始化,我就可以将其序列化并将其从服务器发送到客户端,以便轻松恢复。

问题是填充商店的最佳方式是什么。在过去的几天里,我一直在谷歌搜索,我遇到了一些策略,考虑到React的这个功能被“提升”了多少,这些策略看起来都不是很好。

  1. 在我看来,最简单的方法是在实际渲染开始之前填充我的所有商店。这意味着在组件层次结构之外的某个地方(例如,挂钩到我的路由器)。这种方法的问题是我必须两次定义页面结构。考虑一个更复杂的页面,例如一个包含许多不同组件的博客页面(实际的博客文章,评论,相关帖子,最新帖子,推特流......)。我必须使用React组件设计页面结构,然后在其他地方我必须定义为当前页面填充每个所需存储的过程。这似乎不是一个很好的解决方案。不幸的是,大多数同构教程都是以这种方式设计的(例如,这个伟大的flux-tutorial)。

  2. React-async。这种方法很完美。它让我只需在每个组件的特殊函数中定义如何初始化状态(无论是同步还是异步),并在将层次结构呈现为HTML时调用这些函数。它的工作方式是在状态完全初始化之前不会呈现组件。问题是它需要Fibers,据我所知,这是一个改变标准JavaScript行为的Node.js扩展。虽然我非常喜欢这个结果,但在我看来,我们并没有找到解决方案,而是改变了游戏规则。而且我认为我们不应该被迫这样做来使用React.js的这个核心功能。我也不确定这个解决方案的一般支持。是否可以在标准Node.js虚拟主机上使用Fiber?

  3. 我在想自己一点点。我没有真正想过通过实现细节,但一般的想法是我会以类似于React-async的方式扩展组件,然后我会在根组件上重复调用React.renderComponentToString()。在每次传递过程中,我会收集扩展回调,然后在传递和传递中调用它们来填充商店。我将重复此步骤,直到填充当前组件层次结构所需的所有存储。有很多事情需要解决,我对性能特别不确定。

  4. 我错过了什么吗?还有其他方法/解决方案吗?现在我正在考虑采用react-async / fiber方式,但我并不完全确定它,如第二点所述。

    Related discussion on GitHub。显然,没有官方方法甚至解决方案。也许真正的问题是如何使用React组件。就像简单的视图层(几乎是我的建议第一)或者像真正的独立和独立组件一样?

6 个答案:

答案 0 :(得分:14)

如果您使用react-router,则只需在组件中定义willTransitionTo个方法,这些方法会传递一个Transition对象,您可以在其上调用.wait

renderToString是否同步并不重要,因为在解析所有Router.run ed promises之前不会调用.wait的回调,所以在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对象(描述路由层次结构)与客户端和服务器逐字共享

答案 1 :(得分: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 ...)  

});

对不起,我手边没有确切的代码,所以这可能不是开箱即用的,但我是为了讨论而发布的。

同样,我们的想法是将大部分组件视为一个愚蠢的视图,并尽可能多地提取数据 out

答案 2 :(得分:0)

今天我对此非常不满,虽然这不是你问题的答案,但我使用了这种方法。我想使用Express而不是React Router,我不想使用Fibers,因为我不需要节点中的线程支持。

所以我只做了一个决定,对于需要在加载时向通用存储器呈现的初始数据,我将执行一个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 });

答案 3 :(得分:0)

我知道这个问题是在一年前被问到的,但是我们遇到了同样的问题,我们用嵌套的promises来解决它,这些promises是从将要渲染的组件派生出来的。最后,我们获得了应用程序的所有数据,并将其发送到了最后。

例如:

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);
    }
);

答案 4 :(得分: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/>;
    });
    

答案 5 :(得分:0)

就像一个简短的汇总-> GraphQL将为您的堆栈彻底解决这个问题...

  • 添加GraphQL
  • 使用apollo和react-apollo
  • 在开始渲染之前使用“ getDataFromTree”

-> getDataFromTree将自动在您的应用程序中找到所有涉及的查询并执行它们,从而将您的阿波罗缓存在服务器上合并,从而启用完全正常的SSR。BÄM