117

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

假设我有一个通用的评论组件,我可以将它放在页面上的几乎任何地方。它只有一个属性、某种标识符(例如,在其下方放置评论的文章的 id),其他一切都由组件本身处理(加载、添加、管理评论)。

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

问题是填充我的商店的最佳方式是什么。在过去的几天里,我一直在谷歌上搜索,我遇到了一些策略,考虑到 React 的这个特性被“推广”了多少,这些策略似乎都不是很好。

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

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

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

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

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

4

6 回答 6

15

If you use react-router, you can just define a willTransitionTo methods in components, which gets passed a Transition object that you can call .wait on.

It doesn't matter if renderToString is synchronous because the callback to Router.run will not be called until all .waited promises are resolved, so by the time renderToString is called in the middleware you could have populated the stores. Even if the stores are singletons you can just set their data temporarily just-in-time before the synchronous rendering call and the component will see it.

Example of middleware:

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

The routes object (which describes the routes hierarchy) is shared verbatim with client and server

于 2014-12-14T14:11:42.347 回答
0

今天我真的搞砸了,虽然这不是你问题的答案,但我已经使用了这种方法。我想使用 Express 而不是 React Router 进行路由,并且我不想使用 Fibers,因为我不需要 node 中的线程支持。

所以我刚刚决定,对于需要在加载时呈现到通量存储的初始数据,我将执行 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 });
于 2015-04-29T05:50:29.563 回答
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 ...)  

});

很抱歉,我手头没有确切的代码,所以这可能无法开箱即用,但我发帖是为了讨论。

同样,这个想法是将大部分组件视为一个哑视图,并尽可能多地组件中获取数据。

于 2014-09-30T20:48:05.020 回答
0

想和大家分享一下我使用的服务端渲染的方法Flux,稍微简化一下例如:

  1. 假设我们有component来自商店的初始数据:

    class MyComponent extends Component {
      constructor(props) {
        super(props);
        this.state = {
          data: myStore.getData()
        };
      }
    }
    
  2. 如果类需要一些预加载的初始状态数据,让我们为以下内容创建 Loader 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/>;
    });
    
于 2016-03-12T22:33:43.510 回答
0

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

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

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

于 2018-09-20T16:08:28.540 回答
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);
    }
);
于 2015-09-04T08:59:22.743 回答