简体   繁体   English

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

[英]Strategies for server-side rendering of asynchronously initialized React.js components

One of the biggest advantages of React.js is supposed to be server-side rendering . React.js的一大优势应该是服务器端渲染 The problem is that the key function React.renderComponentToString() is synchronous which makes it impossible to load any asynchronous data as the component hierarchy is rendered on the server. 问题是关键函数React.renderComponentToString()是同步的,这使得在服务器上呈现组件层次结构时无法加载任何异步数据。

Let's say I have a universal component for commenting which I can drop pretty much anywhere on the page. 假设我有一个用于评论的通用组件,我可以在页面的任何位置删除它。 It has only one property, some kind of identifier (for example id of an article below which the comments are placed), and everything else is handled by the component itself (loading, adding, managing comments). 它只有一个属性,某种标识符(例如文章的id在其下面放置注释),其他所有内容都由组件本身处理(加载,添加,管理注释)。

I really like the Flux architecture because it makes a lot of things much easier, and its stores are perfect for sharing state between server and client. 我非常喜欢Flux架构,因为它使很多东西变得更容易,而且它的存储非常适合在服务器和客户端之间共享状态。 Once my store containing comments is initialized, I can just serialize it and send it from server to client where it is easily restored. 一旦我的包含注释的商店被初始化,我就可以将其序列化并将其从服务器发送到客户端,在那里可以轻松恢复。

The question is what is the best way to populate my store. 问题是填充我的商店的最佳方式是什么。 During past days I've been googling a lot and I've come across few strategies, none of which seemed really good considering how much this feature of React is being "promoted". 在过去的几天里,我一直在谷歌搜索,我遇到了一些策略,考虑到React的这个功能被“提升”了多少,这些策略似乎都不是很好。

  1. In my opinion, the simplest way is to populate all my stores before the actual rendering begins. 在我看来,最简单的方法是在实际渲染开始之前填充我的所有商店。 That means somewhere outside of the component hierarchy (hooked to my router for example). 这意味着在组件层次结构之外的某个地方(例如,挂钩到我的路由器)。 The problem with this approach is that I would have to pretty much define the page structure twice. 这种方法的问题是我必须两次定义页面结构。 Consider a more complex page, for example a blog page with many different components (actual blog post, comments, related posts, newest posts, twitter stream...). 考虑一个更复杂的页面,例如一个包含许多不同组件的博客页面(实际的博客文章,评论,相关帖子,最新帖子,推特流......)。 I would have to design the page structure using React components and then somewhere else I would have to define the process of populating each required store for this current page. 我必须使用React组件设计页面结构,然后在其他地方我必须定义为当前页面填充每个所需存储的过程。 That doesn't seem like a nice solution to me. 这似乎不是一个很好的解决方案。 Unfortunately most isomorphic tutorials are designed this way (for example this great flux-tutorial ). 不幸的是,大多数同构教程都是以这种方式设计的(例如,这个伟大的通量教程 )。

  2. React-async . React-async This approach is perfect. 这种方法很完美。 It lets me simply define in a special function in each component how to initialize the state (doesn't matter whether synchronously or asynchronously) and these functions are called as the hierarchy is being rendered to HTML. 它让我只需在每个组件的特殊函数中定义如何初始化状态(无论是同步还是异步),并在将层次结构呈现为HTML时调用这些函数。 It works in a way that a component is not rendered until the state is completely initialized. 它的工作方式是在状态完全初始化之前不会呈现组件。 The problem is that it requires Fibers which is, as far as I understand, a Node.js extension that alters the standard JavaScript behavior. 问题是它需要Fibers ,据我所知,这是一个改变标准JavaScript行为的Node.js扩展。 Although I really like the result, it still seems to me that instead of finding a solution we changed the rules of the game. 虽然我非常喜欢这个结果,但在我看来,我们并没有找到解决方案,而是改变了游戏规则。 And I think we shouldn't be forced to do that to use this core feature of React.js. 而且我认为我们不应该被迫这样做来使用React.js的这个核心功能。 I'm also not sure about the general support of this solution. 我也不确定这个解决方案的一般支持。 Is it possible to use Fiber on standard Node.js web hosting? 是否可以在标准Node.js虚拟主机上使用Fiber?

  3. I was thinking a little on my own. 我自己在思考一点。 I haven't really thought trough the implementation details but the general idea is that I would extend the components in similar way to React-async and then I would repeatedly call React.renderComponentToString() on the root component. 我没有真正想过通过实现细节,但一般的想法是我会以类似于React-async的方式扩展组件,然后我会在根组件上重复调用React.renderComponentToString()。 During each pass I would collect the extending callbacks and then call them at the and of the pass to populate the stores. 在每次传递过程中,我会收集扩展回调,然后在传递和传递中调用它们来填充商店。 I would repeat this step until all stores required by current component hierarchy would be populated. 我将重复此步骤,直到填充当前组件层次结构所需的所有存储。 There are many things to be solved and I'm particularly unsure about the performance. 有很多事情需要解决,我对性能特别不确定。

Did I miss something? 我错过了什么? Is there another approach/solution? 还有其他方法/解决方案吗? Right now I'm thinking about going the react-async/fibers way but I'm not completely sure about it as explained in the second point. 现在我正在考虑采用react-async / fiber方式,但我并不完全确定它,如第二点所述。

Related discussion on GitHub . 关于GitHub的相关讨论 Apparently, there is no official approach or even solution. 显然,没有官方方法甚至解决方案。 Maybe the real question is how the React components are intended to be used. 也许真正的问题是如何使用React组件。 Like simple view layer (pretty much my suggestion number one) or like real independent and standalone components? 像简单的视图层(几乎是我的建议第一)或像真正的独立和独立组件?

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. 如果你使用react-router ,你可以在组件中定义一个willTransitionTo方法,它们会传递一个你可以调用.waitTransition对象。

It doesn't matter if renderToString is synchronous because the callback to Router.run will not be called until all .wait ed promises are resolved, so by the time renderToString is called in the middleware you could have populated the stores. 如果renderToString是同步的并不重要,因为在解析所有.wait ed promise之前不会调用对Router.run的回调,因此在中间件中调用renderToString你可以填充存储。 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 routes对象(描述路由层次结构)与客户端和服务器逐字共享

I know this is probably not exactly what you want, and it might not make sense, but I remember getting by with slighly modifying the component to handle both : 我知道这可能不是你想要的,它可能没有意义,但我记得通过轻微修改组件来处理两者:

  • rendering on the server side, with all the initial state already retrieved, asynchronously if needed) 在服务器端渲染,已经检索到所有初始状态,如果需要,可以异步进行)
  • rendering on the client side, with ajax if needed 在客户端渲染,如果需要,使用ajax

So something like : 所以类似于:

/** @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 ...)  

});

I'm sorry I don't have the exact code at hand, so this might not work out of the box, but I'm posting in the interest of discussion. 对不起,我手边没有确切的代码,所以这可能不是开箱即用的,但我是为了讨论而发布的。

Again, the idea is to treat most of the component as a dumb view, and deal with fetching data as much as possible out of the component. 同样,我们的想法是将大部分组件视为一个愚蠢的视图,并尽可能多地组件中获取数据。

I was really messed around with this today, and although this is not an answer to your problem, I have used this approach. 今天我真的很乱,虽然这不是你的问题的答案,但我已经使用了这种方法。 I wanted to use Express for routing rather than React Router, and I didn't want to use Fibers as I didn't need threading support in node. 我想使用Express进行路由而不是React Router,我不想使用Fibers,因为我不需要节点中的线程支持。

So I just made a decision that for initial data which needs to be rendered to the flux store on load, I will perform an AJAX request and pass the initial data into the store 所以我只是决定对于需要在加载时将其呈现给磁通存储的初始数据,我将执行AJAX请求并将初始数据传递到存储中

I was using Fluxxor for this example. 我在这个例子中使用Fluxxor。

So on my express route, in this case a /products route: 所以在我的快速路线上,在这种情况下是/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));

Then my initialize render method which passes the data to the store. 然后我初始化渲染方法,将数据传递给商店。

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

I know this question was asked a year ago but we had the same problem and we solve it with nested promises that were derived from the components that are going to be render. 我知道这个问题是在一年前问的,但是我们遇到了同样的问题,我们用嵌套的promises来解决它,这些promises是从将要渲染的组件派生出来的。 In the end we had the all data for the app and just sent it down the way. 最后,我们获得了应用程序的所有数据,并将其发送到了最后。

For example: 例如:

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

    }

});

and in the router 并在路由器中

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

Wanna share with you my approach of server side rendering using Flux , little be simplified for example: 想与我分享我使用Flux进行服务器端渲染的方法,例如:

  1. Let's say we have component with initial data from store: 假设我们有来自商店的初始数据的component

     class MyComponent extends Component { constructor(props) { super(props); this.state = { data: myStore.getData() }; } } 
  2. If class require some preloaded data for initial state let's create Loader for MyComponent : 如果class需要一些初始状态的预加载数据,那么让我们为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. Store: 商店:

     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. Now just load data in router: 现在只需在路由器中加载数据:

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

just as a short rollup -> GraphQL will solve this entierly for your stack... 就像一个简短的汇总 - > GraphQL将为你的堆栈解决这个问题...

  • add GraphQL 添加GraphQL
  • use apollo and react-apollo 使用apollo和react-apollo
  • use "getDataFromTree" before you start rendering 在开始渲染之前使用“getDataFromTree”

-> getDataFromTree will automatically find all the involved queries in your app and execute them, pouplating your apollo cache on the server and thus, enabling fully working SSR.. BÄM - > getDataFromTree将自动在您的应用程序中查找所有相关查询并执行它们,在服务器上放置您的apollo缓存,从而实现完全正常工作的SSR..BÄM

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM