Strategien für das serverseitige Rendern von asynchron initialisierten React.js-Komponenten

114

Einer der größten Vorteile von React.js soll das serverseitige Rendern sein . Das Problem ist, dass die Schlüsselfunktion React.renderComponentToString()synchron ist, wodurch es unmöglich wird, asynchrone Daten zu laden, wenn die Komponentenhierarchie auf dem Server gerendert wird.

Angenommen, ich habe eine universelle Komponente zum Kommentieren, die ich praktisch überall auf der Seite ablegen kann. Es hat nur eine Eigenschaft, eine Art Bezeichner (z. B. die ID eines Artikels, unter dem die Kommentare platziert sind), und alles andere wird von der Komponente selbst verwaltet (Laden, Hinzufügen, Verwalten von Kommentaren).

Ich mag die Flux- Architektur sehr, weil sie viele Dinge viel einfacher macht und ihre Geschäfte perfekt sind, um den Status zwischen Server und Client zu teilen. Sobald mein Geschäft mit Kommentaren initialisiert ist, kann ich es einfach serialisieren und vom Server an den Client senden, wo es leicht wiederhergestellt werden kann.

Die Frage ist, wie ich meinen Laden am besten bevölkern kann. In den letzten Tagen habe ich viel gegoogelt und bin auf einige Strategien gestoßen, von denen keine wirklich gut schien, wenn man bedenkt, wie sehr diese Funktion von React "beworben" wird.

  1. Meiner Meinung nach ist es am einfachsten, alle meine Geschäfte zu füllen, bevor das eigentliche Rendern beginnt. Das heißt, irgendwo außerhalb der Komponentenhierarchie (zum Beispiel an meinen Router angeschlossen). Das Problem bei diesem Ansatz ist, dass ich die Seitenstruktur ziemlich zweimal definieren müsste. Stellen Sie sich eine komplexere Seite vor, beispielsweise eine Blog-Seite mit vielen verschiedenen Komponenten (tatsächlicher Blog-Beitrag, Kommentare, verwandte Beiträge, neueste Beiträge, Twitter-Stream ...). Ich müsste die Seitenstruktur mithilfe von React-Komponenten entwerfen und dann an einer anderen Stelle den Prozess des Auffüllens jedes erforderlichen Speichers für diese aktuelle Seite definieren. Das scheint mir keine gute Lösung zu sein. Leider sind die meisten isomorphen Tutorials so konzipiert (zum Beispiel dieses großartige Flux-Tutorial ).

  2. Reagieren Sie asynchron . Dieser Ansatz ist perfekt. Ich kann einfach in einer speziellen Funktion in jeder Komponente definieren, wie der Status initialisiert werden soll (egal ob synchron oder asynchron), und diese Funktionen werden aufgerufen, wenn die Hierarchie in HTML gerendert wird. Es funktioniert so, dass eine Komponente erst gerendert wird, wenn der Status vollständig initialisiert ist. Das Problem ist, dass Fasern erforderlich sindSoweit ich weiß, handelt es sich um eine Node.js-Erweiterung, die das Standardverhalten von JavaScript ändert. Obwohl mir das Ergebnis wirklich gefällt, scheint es mir immer noch, dass wir, anstatt eine Lösung zu finden, die Spielregeln geändert haben. Und ich denke, wir sollten nicht dazu gezwungen werden, um diese Kernfunktion von React.js zu nutzen. Ich bin mir auch nicht sicher über die allgemeine Unterstützung dieser Lösung. Ist es möglich, Fibre auf Standard-Webhosting von Node.j zu verwenden?

  3. Ich habe ein wenig alleine nachgedacht. Ich habe nicht wirklich über die Implementierungsdetails nachgedacht, aber die allgemeine Idee ist, dass ich die Komponenten ähnlich wie React-async erweitern und dann wiederholt React.renderComponentToString () für die Stammkomponente aufrufen würde. Während jedes Durchgangs sammelte ich die erweiterten Rückrufe und rief sie dann am und des Durchgangs an, um die Geschäfte zu füllen. Ich würde diesen Schritt wiederholen, bis alle für die aktuelle Komponentenhierarchie erforderlichen Speicher gefüllt sind. Es gibt viele Dinge zu lösen und ich bin mir über die Leistung besonders unsicher.

Habe ich etwas verpasst? Gibt es einen anderen Ansatz / eine andere Lösung? Im Moment denke ich darüber nach, asynchron zu reagieren / Fasern zu gehen, bin mir aber nicht ganz sicher, wie im zweiten Punkt erläutert.

Verwandte Diskussion auf GitHub . Anscheinend gibt es keinen offiziellen Ansatz oder gar eine Lösung. Vielleicht ist die eigentliche Frage, wie die React-Komponenten verwendet werden sollen. Wie eine einfache Ansichtsebene (so ziemlich mein Vorschlag Nummer eins) oder wie echte unabhängige und eigenständige Komponenten?

tobik
quelle
Nur um Dinge zu bekommen: Die asynchronen Aufrufe würden auch auf der Serverseite stattfinden? Ich verstehe die Vorteile in diesem Fall nicht, anstatt die Ansicht mit einigen leeren Teilen zu rendern und sie zu füllen, wenn die Ergebnisse der asynchronen Antwort eintreffen. Vermutlich fehlt etwas, sorry!
Phtrivier
Sie dürfen nicht vergessen, dass in JavaScript selbst die einfachste Abfrage an die Datenbank zum Abrufen der neuesten Beiträge asynchron ist. Wenn Sie also eine Ansicht rendern, müssen Sie warten, bis die Daten aus der Datenbank abgerufen wurden. Und das Rendern auf der Serverseite bietet offensichtliche Vorteile: SEO zum Beispiel. Außerdem wird verhindert, dass die Seite flackert. Tatsächlich ist das serverseitige Rendern der Standardansatz, den die meisten Websites noch verwenden.
Tobik
Sicher, aber versuchen Sie, die gesamte Seite zu rendern (sobald alle asynchronen Datenbankabfragen geantwortet haben)? In diesem Fall hätte ich es naiv als 1 / asynchrones Abrufen aller Daten 2 / getrennt, wenn es fertig wäre, an eine "dumme" Reaktionsansicht übergeben und auf die Anfrage geantwortet. Oder versuchen Sie, sowohl serverseitiges als auch clientseitiges Rendern mit demselben Code durchzuführen (und Sie benötigen den asynchronen Code, um in der Nähe der Reaktionsansicht zu sein?). Entschuldigung, wenn das albern klingt, bin ich mir einfach nicht sicher, ob ich das bekomme was tust du.
Phtrivier
Kein Problem, vielleicht haben auch andere Leute Probleme zu verstehen :) Was Sie gerade beschrieben haben, ist die Lösung Nummer zwei. Nehmen Sie zum Beispiel die Komponente zum Kommentieren aus der Frage. In einer gängigen clientseitigen Anwendung konnte ich alles in dieser Komponente tun (Laden / Hinzufügen von Kommentaren). Die Komponente würde von der Außenwelt getrennt sein und die Außenwelt müsste sich nicht um diese Komponente kümmern. Es wäre völlig unabhängig und eigenständig. Aber sobald ich das serverseitige Rendern einführen möchte, muss ich die asynchronen Dinge draußen erledigen. Und das bricht das ganze Prinzip.
Tobik
Um ganz klar zu sein, ich befürworte nicht die Verwendung von Fasern, sondern nur die Ausführung aller asynchronen Aufrufe. Wenn alle abgeschlossen sind (mit Versprechen oder was auch immer), wird die Komponente auf der Serverseite gerendert . (Also die reagieren Komponenten würden nicht wissen , überhaupt über die asynchronen Sachen.) Nun, die nur eine Meinung, aber mir wirklich wie die Idee völlig alles zu Server - Kommunikation im Zusammenhang Entfernen von React Komponenten (die hier wirklich nur um die Ansicht zu machen .) Und ich denke, das ist die Philosophie hinter der Reaktion, die erklären könnte, warum das, was Sie tun, etwas kompliziert ist. Wie auch immer, viel Glück :)
Phtrivier

Antworten:

15

Wenn du benutzt React-Router verwenden , können Sie einfach eine willTransitionToMethode in Komponenten definieren, die ein TransitionObjekt übergibt, das Sie aufrufen können .wait.

Es spielt keine Rolle, ob renderToString synchron ist, da der Rückruf an Router.runerst aufgerufen wird, wenn alle .waitEd-Versprechen gelöst sind. Wenn also die Zeit renderToStringin der Middleware aufgerufen wird, könnten Sie die Stores gefüllt haben. Selbst wenn es sich bei den Speichern um Singletons handelt, können Sie ihre Daten nur vorübergehend just-in-time festlegen, bevor der synchrone Rendering-Aufruf und die Komponente sie sehen.

Beispiel für 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);
        });
    };
};

Das routesObjekt (das die Routenhierarchie beschreibt) wird wörtlich mit Client und Server geteilt

Esailija
quelle
Vielen Dank. Die Sache ist, dass meines Wissens nur Routenkomponenten diese willTransitionToMethode unterstützen. Dies bedeutet, dass es immer noch nicht möglich ist, vollständig eigenständige wiederverwendbare Komponenten wie die in der Frage beschriebene zu schreiben. Aber wenn wir nicht bereit sind, uns für Glasfasern zu entscheiden, ist dies wahrscheinlich die beste und reaktionsschnellste Methode, um serverseitiges Rendering zu implementieren.
Tobik
Das ist interessant. Wie würde eine Implementierung der willTransitionTo-Methode aussehen, wenn asynchrone Daten geladen würden?
Hyra
Sie erhalten das transitionObjekt als Parameter und rufen es einfach auf transition.wait(yourPromise). Das bedeutet natürlich, dass Sie Ihre API implementieren müssen, um Versprechen zu unterstützen. Ein weiterer Nachteil dieses Ansatzes besteht darin, dass es keine einfache Möglichkeit gibt, einen "Ladeindikator" auf der Clientseite zu implementieren. Der Übergang wechselt erst zur Route-Handler-Komponente, wenn alle Versprechen erfüllt sind.
Tobik
Aber ich bin mir eigentlich nicht sicher über den "Just-in-Time" -Ansatz. Mehrere verschachtelte Routen-Handler können mit einer URL übereinstimmen, was bedeutet, dass mehrere Versprechen gelöst werden müssen. Es gibt keine Garantie dafür, dass alle gleichzeitig enden. Wenn die Geschäfte Singletons sind, kann dies zu Konflikten führen. @Esailija könntest du vielleicht deine Antwort ein wenig erklären?
Tobik
Ich habe eine automatische Installation, die alle Versprechen .waitedfür einen Übergang sammelt . Sobald alle erfüllt sind, wird der .runRückruf aufgerufen. Kurz bevor .render()ich alle Daten aus den Versprechungen zusammen sammle und die Singelton-Speicherzustände festlege, initialisiere ich in der nächsten Zeile nach dem Render-Aufruf die Singleton-Speicher zurück. Es ist ziemlich hackig, aber alles geschieht automatisch und der Code der Komponenten- und Speicheranwendung bleibt praktisch gleich.
Esailija
0

Ich weiß, dass dies wahrscheinlich nicht genau das ist, was Sie wollen, und es macht möglicherweise keinen Sinn, aber ich erinnere mich, dass ich die Komponente leicht modifiziert habe, um beides zu handhaben:

  • Rendern auf der Serverseite, wobei der gesamte Ausgangszustand bereits abgerufen wurde, bei Bedarf asynchron)
  • Rendern auf der Clientseite, bei Bedarf mit Ajax

Also so etwas wie:

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

});

Es tut mir leid, dass ich nicht den genauen Code zur Hand habe, daher funktioniert dies möglicherweise nicht sofort, aber ich poste im Interesse der Diskussion.

Auch hier besteht die Idee darin, den größten Teil der Komponente als dumme Ansicht zu behandeln und so viel Daten wie möglich aus der Komponente abzurufen.

phtrivier
quelle
1
Danke dir. Ich habe die Idee, aber es ist wirklich nicht das, was ich will. Angenommen, ich möchte mit React eine komplexere Website wie bbc.com erstellen . Auf der Seite sehe ich überall "Komponenten". Ein Abschnitt (Sport, Business ...) ist eine typische Komponente. Wie würden Sie es implementieren? Wo würden Sie alle Daten vorab abrufen? Um solch eine komplexe Site zu entwerfen, sind Komponenten (im Prinzip wie kleine MVC-Container) ein sehr guter (wenn auch der einzige) Weg. Der Komponentenansatz ist für viele typische serverseitige Frameworks üblich. Die Frage ist: Kann ich dafür React verwenden?
Tobik
Sie werden die Daten auf der Serverseite vorab abrufen (wie dies wahrscheinlich in diesem Fall der Fall ist, bevor Sie sie an ein "traditionelles" serverseitiges Vorlagensystem übergeben). Nur weil die Anzeige der Daten modular ist, bedeutet dies, dass die Berechnung der Daten notwendigerweise der gleichen Struktur folgen muss? Ich spiele hier ein bisschen Devil's Advocate. Ich hatte die gleichen Probleme, die Sie beim Auschecken haben. Und ich hoffe, dass jemand mehr Einsichten darüber hat als ich - nahtloses Komponieren von Sachen auf jeder Seite des Drahtes würde viel helfen.
Phtrivier
1
Mit wo meine ich wo im Code. In der Steuerung? Die Controller-Methode, die die Homepage von BBC behandelt, würde also für jeden Abschnitt einen Dutzend ähnlicher Abfragen enthalten? Das ist imho ein Weg zur Hölle. Also ja, ich tun denken , dass die Berechnung als auch modular sein sollte. Alles verpackt in einer Komponente, in einem MVC-Container. Auf diese Weise entwickle ich standardmäßige serverseitige Apps und bin ziemlich sicher, dass dieser Ansatz gut ist. Und der Grund, warum ich von React.js so begeistert bin, ist, dass es ein großes Potenzial gibt, diesen Ansatz sowohl auf Client- als auch auf Serverseite zu verwenden, um fantastische isomorphe Apps zu erstellen.
Tobik
1
Auf jeder Site (groß / klein) müssen Sie nur die aktuelle Seite mit ihrem Init-Status serverseitig rendern (SSR). Sie benötigen den Init-Status nicht für jede Seite. Der Server erfasst den Init-Status, rendert ihn und übergibt ihn an den Client <script type=application/json>{initState}</script>. Auf diese Weise werden die Daten im HTML-Code gespeichert. Rehydrieren / binden Sie UI-Ereignisse an die Seite, indem Sie render auf dem Client aufrufen. Nachfolgende Seiten werden vom js-Code des Clients erstellt (Daten nach Bedarf abrufen) und vom Client gerendert. Auf diese Weise lädt jede Aktualisierung neue SSR-Seiten und das Klicken auf eine Seite ist CSR. = isomorph & SEO freundlich
Federico
0

Ich war heute wirklich durcheinander und obwohl dies keine Antwort auf Ihr Problem ist, habe ich diesen Ansatz verwendet. Ich wollte Express eher für das Routing als für React Router verwenden, und ich wollte keine Glasfasern verwenden, da ich keine Threading-Unterstützung im Knoten benötigte.

Daher habe ich gerade entschieden, dass ich für Anfangsdaten, die beim Laden in den Flussspeicher gerendert werden müssen, eine AJAX-Anforderung ausführen und die Anfangsdaten an den Speicher übergeben werde

Ich habe Fluxxor für dieses Beispiel verwendet.

Also auf meiner Expressroute, in diesem Fall eine /productsRoute:

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

Dann meine Rendermethode initialisieren, die die Daten an den Speicher weitergibt.

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
quelle
0

Ich weiß, dass diese Frage vor einem Jahr gestellt wurde, aber wir hatten das gleiche Problem und lösen es mit verschachtelten Versprechungen, die von den Komponenten abgeleitet wurden, die gerendert werden sollen. Am Ende hatten wir alle Daten für die App und haben sie einfach nach unten geschickt.

Beispielsweise:

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

    }

});

und im Router

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
quelle
0

Möchten Sie meinen Ansatz des serverseitigen Renderns mit Ihnen teilen, lassen Sie Fluxsich zum Beispiel wenig vereinfachen:

  1. Nehmen wir an, wir haben componentmit anfänglichen Daten aus dem Speicher:

    class MyComponent extends Component {
      constructor(props) {
        super(props);
        this.state = {
          data: myStore.getData()
        };
      }
    }
  2. Wenn für die Klasse einige vorinstallierte Daten für den Anfangszustand erforderlich sind, erstellen wir einen Loader für MyComponent:

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

    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. Laden Sie jetzt einfach die Daten in den Router:

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

Nur als kurzes Rollup -> GraphQL löst dies vollständig für Ihren Stack ...

  • GraphQL hinzufügen
  • benutze apollo und reagiere-apollo
  • Verwenden Sie "getDataFromTree", bevor Sie mit dem Rendern beginnen

-> getDataFromTree findet automatisch alle beteiligten Abfragen in Ihrer App und führt sie aus, indem es Ihren Apollo-Cache auf dem Server poupliert und so SSR .. BÄM voll funktionsfähig macht

jebbie
quelle