it-swarm.com.ru

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

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

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

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

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

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

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

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

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

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

111
tobik

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

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

14
Esailija

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

Затем мой метод визуализации инициализации, который передает данные в хранилище.

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
steven iseki

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

Например:

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
Rotem

просто как короткий накопительный пакет -> GraphQL решит это для вашего стека ...

  • добавить GraphQL
  • использовать Аполлон и реагировать-Аполлон
  • используйте "getDataFromTree" перед началом рендеринга

-> getDataFromTree автоматически найдет все вовлеченные запросы в вашем приложении и выполнит их, накапливая ваш кеш apollo на сервере и, таким образом, обеспечивая полностью работающий SSR .. BÄM

0
jebbie

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

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

});

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

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

0
phtrivier

Хочу поделиться с вами моим подходом рендеринга на стороне сервера с использованием 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/>;
    });
    
0
zooblin