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

100

Одно из самых больших преимуществ 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 для использования. Как простой слой обзора (в значительной степени мое предложение номер один) или как реальные независимые и автономные компоненты?

  • 0
    Просто чтобы понять: асинхронные вызовы будут происходить и на стороне сервера? Я не понимаю преимуществ в этом случае по сравнению с рендерингом представления с некоторыми оставленными пустыми частями и заполнением его по мере получения результатов асинхронного ответа. Наверное, что-то упустил, извините!
  • 0
    Вы не должны забывать, что в JavaScript даже самый простой запрос к базе данных для получения последних сообщений является асинхронным. Поэтому, если вы визуализируете представление, вам нужно подождать, пока данные не будут извлечены из базы данных. И у рендеринга на стороне сервера есть очевидные преимущества: например, SEO. А также это предотвращает мерцание страницы. На самом деле рендеринг на стороне сервера - это стандартный подход, который до сих пор используется большинством веб-сайтов.
Показать ещё 11 комментариев
Теги:
flux
reactjs-flux

5 ответов

14

Если вы используете 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 (который описывает иерархию маршрутов) делится дословно с клиентом и сервером

  • 0
    Благодарю. Дело в том, что, насколько я знаю, только компоненты маршрута поддерживают этот метод willTransitionTo . Это означает, что все еще невозможно написать полностью автономные компоненты многократного использования, подобные тому, который я описал в этом вопросе. Но если мы не готовы идти с волокнам, это, вероятно , самый лучший и самый реагировать способ реализовать серверный рендеринг.
  • 0
    Это интересно. Как будет выглядеть реализация метода willTransitionTo для загрузки асинхронных данных?
Показать ещё 11 комментариев
0

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

Я знаю, что этот вопрос задавали год назад, но у нас была та же проблема, и мы решаем его с вложенными 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);
    }
);
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 });
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} 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 для этого?
  • 0
    Вы будете предварительно извлекать данные на стороне сервера (как это, вероятно, и делается в этом случае, прежде чем передавать их в «традиционную» систему шаблонов на стороне сервера); только потому, что отображение данных выигрывает от модульности, означает ли это, что вычисление данных обязательно должно следовать той же структуре? Я немного играю адвокат дьявола, у меня были те же проблемы, что и у вас при проверке ом. И я очень надеюсь, что у кого-то есть более глубокие идеи по этому вопросу, чем у меня - плавное написание материала на любой стороне провода очень помогло бы.
Показать ещё 2 комментария

Ещё вопросы

Сообщество Overcoder
Наверх
Меню