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


114

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

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

我真的很喜欢 Flux架构,因为它使许多事情变得容易得多,并且它的存储非常适合在服务器和客户端之间共享状态。初始化包含评论的商店后,我可以对其进行序列化并将其从服务器发送到易于还原的客户端。

问题是填充我的商店的最佳方法是什么。在过去的几天里,我一直在搜寻很多东西,并且遇到过几种策略,考虑到React的这一功能正在“推广”多少,这些策略似乎都不是一件好事。

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

  2. 反应-异步。这种方法是完美的。它使我可以简单地在每个组件的特殊功能中定义如何初始化状态(无论是同步还是异步),这些功能在将层次结构呈现给HTML时被调用。它的工作方式是直到状态完全初始化后才呈现组件。问题是它需要纤维据我了解,这是一个Node.js扩展,可更改标准JavaScript行为。尽管我非常喜欢结果,但在我看来,我们没有找到解决方案,而是更改了游戏规则。而且我认为我们不应被迫使用React.js的这一核心功能。我也不确定该解决方案的一般支持。是否可以在标准Node.js虚拟主机上使用Fiber?

  3. 我自己想了一下。我还没有真正考虑过实现细节,但是总体思路是,我将以类似于React-async的方式扩展组件,然后在根组件上重复调用React.renderComponentToString()。在每次通过期间,我将收集扩展的回调,然后在和处调用它们,以填充商店。我将重复此步骤,直到将填充当前组件层次结构所需的所有商店。有很多事情需要解决,我尤其不确定性能。

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

GitHub上的相关讨论。显然,没有官方的方法甚至解决方案。可能真正的问题是应如何使用React组件。像简单的视图层(几乎是我的建议之一)还是真正的独立组件?


只是想知道:异步调用也会在服务器端发生?与将视图中的某些部分留为空白并在异步响应结果到达时填充视图相比,我不了解这种情况下的好处。可能缺少一些东西,对不起!
phtrivier 2014年

您一定不要忘记在JavaScript中,即使是最简单的数据库查询也是最新的异步查询。因此,如果要渲染视图,则必须等到从数据库中获取数据。在服务器端进行渲染有明显的好处:例如SEO。并且还可以防止页面闪烁。实际上,服务器端渲染是大多数网站仍在使用的标准方法。
tobik

可以,但是您是否要渲染整个页面(一旦所有异步数据库查询都已响应)?在这种情况下,我会天真地将其分隔为1 /异步获取所有数据2 /完成后将其传递给“哑” React View,并响应请求。还是您要同时进行服务器端渲染和客户端渲染(都需要使用相同的代码(并且您需要异步代码来接近react视图)?)很抱歉,这听起来很愚蠢,但我不确定我是否知道你在做什么。
phtrivier 2014年

没问题,也许其他人也有需要理解的问题:)您刚刚描述的是第二个解决方案。但是以问题注释的组件为例。在普通的客户端应用程序中,我可以执行该组件中的所有操作(加载/添加注释)。该组件将与外部世界分离,并且外部世界将不必在意此组件。这将是完全独立和独立的。但是,一旦要引入服务器端渲染,就必须在外部处理异步内容。这打破了整个原则。
tobik 2014年

需要明确的是,我不主张使用光纤,而只是进行所有异步调用,并在完成所有异步调用(使用promise或其他方法),将组件呈现在服务器端。(因此,反应的组分不知道在所有关于异步的东西。)现在,这只是一个意见,但我确实喜欢完全消除与从反应的组分服务器通信的任何想法(这是真的只有在这里渲染视图。)我认为这就是反应背后的理念,这也许可以解释为什么您在做的事情有点复杂。无论如何,祝你好运:)
phtrivier 2014年

Answers:


15

如果使用react-router,则只能willTransitionTo在组件中定义一个方法,该方法将传递一个Transition可以调用的对象.wait

renderToString是否同步并不重要,因为在解决Router.run所有.waited promise 之前不会调用to的回调,因此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,否则这可能是实现服务器端渲染的最佳方法,也是反应最快的方法。
tobik 2014年

这是有趣的。willTransitionTo方法的实现将如何加载异步数据?
Hyra 2014年

您将获得该transition对象作为参数,因此只需调用transition.wait(yourPromise)。当然,这意味着您必须实现您的API以支持诺言。这种方法的另一个缺点是,没有简单的方法可以在客户端实现“加载指示器”。在解决所有承诺之前,过渡不会切换到路由处理程序组件。
tobik

但是我实际上不确定“及时”方法。多个嵌套的路由处理程序可以匹配一个URL,这意味着必须解决多个promise。无法保证它们都将同时结束。如果商店是单身人士,则可能导致冲突。@Esailija您能否解释一下您的答案?
tobik

我已经安装了自动管道系统,该管道系统收集了所有有关.waited过渡的承诺。一旦满足所有条件,便会.run调用回调。在.render()我从承诺中收集所有数据并设置singelton存储状态之前,然后在render调用之后的下一行初始化单例存储。这很hacky,但是它们都是自动发生的,并且组件和存储应用程序代码几乎保持不变。
Esailija 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。看着页面,到处都可以看到“组件”。一个部分(体育,商务...)是典型的组成部分。您将如何实施?您将在哪里预取所有数据?要设计如此复杂的站点,组件(作为原则,就像小的MVC容器一样)是非常好的方法(如果可能的话)。组件方法在许多典型的服务器端框架中很常见。问题是:我可以使用React吗?
tobik

您将在服务器端预取数据(在这种情况下可能已经完成,然后将其传递给“传统的”服务器端模板系统);仅仅因为数据的显示受益于模块化,这是否意味着数据的计算必须遵循相同的结构?我在这里扮演恶魔的拥护者,签出om时遇到了同样的麻烦。而且我敢肯定,有人会比我有更多的见识-在电线的任何一侧无缝地组合内容会很有帮助。
phtrivier

1
我指的是代码中的位置。在控制器中?那么处理bbc主页的控制器方法将包含许多类似的查询,对于每个部分而言?那真是个地狱的方法。是的,我确实认为计算也应该是模块化的。一切都打包成一个组件,放在一个MVC容器中。这就是我开发标准服务器端应用程序的方式,我对此方法很有信心。我之所以对React.js如此兴奋,是因为在客户端和服务器端都可以使用这种方法来创建很棒的同构应用程序。
tobik 2014年

1
在任何站点(大/小)上,您只需要将当前页面及其初始化状态进行服务器端渲染(SSR);您不需要每个页面的初始化状态。服务器获取初始化状态,将其渲染,然后将其传递给客户端<script type=application/json>{initState}</script>。这样,数据将保存在HTML中。通过在客户端上调用render将UI事件重新绑定/绑定到页面。随后的页面由客户端的js代码创建(根据需要获取数据)并由客户端呈现。这样,任何刷新将加载新的SSR页面,并且单击页面将是CSR。=同构和SEO友好
2014年

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

然后是我的initialize渲染方法,该方法将数据传递到商店。

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

我知道这个问题是一年前提出的,但是我们遇到了同样的问题,我们使用嵌套承诺来解决它,该承诺是从将要渲染的组件派生而来的。最终,我们获得了该应用程序的所有数据,然后将其发送出去。

例如:

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以下对象创建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将自动在您的应用程序中找到所有涉及的查询并执行它们,从而将您的阿波罗缓存在服务器上合并,从而启用完全正常的SSR。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.