Strategie renderowania po stronie serwera asynchronicznie zainicjowanych komponentów React.js.

114

Jedną z największych zalet React.js ma być renderowanie po stronie serwera . Problem polega na tym, że funkcja klucza React.renderComponentToString()jest synchroniczna, co uniemożliwia ładowanie jakichkolwiek danych asynchronicznych, gdy hierarchia komponentów jest renderowana na serwerze.

Powiedzmy, że mam uniwersalny komponent do komentowania, który mogę umieścić w dowolnym miejscu na stronie. Ma tylko jedną właściwość, jakiś identyfikator (np. Id artykułu, pod którym umieszczane są komentarze), a całą resztą zajmuje się sam komponent (ładowanie, dodawanie, zarządzanie komentarzami).

Bardzo podoba mi się architektura Flux, ponieważ znacznie ułatwia wiele rzeczy, a jej sklepy są idealne do udostępniania stanu między serwerem a klientem. Po zainicjowaniu mojego sklepu zawierającego komentarze mogę go po prostu serializować i wysłać z serwera do klienta, gdzie można go łatwo przywrócić.

Pytanie brzmi, jak najlepiej zapełnić mój sklep. W ciągu ostatnich dni dużo googlowałem i natknąłem się na kilka strategii, z których żadna nie wydawała się naprawdę dobra, biorąc pod uwagę, jak bardzo ta funkcja React jest „promowana”.

  1. Moim zdaniem najprostszym sposobem jest wypełnienie wszystkich moich sklepów przed rozpoczęciem właściwego renderowania. Oznacza to, że znajduje się poza hierarchią komponentów (na przykład podłączony do mojego routera). Problem z tym podejściem polega na tym, że musiałbym prawie dwukrotnie zdefiniować strukturę strony. Rozważ bardziej złożoną stronę, na przykład stronę bloga z wieloma różnymi składnikami (rzeczywisty post na blogu, komentarze, pokrewne posty, najnowsze posty, strumień na Twitterze ...). Musiałbym zaprojektować strukturę strony za pomocą komponentów Reacta, a następnie gdzie indziej musiałbym zdefiniować proces zapełniania każdego wymaganego sklepu dla tej bieżącej strony. Nie wydaje mi się to miłym rozwiązaniem. Niestety większość samouczków izomorficznych jest zaprojektowana w ten sposób (na przykład ten wspaniały samouczek dotyczący flux ).

  2. React-async . To podejście jest doskonałe. Pozwala mi to po prostu zdefiniować w specjalnej funkcji w każdym komponencie, jak zainicjować stan (nie ma znaczenia, czy synchronicznie czy asynchronicznie), a te funkcje są wywoływane, gdy hierarchia jest renderowana do HTML. Działa w taki sposób, że składnik nie jest renderowany, dopóki stan nie zostanie całkowicie zainicjowany. Problem w tym, że wymaga włókienczyli, o ile rozumiem, rozszerzenie Node.js, które zmienia standardowe zachowanie JavaScript. Chociaż bardzo mi się podoba wynik, nadal wydaje mi się, że zamiast znaleźć rozwiązanie, zmieniliśmy zasady gry. I myślę, że nie powinniśmy być do tego zmuszani, aby korzystać z tej podstawowej funkcji React.js. Nie jestem też pewien ogólnego wsparcia tego rozwiązania. Czy można używać Fibre na standardowym serwerze WWW Node.js?

  3. Myślałem trochę sam. Tak naprawdę nie zastanawiałem się nad szczegółami implementacji, ale ogólna idea jest taka, że ​​powinienem rozszerzyć komponenty w podobny sposób jak React-async, a następnie wielokrotnie wywoływać React.renderComponentToString () na komponencie głównym. Podczas każdego przejazdu zbierałem przedłużające się callbacki, a następnie dzwoniłem do nich na końcu przepustki, aby zapełnić sklepy. Powtarzałbym ten krok, aż wszystkie magazyny wymagane przez aktualną hierarchię komponentów zostałyby wypełnione. Jest wiele rzeczy do rozwiązania i jestem szczególnie niepewny co do wydajności.

Przegapiłem coś? Czy jest inne podejście / rozwiązanie? W tej chwili myślę o przejściu na drogę reagowania asynchronicznego / włókien, ale nie jestem tego do końca pewien, jak wyjaśniono w drugim punkcie.

Powiązana dyskusja na GitHub . Najwyraźniej nie ma oficjalnego podejścia ani nawet rozwiązania. Może prawdziwe pytanie brzmi, jak mają być używane komponenty React. Na przykład prosta warstwa widoku (w zasadzie moja sugestia numer jeden) czy prawdziwie niezależne i samodzielne komponenty?

tobik
źródło
Tylko po to, żeby uzyskać rzeczy: asynchroniczne wywołania miałyby miejsce również po stronie serwera? Nie rozumiem korzyści w tym przypadku w przeciwieństwie do renderowania widoku z niektórymi częściami pozostawionymi pustymi i wypełniania go, gdy przychodzą wyniki odpowiedzi asynchronicznej. Prawdopodobnie czegoś brakuje, przepraszam!
phtrivier
Nie wolno zapominać, że w JavaScript nawet najprostsze zapytanie do bazy danych w celu pobrania najnowszych postów jest asynchroniczne. Więc jeśli renderujesz widok, musisz poczekać, aż dane zostaną pobrane z bazy danych. Renderowanie po stronie serwera ma oczywiste zalety: na przykład SEO. A także zapobiega migotaniu strony. W rzeczywistości renderowanie po stronie serwera jest standardowym podejściem, z którego nadal korzysta większość witryn internetowych.
tobik
Jasne, ale czy próbujesz wyrenderować całą stronę (po udzieleniu odpowiedzi na wszystkie asynchroniczne zapytania bazy danych)? W takim przypadku naiwnie oddzieliłbym to jako 1 / asynchroniczne pobieranie wszystkich danych 2 / po zakończeniu, przekazanie go do „głupiego” React View i odpowiedzenie na żądanie. A może próbujesz wykonać renderowanie po stronie serwera, a następnie po stronie klienta z tym samym kodem (i potrzebujesz kodu asynchronicznego, aby był blisko widoku reagowania?) Przepraszam, jeśli to brzmi głupio, po prostu nie jestem pewien, czy otrzymam co robisz.
phtrivier
Nie ma problemu, być może inni też mają problemy ze zrozumieniem :) To, co właśnie opisałeś, jest rozwiązaniem numer dwa. Ale weźmy na przykład komponent komentarza z pytania. W zwykłej aplikacji po stronie klienta mogłem zrobić wszystko w tym komponencie (ładowanie / dodawanie komentarzy). Komponent zostałby oddzielony od świata zewnętrznego, a świat zewnętrzny nie musiałby przejmować się tym komponentem. Byłby całkowicie niezależny i samodzielny. Ale kiedy chcę wprowadzić renderowanie po stronie serwera, muszę obsługiwać asynchroniczne rzeczy na zewnątrz. A to łamie całą zasadę.
tobik
Żeby było jasne, nie zalecam używania włókien, ale po prostu wykonuję wszystkie wywołania asynchroniczne, a po ich zakończeniu (używając obietnicy lub czegokolwiek), renderuj komponent po stronie serwera. (Więc komponenty React w ogóle nie wiedziałyby o rzeczy asynchronicznej.) To tylko opinia, ale tak naprawdę podoba mi się pomysł całkowitego usunięcia wszystkiego związanego z komunikacją z serwerem z komponentów Reacta (które są tak naprawdę tutaj tylko po to, aby renderować widok .) I myślę, że to filozofia stojąca za reagowaniem, co może wyjaśniać, dlaczego to, co robisz, jest nieco skomplikowane. W każdym razie powodzenia :)
phtrivier

Odpowiedzi:

15

Jeśli używaszreak-router , możesz po prostu zdefiniować willTransitionTometody w komponentach, które otrzymają Transitionobiekt, który możesz wywołać .wait.

Nie ma znaczenia, czy funkcja renderToString jest synchroniczna, ponieważ wywołanie zwrotne do Router.runnie zostanie wywołane, dopóki wszystkie .waitobietnice ed nie zostaną rozwiązane, więc do czasu renderToStringwywołania w oprogramowaniu pośredniczącym można było zapełnić sklepy. Nawet jeśli sklepy są singletonami, możesz po prostu tymczasowo ustawić ich dane dokładnie na czas przed wywołaniem renderowania synchronicznego, a komponent je zobaczy.

Przykład oprogramowania pośredniego:

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

routesPrzedmiot (który opisuje hierarchię tras) jest dzielona verbatim z klientem i serwerem

Esailija
źródło
Dzięki. Chodzi o to, że o ile wiem, tylko komponenty trasy obsługują tę willTransitionTometodę. Co oznacza, że ​​nadal nie jest możliwe napisanie całkowicie samodzielnych komponentów wielokrotnego użytku, takich jak ten, który opisałem w pytaniu. Ale jeśli nie chcemy iść na Fibers, jest to prawdopodobnie najlepszy i najbardziej reagujący sposób na zaimplementowanie renderowania po stronie serwera.
tobik
To jest interesujące. Jak wyglądałaby implementacja metody willTransitionTo z załadowanymi danymi asynchronicznymi?
Hyra
Otrzymasz transitionobiekt jako parametr, więc po prostu zadzwonisz transition.wait(yourPromise). To oczywiście oznacza, że ​​musisz zaimplementować swoje API, aby obsługiwać obietnice. Inną wadą tego podejścia jest to, że nie ma prostego sposobu na zaimplementowanie „wskaźnika obciążenia” po stronie klienta. Przejście nie przełączy się na komponent obsługi trasy, dopóki wszystkie obietnice nie zostaną rozwiązane.
tobik
Ale właściwie nie jestem pewien co do podejścia „dokładnie na czas”. Wiele zagnieżdżonych programów obsługi tras może pasować do jednego adresu URL, co oznacza, że ​​trzeba będzie rozwiązać wiele obietnic. Nie ma gwarancji, że wszystkie zakończą się w tym samym czasie. Jeśli sklepy są singletonami, może to powodować konflikty. @Esailija, czy możesz trochę wyjaśnić swoją odpowiedź?
tobik
Mam na miejscu automatyczną instalację wodociągową, która zbiera wszystkie obietnice .waitedzwiązane z przejściem. Gdy wszystkie z nich zostaną spełnione, .runwywoływane jest wywołanie zwrotne. Tuż przed .render()zebraniem wszystkich danych z obietnic i ustawieniem stanów magazynu singelton, w następnym wierszu po wywołaniu renderowania inicjalizuję z powrotem sklepy singleton. To dość hakerskie, ale wszystko dzieje się automatycznie, a kod komponentu i aplikacji pozostaje praktycznie taki sam.
Esailija
0

Wiem, że prawdopodobnie nie jest to dokładnie to, czego chcesz i może to nie mieć sensu, ale pamiętam, jak radziłem sobie z niewielką modyfikacją komponentu, aby obsłużyć oba:

  • renderowanie po stronie serwera, z już pobranym całym stanem początkowym, w razie potrzeby asynchronicznie)
  • renderowanie po stronie klienta, w razie potrzeby z wykorzystaniem technologii AJAX

Więc coś takiego:

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

});

Przepraszam, że nie mam pod ręką dokładnego kodu, więc może to nie zadziałać po wyjęciu z pudełka, ale piszę w celu dyskusji.

Znowu chodzi o to, aby leczyć większość składnika jako niemy widzenia, oraz radzenia sobie z pobierania danych jak najwięcej out składnika.

phtrivier
źródło
1
Dziękuję Ci. Rozumiem, ale naprawdę nie tego chcę. Powiedzmy, że chcę zbudować bardziej złożoną stronę internetową przy użyciu Reacta , na przykład bbc.com . Patrząc na stronę, wszędzie widzę „komponenty”. Sekcja (sport, biznes ...) to typowy element. Jak byś to zaimplementował? Gdzie pobrałbyś wszystkie dane? Aby zaprojektować tak złożoną witrynę, komponenty (z zasady, takie jak małe kontenery MVC) są bardzo dobrym (jeśli może jedynym) rozwiązaniem. Podejście oparte na komponentach jest wspólne dla wielu typowych struktur po stronie serwera. Pytanie brzmi: czy mogę do tego użyć Reacta?
tobik
Dane zostaną wstępnie pobrane po stronie serwera (jak to prawdopodobnie zrobiono w tym przypadku, przed przekazaniem ich do „tradycyjnego” systemu szablonów po stronie serwera); czy tylko dlatego, że wyświetlanie danych zyskuje na byciu modułowym, oznacza to, że obliczenia danych muszą koniecznie mieć tę samą strukturę? Gram tu trochę adwokata diabła, miałem ten sam problem, co ty, sprawdzając om. I mam nadzieję, że ktoś ma więcej informacji na ten temat niż ja - płynne komponowanie rzeczy po dowolnej stronie drutu bardzo by pomogło.
phtrivier
1
Gdzie mam na myśli gdzie w kodzie. W kontrolerze? Więc metoda kontrolera obsługująca stronę główną bbc zawierałaby kilkanaście podobnych zapytań, po jednym dla każdej sekcji? To jest droga do piekła. Więc tak, mam zrobić pomyśleć, że obliczenia powinny być modułowy, jak również. Wszystko zapakowane w jeden komponent, w jednym kontenerze MVC. W ten sposób tworzę standardowe aplikacje po stronie serwera i jestem pewien, że to podejście jest dobre. Powodem, dla którego jestem tak podekscytowany React.js, jest to, że istnieje duży potencjał wykorzystania tego podejścia zarówno po stronie klienta, jak i serwera, do tworzenia niesamowitych aplikacji izomorficznych.
tobik
1
W każdej witrynie (dużej / małej) wystarczy wyrenderować po stronie serwera (SSR) bieżącą stronę z jej stanem początkowym; nie potrzebujesz stanu inicjalizacji dla każdej strony. Serwer pobiera stan init, renderuje go i przekazuje klientowi <script type=application/json>{initState}</script>; w ten sposób dane będą w HTML. Ponownie nawadniaj / powiąż zdarzenia interfejsu użytkownika ze stroną, wywołując renderowanie na kliencie. Kolejne strony są tworzone przez kod js klienta (pobieranie danych w razie potrzeby) i renderowane przez klienta. W ten sposób każde odświeżenie załaduje nowe strony SSR, a kliknięcie strony będzie CSR. = izomorficzny i przyjazny dla SEO
Federico
0

Naprawdę się tym dzisiaj bawiłem i chociaż nie jest to odpowiedź na twój problem, zastosowałem to podejście. Chciałem używać Express do routingu, a nie React Router, i nie chciałem używać Fibers, ponieważ nie potrzebowałem obsługi wątków w węźle.

Dlatego właśnie podjąłem decyzję, że dla danych początkowych, które muszą być przekazane do magazynu flux podczas ładowania, wykonam żądanie AJAX i przekażę dane początkowe do magazynu

W tym przykładzie używałem Fluxxor.

A więc na mojej trasie ekspresowej, w tym przypadku /productstrasa:

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

Następnie moja metoda renderowania inicjalizacji, która przekazuje dane do sklepu.

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
źródło
0

Wiem, że to pytanie zostało zadane rok temu, ale mieliśmy ten sam problem i rozwiązujemy go za pomocą zagnieżdżonych obietnic, które pochodzą z komponentów, które mają być renderowane. W końcu mieliśmy wszystkie dane dotyczące aplikacji i po prostu wysłaliśmy je.

Na przykład:

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

    }

});

iw routerze

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
źródło
0

Chcę podzielić się z wami moim podejściem do renderowania po stronie serwera Flux, trochę uproszczone, na przykład:

  1. Powiedzmy, że mamy componentwstępne dane ze sklepu:

    class MyComponent extends Component {
      constructor(props) {
        super(props);
        this.state = {
          data: myStore.getData()
        };
      }
    }
  2. Jeśli klasa wymaga pewnych wstępnie załadowanych danych dla stanu początkowego, stwórzmy Loader dla MyComponent:

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

    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. Teraz po prostu załaduj dane do routera:

    on('/my-route', async () => {
        await new MyComponentLoader().load();
        return <MyComponent/>;
    });
zooblin
źródło
0

tak jak krótki pakiet -> GraphQL rozwiąże to całkowicie dla twojego stosu ...

  • dodaj GraphQL
  • użyj apollo i react-apollo
  • użyj „getDataFromTree” przed rozpoczęciem renderowania

-> getDataFromTree automatycznie znajdzie wszystkie zaangażowane zapytania w Twojej aplikacji i wykona je, umieszczając pamięć podręczną Apollo na serwerze, umożliwiając w ten sposób w pełni działające SSR. BĘM

jebbie
źródło