Stratégies pour le rendu côté serveur des composants React.js initialisés de manière asynchrone


114

L'un des plus grands avantages de React.js est censé être le rendu côté serveur . Le problème est que la fonction clé React.renderComponentToString()est synchrone, ce qui rend impossible le chargement de données asynchrones lorsque la hiérarchie des composants est rendue sur le serveur.

Disons que j'ai un composant universel pour les commentaires que je peux déposer à peu près n'importe où sur la page. Il n'a qu'une seule propriété, une sorte d'identifiant (par exemple l'id d'un article sous lequel les commentaires sont placés), et tout le reste est géré par le composant lui-même (chargement, ajout, gestion des commentaires).

J'aime beaucoup l' architecture Flux car elle facilite beaucoup de choses et ses magasins sont parfaits pour partager l'état entre le serveur et le client. Une fois que mon magasin contenant des commentaires est initialisé, je peux simplement le sérialiser et l'envoyer du serveur au client où il est facilement restauré.

La question est de savoir quelle est la meilleure façon de peupler mon magasin. Au cours des derniers jours, j'ai beaucoup cherché sur Google et je suis tombé sur quelques stratégies, dont aucune ne me paraissait vraiment bonne compte tenu de la "promotion" de cette fonctionnalité de React.

  1. À mon avis, le moyen le plus simple est de peupler tous mes magasins avant le début du rendu réel. Cela signifie quelque part en dehors de la hiérarchie des composants (accroché à mon routeur par exemple). Le problème avec cette approche est que je devrais définir à peu près deux fois la structure de la page. Prenons une page plus complexe, par exemple une page de blog avec de nombreux composants différents (article de blog réel, commentaires, articles associés, articles les plus récents, flux Twitter ...). Je devrais concevoir la structure de la page à l'aide des composants React, puis ailleurs, je devrais définir le processus de remplissage de chaque magasin requis pour cette page actuelle. Cela ne me semble pas être une bonne solution. Malheureusement, la plupart des didacticiels isomorphes sont conçus de cette façon (par exemple ce grand didacticiel sur les flux ).

  2. Réagissez-async . Cette approche est parfaite. Cela me permet simplement de définir dans une fonction spéciale dans chaque composant comment initialiser l'état (peu importe que ce soit de manière synchrone ou asynchrone) et ces fonctions sont appelées lorsque la hiérarchie est rendue en HTML. Cela fonctionne de telle sorte qu'un composant n'est pas rendu tant que l'état n'est pas complètement initialisé. Le problème est qu'il nécessite des fibresqui est, pour autant que je sache, une extension Node.js qui modifie le comportement JavaScript standard. Bien que j'aime beaucoup le résultat, il me semble toujours qu'au lieu de trouver une solution, nous avons changé les règles du jeu. Et je pense que nous ne devrions pas être obligés de faire cela pour utiliser cette fonctionnalité de base de React.js. Je ne suis pas non plus sûr du support général de cette solution. Est-il possible d'utiliser la fibre sur l'hébergement Web standard Node.js?

  3. Je réfléchissais un peu par moi-même. Je n'ai pas vraiment réfléchi aux détails de l'implémentation, mais l'idée générale est que j'étendrais les composants de la même manière que React-async, puis j'appellerais à plusieurs reprises React.renderComponentToString () sur le composant racine. Au cours de chaque passage, je collectais les rappels étendus, puis je les appelais au et du passage pour peupler les magasins. Je répéterais cette étape jusqu'à ce que tous les magasins requis par la hiérarchie actuelle des composants soient remplis. Il y a beaucoup de choses à résoudre et je ne suis particulièrement pas sûr de la performance.

Ai-je oublié quelque chose? Existe-t-il une autre approche / solution? En ce moment, je pense aller dans le sens de la réaction asynchrone / fibres mais je n'en suis pas complètement sûr, comme expliqué dans le deuxième point.

Discussion connexe sur GitHub . Apparemment, il n'y a pas d'approche officielle ni même de solution. Peut-être que la vraie question est de savoir comment les composants React sont destinés à être utilisés. Comme une simple couche de vue (à peu près ma suggestion numéro un) ou comme de vrais composants indépendants et autonomes?


Juste pour obtenir des choses: les appels asynchrones se produiraient aussi du côté serveur? Je ne comprends pas les avantages dans ce cas par rapport au rendu de la vue avec certaines parties laissées vides et à le remplir lorsque les résultats d'une réponse asynchrone arrivent. Il manque probablement quelque chose, désolé!
phtrivier le

Vous ne devez pas oublier qu'en JavaScript, même la requête la plus simple à la base de données pour récupérer les derniers articles est asynchrone. Donc, si vous affichez une vue, vous devez attendre que les données soient extraites de la base de données. Et il y a des avantages évidents au rendu côté serveur: SEO par exemple. Et cela empêche également la page de scintiller. En fait, le rendu côté serveur est l'approche standard que la plupart des sites Web utilisent encore.
tobik le

Bien sûr, mais essayez-vous de rendre la page entière (une fois que toutes les requêtes de base de données asynchrones ont répondu)? Dans ce cas, je l'aurais naïvement séparé en 1 / récupérer toutes les données de manière asynchrone 2 / une fois terminé, le passer à une vue React "stupide", et répondre à la demande. Ou essayez-vous de faire à la fois le rendu côté serveur, puis côté client avec le même code (et vous avez besoin que le code async soit proche de la vue de réaction?) Désolé si cela semble idiot, je ne suis pas sûr de comprendre que fais tu.
phtrivier

Pas de problème, peut-être que d'autres personnes ont aussi des problèmes à comprendre :) Ce que vous venez de décrire est la solution numéro deux. Mais prenez par exemple le composant pour commenter la question. Dans une application courante côté client, je pourrais tout faire dans ce composant (chargement / ajout de commentaires). Le composant serait séparé du monde extérieur et le monde extérieur n'aurait pas à se soucier de ce composant. Ce serait complètement indépendant et autonome. Mais une fois que je veux introduire le rendu côté serveur, je dois gérer les choses asynchrones à l'extérieur. Et cela brise tout le principe.
tobik le

Pour être clair, je ne préconise pas d'utiliser des fibres, mais juste de faire tous les appels asynchrones, et une fois qu'ils sont tous terminés (en utilisant la promesse ou autre), effectuez le rendu du composant côté serveur. (Donc , REACT composants ne sais pas du tout sur les choses asynchrones.) Maintenant, qui est seulement une opinion, mais je fait comme l'idée de supprimer complètement tout ce qui concerne la communication de serveur à partir React composants (qui sont vraiment là que pour rendre la vue .) Et je pense que c'est la philosophie derrière react, ce qui pourrait expliquer pourquoi ce que vous faites est un peu compliqué. Bref, bonne chance :)
phtrivier

Réponses:


15

Si vous utilisez react-router , vous pouvez simplement définir une willTransitionTométhode dans les composants, qui reçoit un Transitionobjet que vous pouvez appeler .wait.

Peu importe si renderToString est synchrone, car le rappel vers Router.runne sera pas appelé tant que toutes les .waitpromesses ed ne seront pas résolues, donc au moment où il renderToStringsera appelé dans le middleware, vous auriez pu remplir les magasins. Même si les magasins sont des singletons, vous pouvez simplement définir leurs données temporairement juste à temps avant l'appel de rendu synchrone et le composant le verra.

Exemple de middleware:

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);
        });
    };
};

L' routesobjet (qui décrit la hiérarchie des routes) est partagé textuellement avec le client et le serveur


Merci. Le fait est que pour autant que je sache, seuls les composants d'itinéraire prennent en charge cette willTransitionTométhode. Ce qui signifie qu'il n'est toujours pas possible d'écrire des composants réutilisables complètement autonomes comme celui que j'ai décrit dans la question. Mais à moins que nous ne soyons disposés à utiliser Fibers, c'est probablement le moyen le meilleur et le plus réactif pour implémenter le rendu côté serveur.
tobik

C'est intéressant. À quoi ressemblerait une implémentation de la méthode willTransitionTo pour charger des données asynchrones?
Hyra

Vous obtiendrez l' transitionobjet en tant que paramètre, vous appellerez donc simplement transition.wait(yourPromise). Cela signifie bien sûr que vous devez implémenter votre API pour tenir les promesses. Un autre inconvénient de cette approche est qu'il n'y a pas de moyen simple d'implémenter un "indicateur de chargement" côté client. La transition ne passera pas au composant de gestionnaire d'itinéraire tant que toutes les promesses ne seront pas résolues.
tobik

Mais en fait, je ne suis pas sûr de l'approche «juste à temps». Plusieurs gestionnaires d'itinéraires imbriqués peuvent correspondre à une URL, ce qui signifie que plusieurs promesses devront être résolues. Il n'y a aucune garantie qu'ils finiront tous en même temps. Si les magasins sont des singletons, cela peut provoquer des conflits. @Esailija pourrais-tu peut-être expliquer un peu ta réponse?
tobik

J'ai mis en place une plomberie automatique qui recueille toutes les promesses que .waitedpour une transition. Une fois que tous sont remplis, le .runrappel est appelé. Juste avant de .render()collecter toutes les données des promesses et de définir les états du magasin singleton, puis sur la ligne suivante après l'appel de rendu, je réinitialise les magasins singleton. C'est assez piraté mais tout se passe automatiquement et le code du composant et de l'application de magasin reste pratiquement le même.
Esailija

0

Je sais que ce n'est probablement pas exactement ce que vous voulez, et cela n'a peut-être pas de sens, mais je me souviens de m'être débrouillé en modifiant légèrement le composant pour gérer les deux:

  • rendu côté serveur, avec tout l'état initial déjà récupéré, de manière asynchrone si nécessaire)
  • rendu côté client, avec ajax si besoin

Donc quelque chose comme:

/** @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 ...)  

});

Je suis désolé de ne pas avoir le code exact sous la main, donc cela pourrait ne pas fonctionner immédiatement, mais je publie dans l'intérêt de la discussion.

Encore une fois, l'idée est de traiter la plupart des composants comme une vue stupide, et de traiter l' extraction de données, autant que possible hors du composant.


1
Je vous remercie. Je comprends l'idée, mais ce n'est vraiment pas ce que je veux. Disons que je veux créer un site Web plus complexe en utilisant React, comme bbc.com . En regardant la page, je peux voir des "composants" partout. Une section (sport, affaires ...) est une composante typique. Comment le mettriez-vous en œuvre? Où préférez-vous extraire toutes les données? Pour concevoir un site aussi complexe, les composants (en principe, comme les petits conteneurs MVC) sont une très bonne solution (si peut-être la seule). L'approche des composants est commune à de nombreux frameworks côté serveur typiques. La question est: puis-je utiliser React pour cela?
tobik

Vous allez pré-extraire les données côté serveur (comme c'est probablement le cas dans ce cas, avant de les transmettre à un système de modèle côté serveur "traditionnel"); simplement parce que l' affichage des données a l'avantage d'être modulaire, cela signifie-t-il que le calcul des données doit nécessairement suivre la même structure? Je joue un peu l'avocat du diable ici, j'ai eu le même problème que vous en vérifiant om. Et j'espère vraiment que quelqu'un a plus d'informations à ce sujet que moi - composer de manière transparente des choses de n'importe quel côté du fil aiderait beaucoup.
phtrivier

1
Par où je veux dire où dans le code. Dans le contrôleur? Donc, la méthode du contrôleur gérant la page d'accueil de la BBC contiendrait une douzaine de requêtes similaires, pour chaque section? C'est à mon humble avis un chemin vers l'enfer. Alors oui, je ne pense que le calcul doit être modulaire ainsi. Tout est emballé dans un seul composant, dans un seul conteneur MVC. C'est ainsi que je développe des applications standard côté serveur et je suis assez convaincu que cette approche est bonne. Et la raison pour laquelle je suis si enthousiasmé par React.js est qu'il existe un grand potentiel pour utiliser cette approche à la fois du côté client et du côté serveur pour créer de superbes applications isomorphes.
tobik

1
Sur n'importe quel site (grand / petit), il vous suffit de rendre côté serveur (SSR) la page actuelle avec son état initial; vous n'avez pas besoin de l'état d'initialisation pour chaque page. Le serveur saisit l'état initial, le restitue et le transmet au client <script type=application/json>{initState}</script>; de cette façon, les données seront dans le HTML. Réhydratez / liez les événements de l'interface utilisateur à la page en appelant render sur le client. Les pages suivantes sont créées par le code js du client (récupérant les données selon les besoins) et rendues par le client. De cette façon, toute actualisation chargera de nouvelles pages SSR et cliquer sur une page sera CSR. = isomorphe & SEO friendly
Federico

0

J'ai été vraiment dérangé avec cela aujourd'hui, et bien que ce ne soit pas une réponse à votre problème, j'ai utilisé cette approche. Je voulais utiliser Express pour le routage plutôt que React Router, et je ne voulais pas utiliser Fibers car je n'avais pas besoin de support de threading dans le nœud.

Je viens donc de décider que pour les données initiales qui doivent être rendues au magasin de flux lors du chargement, je vais effectuer une requête AJAX et transmettre les données initiales au magasin

J'utilisais Fluxxor pour cet exemple.

Donc sur mon itinéraire express, dans ce cas un /productsitinéraire:

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

Ensuite, ma méthode de rendu initialize qui transmet les données au magasin.

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

Je sais que cette question a été posée il y a un an mais nous avons eu le même problème et nous le résolvons avec des promesses imbriquées dérivées des composants qui vont être rendus. En fin de compte, nous avons eu toutes les données de l'application et nous l'avons simplement envoyée.

Par exemple:

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

    }

});

et dans le routeur

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

Je veux partager avec vous mon approche du rendu côté serveur en utilisant Flux, peu être simplifié par exemple:

  1. Disons que nous avons componentavec les données initiales du magasin:

    class MyComponent extends Component {
      constructor(props) {
        super(props);
        this.state = {
          data: myStore.getData()
        };
      }
    }
  2. Si la classe nécessite des données préchargées pour l'état initial, créons Loader pour MyComponent:

     class MyComponentLoader {
        constructor() {
            myStore.addChangeListener(this.onFetch);
        }
        load() {
            return new Promise((resolve, reject) => {
                this.resolve = resolve;
                myActions.getInitialData(); 
            });
        }
        onFetch = () => this.resolve(data);
    }
  3. Boutique:

    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. Maintenant, chargez simplement les données dans le routeur:

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

0

tout comme un bref rollup -> GraphQL résoudra cela entièrement pour votre pile ...

  • ajouter GraphQL
  • utiliser apollo et react-apollo
  • utilisez "getDataFromTree" avant de commencer le rendu

-> getDataFromTree trouvera automatiquement toutes les requêtes impliquées dans votre application et les exécutera, plaçant votre cache apollo sur le serveur et activant ainsi le SSR pleinement opérationnel. BÄM

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.