Стратегії для серверного візуалізації асинхронно ініціалізованих компонентів 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. Як простий шар перегляду (майже моя пропозиція номер один) або як реальні незалежні та окремі компоненти?


Просто для того, щоб дістати речі: асинхронні дзвінки відбуватимуться і на стороні сервера? Я не розумію переваг у цьому випадку, на відміну від того, щоб зробити перегляд деякими частинами порожніми, і заповнити його по мірі надходження результатів асинхронної відповіді. Напевно, чогось не вистачає, вибачте!
фтрівер

Ви не повинні забувати, що в JavaScript навіть найпростіший запит до бази даних для отримання останніх публікацій є асинхронним. Тож якщо ви надаєте подання, вам доведеться почекати, поки дані будуть отримані з бази даних. І очевидні переваги від надання на сервері: наприклад, SEO. А також запобігає мерехтінню сторінки. Власне візуалізація на стороні сервера - це стандартний підхід, який більшість веб-сайтів все ще використовують.
tobik

Звичайно, але ви намагаєтеся вивести цілу сторінку (як тільки всі асинхронні запити DB відповіли)? У такому випадку я б наївно розділив їх як 1 / отримання всіх даних асинхронно 2 /, коли це буде зроблено, передати їх у "тупий" Реактивний вигляд та відповісти на запит. Або ти намагаєшся зробити рендеринг на стороні сервера, а потім на стороні клієнта з тим самим кодом (і вам потрібен код асинхронізації, щоб бути близьким до перегляду реагування?) Вибачте, якщо це звучить нерозумно, я просто не впевнений, що отримую що ти робиш.
phtrivier

Немає проблем, можливо, інші люди також мають проблеми з розумінням :) Що ви тільки що описали, це рішення номер два. Але візьмемо для прикладу компонент для коментарів із питання. У звичайному застосуванні на стороні клієнта я міг би зробити все в цьому компоненті (завантаження / додавання коментарів). Компонент буде відокремлений від зовнішнього світу, і зовнішній світ не повинен дбати про цей компонент. Це було б абсолютно незалежно і самостійно. Але як тільки я хочу ввести рендеринг на стороні сервера, мені доведеться обробляти асинхронні речі зовні. І це порушує весь принцип.
tobik

Щоб було зрозуміло, я не виступаю за використання волокон, а просто виконую всі виклики asyncs, і після того, як вони будуть закінчені (використовуючи обіцянки чи що завгодно), перенесіть компонент на сторону сервера. (Таким чином, реагують компоненти не будуть знати взагалі про асинхронних речах.) Тепер, це тільки думка, але я на самом деле , як ідея повного видалення все , що пов'язано з сервером зв'язку від React компонентів (які на насправді тільки тут , щоб зробити вигляд .) І я думаю, що це філософія реагування, яка може пояснити, чому те, що ти робиш, трохи складніше. У всякому разі, удачі :)
phtrivier

Відповіді:


15

Якщо ви використовуєте react-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Об'єкт (який описує ієрархію маршрутів) ділиться дослівно з клієнтом і сервером


Дякую. Вся справа в тому, що, наскільки я знаю, лише цей компонент маршруту підтримує цей willTransitionToметод. Що означає, що поки що неможливо написати повністю окремі компоненти для багаторазового використання, як той, який я описав у питанні. Але якщо ми не готові йти з Fibers, це, мабуть, найкращий і найбільш реагуючий спосіб здійснити візуалізацію на стороні сервера.
tobik

Це цікаво. Як може виглядати реалізація методу willTransitionTo із завантаженням даних про асинхронізацію?
Гіра

Ви отримаєте transitionоб'єкт як параметр, тому просто подзвоните transition.wait(yourPromise). Це, звичайно, означає, що вам потрібно реалізувати свій API, щоб підтримувати обіцянки. Ще одним недоліком такого підходу є те, що не існує простого способу реалізації "показника завантаження" на стороні клієнта. Перехід не перейде на компонент обробника маршруту, поки всі обіцянки не будуть вирішені.
tobik

Але я фактично не впевнений у підході "щойно". Кілька вкладених обробників маршрутів можуть збігатися з однією URL-адресою, що означає, що декілька обіцянок доведеться вирішити. Немає гарантії, що всі вони закінчаться одночасно. Якщо магазини одинакові, це може спричинити конфлікти. @Esailija ви могли б трохи пояснити свою відповідь?
tobik

У мене є автоматична сантехніка, яка збирає всі обіцянки, що .waitedстосуються переходу. Після того, як всі вони виконані, .runвикликається зворотний дзвінок. Незадовго до того, як .render()я збираю всі дані разом із обіцянок і встановлюю стан магазину singelton, тоді в наступному рядку після виклику візуалізації я ініціалізую назад одноразові магазини. Це досить хакі, але все відбувається автоматично, і код компонента та магазину програми залишається практично однаковим.
Есаїлія

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

});

Вибачте, у мене немає точного коду під рукою, тому це може не вийти з коробки, але я публікую повідомлення з метою обговорення.

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


1
Дякую. Я розумію, але це дійсно не те, що я хочу. Скажімо, я хочу створити якийсь більш складний веб-сайт за допомогою React, наприклад bbc.com . Переглядаючи сторінку, я можу побачити "компоненти" скрізь. Секція (спорт, бізнес ...) - типова складова. Як би ти це реалізував? Де ви б попередньо вилучили всі дані? Щоб створити такий складний сайт, компоненти (як правило, як маленькі контейнери MVC) є дуже хорошим (якщо, можливо, єдиним) шляхом. Компонентний підхід є загальним для багатьох типових систем на сервері. Питання: чи можу я використовувати для цього React?
tobik

Ви попередньо виберіть дані на стороні сервера (як це, мабуть, робиться в цьому випадку, перш ніж передавати їх у "традиційну" систему шаблонів на стороні сервера); тільки тому, що відображення даних має вигоду від того, що вони є модульними, чи означає це обчислення даних, необхідних для того, щоб дотримуватися тієї самої структури? Тут я трохи граю в захисника диявола, у мене були ті ж проблеми, що і у вас, коли ви перевіряєте ом. І я сподіваюся, що хтось має більше розумінь щодо цього, ніж я, - безперебійно складання матеріалів з будь-якої сторони дроту допоможе дуже багато.
phtrivier

1
Під тим, де я маю на увазі, де в коді. У контролері? Тож метод контролера, що обробляє домашню сторінку bbc, містив би десяток подібних запитів, для кожного розділу один? Це імхо шлях до пекла. Так що так, я робити думаю , що розрахунок повинен бути модульним , а також. Все упаковано в один компонент, в один контейнер MVC. Ось так я розробляю стандартні додатки на сервері, і я майже впевнений, що такий підхід хороший. І тому я дуже в захваті від React.js, це те, що існує великий потенціал для використання такого підходу як на стороні клієнта, так і на сервері для створення дивовижних ізоморфних додатків.
tobik

1
На будь-якому веб-сайті (великому / маленькому) вам потрібно лише надати на сторону сервера поточну сторінку (SSR) зі своїм станом init; вам не потрібно стан init для кожної сторінки. Сервер захоплює стан init, надає його та передає клієнту <script type=application/json>{initState}</script>; таким чином дані будуть у HTML. Регідратація / прив'язування подій інтерфейсу користувача до сторінки, зателефонувавши до рендерінгу на клієнта. Наступні сторінки створюються за допомогою js-коду клієнта (отримання даних за потребою) та надаються клієнтом. Таким чином, будь-яке оновлення завантажить свіжі сторінки SSR, а натискання на сторінку буде CSR. = isomorphic & SEO friendly
Федеріко,

0

Сьогодні я з цим дуже заплутався, і хоча це не є відповіддю на вашу проблему, я використовував такий підхід. Я хотів використовувати 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

Я знаю, що це питання було задано рік тому, але у нас була та сама проблема, і ми вирішуємо її за допомогою вкладених обіцянок, які були отримані з компонентів, які збираються винести. Врешті-решт, ми отримали всі дані для програми та просто надіслали їх.

Наприклад:

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

Хочете поділитися з вами своїм підходом до надання серверної сторони 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

як короткий збір -> GraphQL вирішить це цілком для вашої стеки ...

  • додати GraphQL
  • використовувати аполлон та реагувати-аполлон
  • перед початком візуалізації використовуйте "getDataFromTree"

-> getDataFromTree автоматично знайде всі залучені запити у вашій програмі та виконає їх, виклавши кеш apollo на сервер і, таким чином, увімкнувши повністю працюючий SSR .. BÄM

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.