Strategie per il rendering lato server dei componenti React.js inizializzati in modo asincrono


114

Uno dei maggiori vantaggi di React.js dovrebbe essere il rendering lato server . Il problema è che la funzione chiave React.renderComponentToString()è sincrona, il che rende impossibile caricare i dati asincroni mentre viene eseguito il rendering della gerarchia dei componenti sul server.

Diciamo che ho un componente universale per i commenti che posso rilasciare praticamente ovunque sulla pagina. Ha solo una proprietà, una sorta di identificatore (ad esempio l'ID di un articolo sotto il quale vengono inseriti i commenti), e tutto il resto è gestito dal componente stesso (caricamento, aggiunta, gestione dei commenti).

Mi piace molto l' architettura Flux perché rende molte cose molto più semplici e i suoi archivi sono perfetti per condividere lo stato tra server e client. Una volta inizializzato il mio negozio contenente commenti, posso semplicemente serializzarlo e inviarlo dal server al client dove viene facilmente ripristinato.

La domanda è qual è il modo migliore per popolare il mio negozio. Negli ultimi giorni ho cercato molto su Google e mi sono imbattuto in poche strategie, nessuna delle quali sembrava davvero buona considerando quanto questa caratteristica di React sia stata "promossa".

  1. A mio parere, il modo più semplice è popolare tutti i miei negozi prima che inizi il rendering effettivo. Ciò significa che da qualche parte al di fuori della gerarchia dei componenti (ad esempio collegato al mio router). Il problema con questo approccio è che dovrei definire praticamente due volte la struttura della pagina. Considera una pagina più complessa, ad esempio una pagina di blog con molti componenti diversi (post del blog effettivo, commenti, post correlati, post più recenti, stream di Twitter ...). Dovrei progettare la struttura della pagina usando i componenti React e poi da qualche altra parte dovrei definire il processo di popolamento di ogni negozio richiesto per questa pagina corrente. Non mi sembra una bella soluzione. Sfortunatamente la maggior parte dei tutorial isomorfi sono progettati in questo modo (ad esempio questo fantastico tutorial sul flusso ).

  2. React-async . Questo approccio è perfetto. Mi consente di definire semplicemente in una funzione speciale in ogni componente come inizializzare lo stato (non importa se in modo sincrono o asincrono) e queste funzioni vengono chiamate mentre la gerarchia viene resa in HTML. Funziona in modo tale che un componente non venga renderizzato fino a quando lo stato non è completamente inizializzato. Il problema è che richiede fibreche è, per quanto ho capito, un'estensione Node.js che altera il comportamento JavaScript standard. Nonostante il risultato mi piaccia molto, mi sembra comunque che invece di trovare una soluzione abbiamo cambiato le regole del gioco. E penso che non dovremmo essere costretti a farlo per utilizzare questa caratteristica principale di React.js. Inoltre, non sono sicuro del supporto generale di questa soluzione. È possibile utilizzare Fiber sul web hosting standard Node.js?

  3. Stavo pensando un po 'da solo. Non ho davvero pensato ai dettagli di implementazione, ma l'idea generale è che estenderei i componenti in modo simile a React-async e quindi chiamerei ripetutamente React.renderComponentToString () sul componente root. Durante ogni passaggio raccoglievo le richiamate estese e poi le chiamavo alla fine del passaggio per popolare i negozi. Ripeterei questo passaggio finché tutti i negozi richiesti dalla gerarchia dei componenti corrente non saranno popolati. Ci sono molte cose da risolvere e sono particolarmente insicuro sulla performance.

Ho dimenticato qualcosa? C'è un altro approccio / soluzione? In questo momento sto pensando di andare in modo reattivo-asincrono / fibre ma non ne sono completamente sicuro come spiegato nel secondo punto.

Discussione correlata su GitHub . Apparentemente, non esiste un approccio ufficiale o addirittura una soluzione. Forse la vera domanda è come devono essere utilizzati i componenti React. Come un semplice livello di visualizzazione (praticamente il mio suggerimento numero uno) o come veri componenti indipendenti e autonomi?


Giusto per capire: le chiamate asincrone avverrebbero anche sul lato server? Non capisco i vantaggi in questo caso rispetto al rendering della vista con alcune parti lasciate vuote e al riempirlo quando arrivano i risultati della risposta asincrona. Probabilmente manca qualcosa, scusa!
phtrivier

Non devi dimenticare che in JavaScript anche la query più semplice al database per recuperare gli ultimi post è asincrona. Quindi, se stai eseguendo il rendering di una vista, devi attendere che i dati vengano recuperati dal database. E ci sono evidenti vantaggi nel rendering lato server: SEO, ad esempio. E impedisce anche lo sfarfallio della pagina. In realtà il rendering lato server è l'approccio standard che la maggior parte dei siti Web utilizza ancora.
tobik

Certo, ma stai cercando di eseguire il rendering dell'intera pagina (una volta che tutte le query asincrone del database hanno risposto)? In tal caso, l'avrei separato ingenuamente come 1 / recuperando tutti i dati in modo asincrono 2 / quando fatto, passarlo a una vista React "stupida" e rispondere alla richiesta. O stai provando a fare entrambi il rendering lato server, quindi lato client con lo stesso codice (e hai bisogno che il codice asincrono sia vicino alla vista di reazione?) Scusa se suona stupido, non sono sicuro di aver capito cosa stai facendo.
phtrivier

Nessun problema, forse anche altre persone hanno problemi a capire :) Quella che hai appena descritto è la soluzione numero due. Ma prendi ad esempio il componente per commentare dalla domanda. In un'applicazione comune lato client potrei fare tutto in quel componente (caricamento / aggiunta di commenti). Il componente sarebbe separato dal mondo esterno e il mondo esterno non dovrebbe preoccuparsi di questo componente. Sarebbe completamente indipendente e autonomo. Ma una volta che voglio introdurre il rendering lato server, devo gestire le cose asincrone all'esterno. E questo infrange l'intero principio.
tobik

Giusto per essere chiari, non sto sostenendo l'uso di fibre, ma solo di fare tutte le chiamate asincrone e, dopo che sono state tutte finite (usando promesse o altro), renderizza il componente sul lato server. (Quindi i componenti di reazione non saprebbero affatto delle cose asincrone.) Questa è solo un'opinione, ma in realtà mi piace l'idea di rimuovere completamente qualsiasi cosa relativa alla comunicazione del server dai componenti di React (che sono qui solo per il rendering della vista .) E penso che questa sia la filosofia alla base di React, che potrebbe spiegare perché quello che stai facendo è un po 'complicato. Comunque, buona fortuna :)
phtrivier

Risposte:


15

Se usi react-router , puoi semplicemente definire un willTransitionTometodo in components, a cui viene passato un Transitionoggetto su cui puoi chiamare .wait.

Non importa se renderToString è sincrono perché il callback a Router.runnon verrà chiamato fino a quando tutte le .waitpromesse di ed non saranno risolte, quindi per il momento in cui renderToStringviene chiamato nel middleware potresti aver popolato gli archivi. Anche se gli archivi sono singleton, puoi semplicemente impostare i loro dati temporaneamente just-in-time prima della chiamata di rendering sincrono e il componente lo vedrà.

Esempio di 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' routesoggetto (che descrive la gerarchia delle rotte) è condiviso alla lettera con client e server


Grazie. Il fatto è che, per quanto ne so, solo i componenti del percorso supportano questo willTransitionTometodo. Ciò significa che non è ancora possibile scrivere componenti riutilizzabili completamente autonomi come quello che ho descritto nella domanda. Ma a meno che non siamo disposti a utilizzare Fibers, questo è probabilmente il modo migliore e più reattivo per implementare il rendering lato server.
tobik

Questo è interessante. Come apparirebbe un'implementazione del metodo willTransitionTo per caricare i dati asincroni?
Hyra

Otterrai l' transitionoggetto come parametro, quindi chiamerai semplicemente transition.wait(yourPromise). Ciò significa ovviamente che devi implementare la tua API per supportare le promesse. Un altro svantaggio di questo approccio è che non esiste un modo semplice per implementare un "indicatore di caricamento" sul lato client. La transizione non passerà al componente gestore di route finché tutte le promesse non saranno state risolte.
tobik

Ma in realtà non sono sicuro dell'approccio "just-in-time". Più gestori di route annidati possono corrispondere a un URL, il che significa che più promesse dovranno essere risolte. Non vi è alcuna garanzia che finiranno tutti nello stesso momento. Se i negozi sono singoli, può causare conflitti. @Esailija potresti forse spiegare un po 'la tua risposta?
tobik

Ho installato l'impianto idraulico automatico che raccoglie tutte le promesse che .waitedper una transizione. Una volta soddisfatte tutte, .runviene richiamata la richiamata. Appena prima di .render()raccogliere tutti i dati dalle promesse e impostare gli stati di archivio singelton, quindi nella riga successiva dopo la chiamata di rendering inizializzo nuovamente i negozi singleton. È piuttosto complicato ma tutto accade automaticamente e il codice dell'applicazione del componente e del negozio rimane praticamente lo stesso.
Esailija

0

So che probabilmente questo non è esattamente quello che vuoi, e potrebbe non avere senso, ma ricordo di cavarmela modificando leggermente il componente per gestirli entrambi:

  • rendering lato server, con tutto lo stato iniziale già recuperato, in modo asincrono se necessario)
  • rendering sul lato client, con ajax se necessario

Quindi qualcosa come:

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

});

Mi dispiace di non avere il codice esatto a portata di mano, quindi potrebbe non funzionare immediatamente, ma sto postando nell'interesse della discussione.

Ancora una volta, l'idea è di trattare la maggior parte del componente come una vista stupida e gestire il recupero dei dati il ​​più possibile dal componente.


1
Grazie. Ho l'idea, ma non è proprio quello che voglio. Diciamo che voglio creare un sito web più complesso usando React, come bbc.com . Guardando la pagina, posso vedere "componenti" ovunque. Una sezione (sport, affari ...) è una componente tipica. Come lo implementeresti? Dove precaricheresti tutti i dati? Per progettare un sito così complesso, i componenti (come principio, come i piccoli contenitori MVC) sono molto buoni (se forse l'unico) modo di procedere. L'approccio a componenti è comune a molti framework tipici lato server. La domanda è: posso usare React per questo?
tobik

Precaricherai i dati sul lato server (come probabilmente avviene in questo caso, prima di passarli a un sistema di template lato server "tradizionale"); solo perché la visualizzazione dei dati trae vantaggio dall'essere modulare, significa che il calcolo dei dati deve necessariamente seguire la stessa struttura? Sto interpretando un po 'l'avvocato del diavolo qui, ho avuto gli stessi problemi che hai quando controlli om. E spero sicuramente che qualcuno abbia più intuizioni su questo di me: comporre cose senza interruzioni su qualsiasi lato del filo sarebbe di grande aiuto.
phtrivier

1
Da dove intendo dove nel codice. Nel controller? Quindi il metodo del controller che gestisce la home page di bbc conterrebbe una dozzina di query simili, per ciascuna sezione? Questo è un modo per andare all'inferno. Quindi sì, mi faccio penso che il calcolo dovrebbe essere modulare pure. Tutto racchiuso in un unico componente, in un unico contenitore MVC. È così che sviluppo app lato server standard e sono abbastanza fiducioso che questo approccio sia buono. E il motivo per cui sono così entusiasta di React.js è che c'è un grande potenziale per utilizzare questo approccio sia sul lato client che su quello server per creare fantastiche app isomorfiche.
tobik

1
Su qualsiasi sito (grande / piccolo), devi solo renderizzare lato server (SSR) la pagina corrente con il suo stato di inizializzazione; non è necessario lo stato di inizializzazione per ogni pagina. Il server acquisisce lo stato di inizializzazione, lo visualizza e lo passa al client <script type=application/json>{initState}</script>; in questo modo i dati saranno nell'HTML. Reidratare / associare gli eventi dell'interfaccia utente alla pagina chiamando render sul client. Le pagine successive vengono create dal codice js del client (recuperando i dati secondo necessità) e renderizzate dal client. In questo modo qualsiasi aggiornamento caricherà nuove pagine SSR e fare clic su una pagina sarà CSR. = isomorfo e SEO friendly
Federico

0

Oggi sono stato davvero incasinato con questo, e sebbene questa non sia una risposta al tuo problema, ho usato questo approccio. Volevo usare Express per il routing piuttosto che React Router e non volevo usare Fibers perché non avevo bisogno del supporto per il threading nel nodo.

Quindi ho appena deciso che per i dati iniziali che devono essere resi nell'archivio di flusso al caricamento, eseguirò una richiesta AJAX e passerò i dati iniziali nell'archivio

Stavo usando Fluxxor per questo esempio.

Quindi sul mio percorso espresso, in questo caso un /productspercorso:

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

Quindi il mio metodo di rendering di inizializzazione che passa i dati all'archivio.

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

So che questa domanda è stata posta un anno fa, ma abbiamo avuto lo stesso problema e lo risolviamo con promesse annidate derivate dai componenti che verranno renderizzati. Alla fine abbiamo avuto tutti i dati per l'app e li abbiamo semplicemente inviati.

Per esempio:

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

    }

});

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

0

Voglio condividere con te il mio approccio al rendering lato server utilizzando Flux, ad esempio semplificato un po ':

  1. Diciamo che abbiamo componentcon i dati iniziali dal negozio:

    class MyComponent extends Component {
      constructor(props) {
        super(props);
        this.state = {
          data: myStore.getData()
        };
      }
    }
  2. Se la classe richiede alcuni dati precaricati per lo stato iniziale, creiamo Loader per MyComponent:

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

    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. Ora carica i dati nel router:

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

0

proprio come un breve rollup -> GraphQL risolverà questo completamente per il tuo stack ...

  • aggiungi GraphQL
  • usa Apollo e React-Apollo
  • usa "getDataFromTree" prima di iniziare il rendering

-> getDataFromTree troverà automaticamente tutte le query coinvolte nella tua app e le eseguirà, caricando la tua cache di Apollo sul server e quindi abilitando SSR completamente funzionante. BÄM

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.