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

119
React.js的最大优势之一是服务器端渲染。问题在于关键函数React.renderComponentToString()是同步的,这使得无法加载任何异步数据,因为组件层次结构是在服务器上呈现的。
假设我有一个通用的评论组件,我可以将它放在页面的任何位置。 它只有一个属性,某种标识符(例如下方文章的ID,在其中放置评论),其他所有内容都由组件本身处理(加载、添加、管理评论)。
我非常喜欢Flux架构,因为它使很多事情变得更容易,而它的stores非常适合在服务器和客户端之间共享状态。一旦我的包含评论的存储库被初始化,我可以将其序列化并从服务器发送到客户端,然后在客户端轻松恢复。
问题是什么是填充我的存储库的最佳方法。在过去的几天里,我已经搜索了很多,并且找到了几个策略,但没有一个看起来真正好,考虑到React的这个特性被“推广”了多少。
  1. 我认为,最简单的方法是在实际渲染开始之前填充所有我的存储库。那意味着在组件层次结构之外的某个地方(例如连接到我的路由器)。这种方法的问题是,我基本上必须两次定义页面结构。考虑一个更复杂的页面,例如带有许多不同组件的博客页面(实际博客文章、评论、相关文章、最新文章、Twitter流...)。我必须使用React组件设计页面结构,然后在其他地方,我必须定义填充用于此当前页面所需的每个存储库的过程。对我来说,那不像是一个好的解决方案。不幸的是,大多数等离子教程都是这样设计的(例如这个伟大的flux-tutorial)。

  2. React-async。这种方法非常完美。它允许我在每个组件中简单地定义一个特殊函数,用于初始化状态(无论是同步还是异步都没问题),并在呈现到 HTML 时调用这些函数。它的工作方式是在状态完全初始化之前不呈现组件。问题在于它需要 Fibers,据我了解,它是一种更改标准 JavaScript 行为的 Node.js 扩展。虽然我非常喜欢结果,但我觉得与其找出解决方案,我们还不如改变游戏规则。我认为我们不应该被迫这样做才能使用 React.js 的核心功能。我也不确定这种解决方案的普遍支持性。能否在标准的 Node.js Web 主机上使用 Fiber?

  3. 我自己想了一下。我还没有仔细思考实现细节,但基本思路是我会类似于 React-async 中的方式扩展组件,然后重复调用 React.renderComponentToString() 在根组件上。在每次遍历过程中,我将收集扩展回调函数,然后在遍历结束时调用它们来填充存储器。我会重复这个步骤,直到当前组件层次结构所需的所有存储器都被填充。有许多问题需要解决,我特别不确定性能。

  4. 我错过了什么吗?还有其他方法/解决方案吗?现在我正在考虑使用 react-async/fibers 的方式,但像第二点中解释的那样,我并不完全确定。

    GitHub 上的相关讨论。显然,没有官方的方法甚至解决方案。也许真正的问题是 React 组件的预期用法。像简单的视图层(几乎就是我的第一个建议)还是像真正独立的组件?


只是为了搞清楚事情:异步调用也会在服务器端发生吗?我不理解这种情况下的好处,与将视图呈现为空白部分并随着异步响应结果的到来填充它相比。可能是我漏掉了什么,抱歉! - phtrivier
你必须记住,在JavaScript中,即使是最简单的查询数据库以获取最新帖子也是异步的。因此,如果您正在渲染视图,则必须等待数据从数据库中获取。而且,服务器端呈现具有明显的好处:例如SEO。它还可以防止页面闪烁。实际上,服务器端呈现是大多数网站仍在使用的标准方法。 - tobik
当然可以,但是你是想在所有异步数据库查询都响应后渲染整个页面吗?如果是这样的话,我会天真地将其分为1/异步获取所有数据2/完成后,将其传递给“愚蠢”的React视图,并响应请求。还是说你想同时进行服务器端渲染和客户端渲染,并且需要将异步代码靠近React视图?如果我听起来很傻,请原谅,我只是不确定你在做什么。 - phtrivier
只是为了明确,我并不主张使用 fibers,而是在所有异步调用完成后(使用 promise 或其他方式),在服务器端呈现组件。 (因此,React 组件将完全不知道与异步相关的任何内容。)现在,这只是一个观点,但我实际上喜欢完全从 React 组件中删除与服务器通信有关的任何内容(这些组件真正只是在此处呈现视图)。 我认为这就是 React 的哲学,这可能解释了为什么你正在做的事情有点复杂。 无论如何,祝你好运 :) - phtrivier
抱歉,我可能让你感到困惑了。你描述的不是解决方案二,而是解决方案一(不是纤程)。我的观点是,我并不孤单,React可以做得更多。这是构建网站的方式。您从小型可重用组件开始,并将它们组合成完整的组件层次结构,从而创建实际页面。如果我必须在组件外获取所有信息,那么我将不得不重新创建整个层次结构。我会做两次相同的事情。我会将React层减少到简单的视图层,这将是浪费潜力。 - tobik
显示剩余8条评论
6个回答

16
如果您使用react-router,您只需要在组件中定义一些willTransitionTo方法,该方法会传递一个Transition对象,您可以在其上调用.wait
无论renderToString是同步还是异步的,因为在所有等待的promise解决之前,回调函数Router.run都不会被调用,因此在中间件中调用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方法。这意味着仍然不可能编写完全独立可重用的组件,就像我在问题中描述的那样。但是,除非我们愿意使用Fibers,否则这可能是实现服务器端渲染的最佳和最_react_的方法。 - tobik
无论如何,我更喜欢商店不是单例。而且这并不难做到,我唯一需要的就是将组件属性(或React上下文)作为willTransitionTo的另一个参数传递。我实际上向react-async团队提出了一个具体的代码更改建议(当时只需要更改一行代码),但他们说他们有更好、更通用的东西即将推出。所以谁知道呢。总之,我的目标是编写100%同构代码,因此如果在你描述的那些情况下没有调用willTransitionTo,那么这可能不是正确的方法。 - tobik
更新:willTransitionTo现在即使只更改查询的一部分也会被执行:https://github.com/rackt/react-router/commit/c6aa4d3 - tobik
组件通过 Promise 将 Store 状态快照传递给 willTransitionTo 方法中的 .wait,这些 Promise 被收集并聚合,当它们全部准备就绪时,快照将被用于获取所有受影响的 Store 的正确状态,就在渲染调用之前。 - Esailija
@tobik 嗯,这都是自动化的,如果客户端正确调用了 willTransitionTo,我的组件代码对双方来说都应该完全一样。尽管有缺陷,但它已经足够好了。唯一的区别在于存储如何获取数据,在客户端上使用 $.ajax,在服务器端上则需要调用 dao 或 api 服务器。 - Esailija
显示剩余8条评论

0
今天我真的被这个问题搞糊涂了,虽然这不是解决你的问题的答案,但我已经采用了这种方法。我想使用Express进行路由而不是React Router,并且我不想使用Fibers,因为我不需要在node中支持线程。
所以我只是决定对于需要在加载时呈现到flux存储器中的初始数据,我将执行一个AJAX请求并将初始数据传递到存储器中。
我在这个例子中使用Fluxxor。
所以在我的express路由上,在这种情况下是一个/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

想和大家分享我使用 Flux 进行服务器端渲染的方法,这里简化了一些示例:

  1. 假设我们有一个带有来自存储的初始数据的 component:

    class MyComponent extends Component {
      constructor(props) {
        super(props);
        this.state = {
          data: myStore.getData()
        };
      }
    }
    
  2. 如果类需要一些预加载数据作为初始状态,让我们为 MyComponent 创建 Loader:

     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
  • 使用 apollo 和 react-apollo
  • 在开始渲染之前使用 "getDataFromTree"

-> getDataFromTree 将自动查找应用程序中涉及的所有查询并执行它们,在服务器上填充您的 apollo 缓存,从而实现完全工作的 SSR.. BÄM


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 构建一个更复杂的网站,像 http://www.bbc.com/ 这样的。看着这个页面,我可以看到“组件”无处不在。一个部分(体育、商业…)就是一个典型的组件。你会怎么实现它?你会在哪里预取所有的数据?为了设计这样一个复杂的网站,组件(作为一个原则,像小的 MVC 容器)是非常好的(如果可能的话,是唯一的)方法。组件化的方法对于许多典型的服务器端框架来说是常见的。问题是:我能用 React 来做到这一点吗? - tobik
你将在服务器端预取数据(在将其传递给“传统”的服务器端模板系统之前,很可能已经完成了这个过程);仅仅因为数据的显示受益于模块化,这是否意味着数据的计算必须遵循相同的结构?我在这里有点像魔鬼的代言人,当我检查om时,我也遇到了同样的问题。我真的希望有人比我更有深入的见解——在任何一侧无缝地组合东西会帮助很多。 - phtrivier
1
我的意思是代码中的哪里。在控制器中吗?因此,处理bbc主页的控制器方法将包含类似的查询,每个部分一个?这是一条通往地狱的路。所以,是的,我认为计算也应该是模块化的。所有内容都打包在一个组件中,在一个MVC容器中。这就是我开发标准服务器端应用程序的方式,我非常有信心这种方法是好的。我对React.js感到如此兴奋的原因是,在客户端和服务器端都可以使用这种方法来创建令人惊叹的同构应用程序。 - tobik
1
在任何网站(大/小)上,您只需要使用服务器端渲染(SSR)当前页面及其初始状态;您不需要为每个页面都初始化状态。服务器获取初始状态,进行渲染,并将其传递给客户端<script type = application / json> {initState} </ script>;这样数据就会在HTML中。通过在客户端上调用渲染,重新激活/绑定UI事件到页面。后续页面由客户端的JS代码创建(根据需要获取数据)并由客户端呈现。这样任何刷新都将加载新的SSR页面,单击页面将是CSR。=等同和有利于SEO - Federico

0

我知道这个问题是一年前提出的,但我们遇到了同样的问题,并通过嵌套的 Promise 解决了它,这些 Promise 派生自将要呈现的组件。最终,我们拥有了应用程序的所有数据,并将其沿着路径发送。

例如:

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接