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

114

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

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

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

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

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

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

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

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

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

тобик
источник
Просто чтобы понять: асинхронные вызовы будут происходить и на стороне сервера? Я не понимаю преимуществ в этом случае по сравнению с рендерингом представления с некоторыми частями, оставленными пустыми, и заполнением его по мере поступления результатов асинхронного ответа. Вероятно, чего-то не хватает, извините!
phtrivier
Вы не должны забывать, что в JavaScript даже самый простой запрос к базе данных для получения последних сообщений является асинхронным. Поэтому, если вы визуализируете представление, вам нужно подождать, пока данные не будут извлечены из базы данных. И есть очевидные преимущества рендеринга на стороне сервера: например, SEO. А также предотвращает мерцание страницы. На самом деле рендеринг на стороне сервера - это стандартный подход, который все еще используют большинство веб-сайтов.
tobik
Конечно, но пытаетесь ли вы отобразить всю страницу (после того, как все асинхронные запросы базы данных ответят)? В этом случае я бы наивно разделил это как 1 / получение всех данных асинхронно 2 / по завершении передать их в «тупой» React View и ответить на запрос. Или вы пытаетесь выполнить рендеринг на стороне сервера, а затем на стороне клиента с тем же кодом (и вам нужен асинхронный код, чтобы он был близок к представлению реакции?) Извините, если это звучит глупо, я просто не уверен, что понимаю что ты делаешь.
phtrivier
Нет проблем, возможно, у других людей тоже есть проблемы с пониманием :) То, что вы только что описали, является решением номер два. Но возьмем, к примеру, компонент для комментирования вопроса. В обычном клиентском приложении я мог делать все в этом компоненте (загружать / добавлять комментарии). Компонент будет отделен от внешнего мира, и внешний мир не будет заботиться об этом компоненте. Он был бы полностью независимым и автономным. Но как только я хочу представить рендеринг на стороне сервера, мне придется обрабатывать асинхронные вещи снаружи. И это нарушает весь принцип.
tobik
Чтобы быть ясным, я не защищаю использование волокон, а просто выполняю все асинхронные вызовы, и после того, как все они завершены (с помощью обещания или чего-то еще), визуализируйте компонент на стороне сервера. (Таким образом, реагируют компоненты не будут знать вообще о асинхронных вещах.) Теперь, это только мнение, но я на самом деле , как идея полного удаления все , что связанно с сервером связи от React компонентов (которые на самом деле только здесь , чтобы сделать вид .) И я думаю, что это философия реакции, которая может объяснить, почему то, что вы делаете, немного сложно. В любом случае, удачи :)
phtrivier

Ответы:

15

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

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

Esailija
источник
Спасибо. Дело в том, что, насколько мне известно, этот willTransitionToметод поддерживают только компоненты маршрута . Это означает, что по-прежнему невозможно написать полностью автономные повторно используемые компоненты, подобные тому, который я описал в вопросе. Но если мы не готовы идти с волокнам, это, вероятно , самый лучший и самый реагировать способ реализовать серверный рендеринг.
tobik
Это интересно. Как будет выглядеть реализация метода willTransitionTo при загрузке асинхронных данных?
Hyra
Вы получите transitionобъект в качестве параметра, поэтому вы просто вызовете transition.wait(yourPromise). Это, конечно, означает, что вы должны реализовать свой API для поддержки обещаний. Еще один недостаток этого подхода состоит в том, что нет простого способа реализовать «индикатор загрузки» на стороне клиента. Переход не переключится на компонент обработчика маршрута, пока не будут выполнены все обещания.
tobik
Но на самом деле я не уверен в подходе «точно в срок». Несколько вложенных обработчиков маршрутов могут соответствовать одному URL-адресу, что означает, что необходимо будет выполнить несколько обещаний. Нет никакой гарантии, что все они закончатся одновременно. Если магазины одиночные, это может вызвать конфликты. @Esailija, может, ты немного объяснишь свой ответ?
tobik
У меня есть автоматическая сантехника, которая собирает все обещания, необходимые .waitedдля перехода. Как только все они выполнены, .runвызывается обратный вызов. Непосредственно перед тем, как .render()я собираю все данные из обещаний и устанавливаю состояния хранилища синглтонов, на следующей строке после вызова рендеринга я снова инициализирую хранилища синглтонов. Это довольно хитроумно, но все происходит автоматически, а код компонента и приложения магазина остается практически неизменным.
Esailija
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 ...)  

});

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

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

phtrivier
источник
1
Спасибо. Я понимаю, но на самом деле это не то, что мне нужно. Допустим, я хочу создать более сложный веб-сайт с использованием React, например bbc.com . Глядя на страницу, я везде вижу «компоненты». Раздел (спорт, бизнес ...) - типичный компонент. Как бы вы это реализовали? Где бы вы предварительно загрузили все данные? Для разработки такого сложного сайта компоненты (в принципе, такие как маленькие контейнеры MVC) - очень хороший (если, может быть, единственный) способ. Компонентный подход является общим для многих типичных серверных фреймворков. Вопрос в том, могу ли я использовать для этого React?
tobik
Вы предварительно загрузите данные на стороне сервера (как это, вероятно, делается в этом случае, прежде чем передать их в «традиционную» систему шаблонов на стороне сервера); только потому, что отображение данных выигрывает от модульности, означает ли это, что вычисление данных обязательно должно следовать одной и той же структуре? Я здесь немного играю в адвоката дьявола, у меня были те же проблемы, что и у вас, когда проверяли ОМ. И я очень надеюсь, что у кого-то есть больше идей по этому поводу, чем у меня - бесшовная компоновка материала на любой стороне провода очень поможет.
phtrivier
1
Под «где» я подразумеваю, где именно в коде. В контроллере? Значит, метод контроллера, обрабатывающий домашнюю страницу bbc, будет содержать примерно дюжину похожих запросов для каждого раздела? Это, имхо, путь в ад. Так что да, я считаю, что вычисления тоже должны быть модульными. Все упаковано в один компонент, в один контейнер MVC. Вот как я разрабатываю стандартные серверные приложения, и я вполне уверен, что этот подход хорош. И причина, по которой я так взволнован React.js, заключается в том, что есть большой потенциал для использования этого подхода как на стороне клиента, так и на стороне сервера для создания потрясающих изоморфных приложений.
tobik
1
На любом сайте (большом / маленьком) вам нужно только выполнить рендеринг на стороне сервера (SSR) текущей страницы с ее состоянием инициализации; вам не нужно состояние инициализации для каждой страницы. Сервер получает состояние инициализации, отображает его и передает клиенту <script type=application/json>{initState}</script>; таким образом данные будут в HTML. Повторная гидратация / привязка событий пользовательского интерфейса к странице путем вызова render на клиенте. Последующие страницы создаются клиентским js-кодом (при необходимости извлекают данные) и обрабатываются клиентом. Таким образом, при любом обновлении будут загружены свежие страницы SSR, а нажатие на страницу будет CSR. = изоморфный и оптимизированный для SEO
Федерико
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 });
svnm
источник
0

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

Например:

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);
    }
);
Rotem
источник
0

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

  1. Допустим, у нас есть componentисходные данные из магазина:

    class MyComponent extends Component {
      constructor(props) {
        super(props);
        this.state = {
          data: myStore.getData()
        };
      }
    }
  2. Если классу требуются некоторые предварительно загруженные данные для начального состояния, давайте создадим загрузчик для 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

так же, как короткий набор -> GraphQL решит эту проблему полностью для вашего стека ...

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

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

Джебби
источник