Стратегии для серверной рендеринга асинхронно инициализированных компонентов React.js

Одно из самых больших преимуществ React.js - рендеринг на стороне сервера. Проблема в том, что ключевая функция React.renderComponentToString() является синхронной, что делает невозможным загрузку любых асинхронных данных по мере рендеринга иерархии компонентов на сервере.

Скажем, у меня есть универсальный компонент для комментирования, который я могу сделать практически на любом месте страницы. Он имеет только одно свойство, какой-то идентификатор (например, идентификатор статьи, ниже которой помещены комментарии), а все остальное обрабатывается самим компонентом (загрузка, добавление, управление комментариями).

Мне очень нравится архитектура Flux, потому что она упрощает многое, и ее магазины идеально подходят для совместного использования состояния между сервером и клиентом. После инициализации моего хранилища, содержащего комментарии, я могу просто сериализовать его и отправить его с сервера на клиент, где он легко восстанавливается.

Вопрос заключается в том, что лучший способ заполнить мой магазин. В прошлые дни я много гулял, и я столкнулся с несколькими стратегиями, ни один из которых не казался действительно хорошим, учитывая, насколько эта особенность React "продвигается".

  • По-моему, самый простой способ - заполнить все мои магазины до начала фактического рендеринга. Это означает, что где-то вне иерархии компонентов (например, подключен к моему маршрутизатору). Проблема с этим подходом заключается в том, что мне придется в значительной степени определить структуру страницы дважды. Рассмотрим более сложную страницу, например, страницу блога со множеством разных компонентов (фактическое сообщение в блоге, комментарии, связанные сообщения, новые сообщения, твиттер-поток...). Мне пришлось бы разрабатывать структуру страницы с использованием компонентов React, а затем в другом месте мне нужно было бы определить процесс заполнения каждого требуемого хранилища для этой текущей страницы. Это не похоже на хорошее решение для меня. К сожалению, большинство изоморфных учебников разработаны таким образом (например, этот отличный flux-tutorial).

  • React-async. Этот подход идеален. Это позволяет мне просто определить в специальной функции в каждом компоненте инициализацию состояния (неважно, синхронно или асинхронно), и эти функции вызывается, когда иерархия отображается в HTML. Он работает таким образом, что компонент не отображается, пока состояние полностью не инициализируется. Проблема в том, что она требует Fibers, которая, насколько я понимаю, является расширением Node.js, которое изменяет стандартное поведение JavaScript. Хотя мне действительно нравится результат, мне все же кажется, что вместо того, чтобы найти решение, мы изменили правила игры. И я думаю, мы не должны быть вынуждены сделать это, чтобы использовать эту основную функцию React.js. Я также не уверен в общей поддержке этого решения. Можно ли использовать Fiber на стандартном веб-хостинге Node.js?

  • Я думал немного о себе. Я действительно не думал о деталях реализации, но общая идея заключается в том, что я бы расширил компоненты аналогично React-async, а затем я бы повторно назовет React.renderComponentToString() в корневом компоненте. Во время каждого прохода я собирал расширившиеся обратные вызовы, а затем вызывал их на проходе и в проходе, чтобы заполнить магазины. Я бы повторил этот шаг, пока не будут заполнены все магазины, требуемые текущей иерархией компонентов. Есть много вещей, которые нужно решить, и я особенно не уверен в производительности.

Я что-то пропустил? Есть ли другой подход/решение? Прямо сейчас я думаю о том, как идти по пути реакции-асинк/волокна, но я не совсем уверен в этом, как объясняется во втором.

Связанная дискуссия о GitHub. По-видимому, нет официального подхода или даже решения. Возможно, реальный вопрос заключается в том, как использовать компоненты React для использования. Как простой слой обзора (в значительной степени мое предложение номер один) или как реальные независимые и автономные компоненты?

Ответ 1

Если вы используете react-router, вы можете просто определить методы willTransitionTo в компонентах, которым передается объект Transition, который вы можете вызвать .wait on.

Не имеет значения, является ли renderToString синхронным, потому что обратный вызов Router.run не будет вызываться до тех пор, пока не будут разрешены все .wait ed promises, поэтому к моменту времени 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 (который описывает иерархию маршрутов) делится дословно с клиентом и сервером

Ответ 2

Я знаю, что это, вероятно, не совсем то, что вы хотите, и это может быть бессмысленным, но я помню, как с помощью небольшой модификации компонент обрабатывал оба:

  • на стороне сервера, при этом все исходное состояние уже получено, асинхронно, если необходимо)
  • рендеринг на стороне клиента, при необходимости 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} 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 ...)  

});

Мне жаль, что у меня нет точного кода, так что это может не сработать, но я публикую в интересах обсуждения.

Опять же, идея состоит в том, чтобы рассматривать большую часть компонента как тупой взгляд и обрабатывать данные как можно больше из компонента.

Ответ 3

Я действительно был испорчен сегодня, и хотя это не ответ на вашу проблему, я использовал этот подход. Я хотел использовать 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 });

Ответ 4

Я знаю, что этот вопрос задавали год назад, но у нас была та же проблема, и мы решаем его с вложенными 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);
    }
);

Ответ 5

Хотите поделиться с вами моим подходом на стороне сервера, используя Flux, немного упростить, например:

  • Скажем, мы имеем component с исходными данными из хранилища:

    class MyComponent extends Component {
      constructor(props) {
        super(props);
        this.state = {
          data: myStore.getData()
        };
      }
    }
    
  • Если для класса требуются некоторые предварительно загруженные данные для начального состояния, создайте Loader для MyComponent:

     class MyComponentLoader {
        constructor() {
            myStore.addChangeListener(this.onFetch);
        }
        load() {
            return new Promise((resolve, reject) => {
                this.resolve = resolve;
                myActions.getInitialData(); 
            });
        }
        onFetch = () => this.resolve(data);
    }
    
  • 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;
    }
    
  • Теперь просто загружайте данные в маршрутизатор:

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