Estratégias para renderização do lado do servidor de componentes React.js inicializados de forma assíncrona

114

Uma das maiores vantagens do React.js deve ser a renderização do lado do servidor . O problema é que a função chave React.renderComponentToString()é síncrona, o que torna impossível carregar quaisquer dados assíncronos conforme a hierarquia do componente é renderizada no servidor.

Digamos que eu tenha um componente universal para comentários que posso colocar em qualquer lugar da página. Possui apenas uma propriedade, algum tipo de identificador (por exemplo id de um artigo abaixo do qual os comentários são colocados), e todo o resto é tratado pelo próprio componente (carregar, adicionar, gerenciar comentários).

Eu realmente gosto da arquitetura Flux porque torna muitas coisas muito mais fáceis, e seus armazenamentos são perfeitos para compartilhar estado entre servidor e cliente. Assim que minha loja contendo comentários for inicializada, posso apenas serializá-la e enviá-la do servidor para o cliente, onde é facilmente restaurada.

A questão é qual é a melhor forma de povoar minha loja. Nos últimos dias, tenho pesquisado muito no Google e encontrei poucas estratégias, nenhuma das quais parecia muito boa, considerando o quanto esse recurso do React está sendo "promovido".

  1. Na minha opinião, a maneira mais simples é preencher todas as minhas lojas antes que a renderização real comece. Isso significa algum lugar fora da hierarquia do componente (conectado ao meu roteador, por exemplo). O problema com essa abordagem é que eu teria que definir a estrutura da página duas vezes. Considere uma página mais complexa, por exemplo, uma página de blog com muitos componentes diferentes (postagem real do blog, comentários, postagens relacionadas, postagens mais recentes, fluxo do Twitter ...). Eu teria que projetar a estrutura da página usando componentes React e então em outro lugar eu teria que definir o processo de preencher cada loja necessária para esta página atual. Isso não parece uma boa solução para mim. Infelizmente, a maioria dos tutoriais isomórficos são projetados dessa maneira (por exemplo, este ótimo tutorial de fluxo ).

  2. React-async . Essa abordagem é perfeita. Ele me permite simplesmente definir em uma função especial em cada componente como inicializar o estado (não importa se de forma síncrona ou assíncrona) e essas funções são chamadas conforme a hierarquia está sendo renderizada em HTML. Funciona de forma que um componente não seja renderizado até que o estado seja completamente inicializado. O problema é que requer fibrasque é, até onde eu entendo, uma extensão Node.js que altera o comportamento padrão do JavaScript. Embora goste muito do resultado, ainda me parece que, em vez de encontrar uma solução, mudamos as regras do jogo. E acho que não devemos ser forçados a fazer isso para usar esse recurso central do React.js. Também não tenho certeza sobre o suporte geral dessa solução. É possível usar o Fiber na hospedagem padrão da Web Node.js?

  3. Eu estava pensando um pouco por conta própria. Eu realmente não pensei nos detalhes de implementação, mas a ideia geral é que eu estenderia os componentes de maneira semelhante ao React-async e, em seguida, chamaria repetidamente React.renderComponentToString () no componente raiz. Durante cada passagem, eu coletaria os retornos de chamada estendidos e depois os chamaria no e do passe para preencher as lojas. Eu repetiria esta etapa até que todos os armazenamentos exigidos pela hierarquia de componentes atual fossem preenchidos. Há muitas coisas a serem resolvidas e estou particularmente inseguro quanto ao desempenho.

Perdi alguma coisa? Existe outra abordagem / solução? No momento, estou pensando em adotar o método de reação assíncrona / fibras, mas não estou completamente certo sobre isso, conforme explicado no segundo ponto.

Discussão relacionada no GitHub . Aparentemente, não existe uma abordagem oficial ou mesmo solução. Talvez a verdadeira questão seja como os componentes React devem ser usados. Como camada de visualização simples (basicamente minha sugestão número um) ou como componentes reais independentes e autônomos?

Tobik
fonte
Só para entender as coisas: as chamadas assíncronas aconteceriam no lado do servidor também? Não entendo os benefícios neste caso, em oposição a renderizar a visualização com algumas partes vazias e preenchê-la conforme os resultados da resposta assíncrona chegam. Provavelmente faltando alguma coisa, desculpe!
phtrivier
Você não deve esquecer que em JavaScript até mesmo a consulta mais simples ao banco de dados para buscar as últimas postagens é assíncrona. Portanto, se você estiver renderizando uma visualização, terá que esperar até que os dados sejam buscados no banco de dados. E há benefícios óbvios na renderização no lado do servidor: SEO, por exemplo. E também evita que a página pisque. Na verdade, a renderização do lado do servidor é a abordagem padrão que a maioria dos sites ainda usa.
tobik
Claro, mas você está tentando renderizar a página inteira (depois que todas as consultas assíncronas do banco de dados tiverem respondido)? Nesse caso, eu ingenuamente o teria separado como 1 / buscando todos os dados de forma assíncrona 2 / quando terminar, passaria para um React View "burro" e responderia à solicitação. Ou você está tentando fazer a renderização do lado do servidor e depois do lado do cliente com o mesmo código (e você precisa que o código assíncrono esteja perto da visualização de reação?) Desculpe se isso parece bobo, só não tenho certeza se entendi o que você está fazendo.
phtrivier
Sem problemas, talvez outras pessoas também tenham problemas para entender :) O que você acabou de descrever é a solução número dois. Mas tome, por exemplo, o componente para comentar a questão. Em aplicativos comuns do lado do cliente, eu poderia fazer tudo naquele componente (carregar / adicionar comentários). O componente seria separado do mundo externo e o mundo externo não teria que se preocupar com este componente. Seria totalmente independente e autônomo. Mas uma vez que eu quero introduzir a renderização do lado do servidor, tenho que lidar com as coisas assíncronas fora. E isso quebra todo o princípio.
tobik
Só para ficar claro, não estou defendendo o uso de fibras, mas apenas fazer todas as chamadas assíncronas e, depois que estiverem todas concluídas (usando promessa ou qualquer outra coisa), renderize o componente no lado do servidor. (Assim, os componentes de reação não saberiam nada sobre as coisas assíncronas.) Agora, isso é apenas uma opinião, mas na verdade eu gosto da ideia de remover completamente qualquer coisa relacionada à comunicação do servidor dos componentes do React (que estão aqui apenas para renderizar a visualização .) E eu acho que essa é a filosofia por trás do react, o que pode explicar porque o que você está fazendo é um pouco complicado. De qualquer forma, boa sorte :)
phtrivier

Respostas:

15

Se você usar o react-router , pode apenas definir willTransitionTométodos nos componentes, que passam um Transitionobjeto que você pode chamar .wait.

Não importa se renderToString é síncrono porque o retorno de chamada para Router.runnão será chamado até que todas as .waitpromessas ed sejam resolvidas, portanto, quando renderToStringfor chamado no middleware, você poderá ter preenchido os armazenamentos. Mesmo se os armazenamentos forem singletons, você pode apenas definir seus dados temporariamente just-in-time antes da chamada de renderização síncrona e o componente os verá.

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

O routesobjeto (que descreve a hierarquia das rotas) é compartilhado literalmente com o cliente e o servidor

Esailija
fonte
Obrigado. A questão é que, até onde eu sei, apenas os componentes de rota suportam esse willTransitionTométodo. O que significa que ainda não é possível escrever componentes reutilizáveis ​​completamente autônomos como o que descrevi na pergunta. Mas, a menos que estejamos dispostos a usar o Fibers, esta é provavelmente a melhor e a mais eficaz maneira de implementar a renderização do lado do servidor.
tobik
Isto é interessante. Como seria uma implementação do método willTransitionTo para ter dados assíncronos carregados?
Hyra
Você obterá o transitionobjeto como um parâmetro, portanto, simplesmente chamará transition.wait(yourPromise). Isso, claro, significa que você precisa implementar sua API para dar suporte às promessas. Outra desvantagem dessa abordagem é que não há uma maneira simples de implementar um "indicador de carregamento" no lado do cliente. A transição não mudará para o componente de manipulador de rota até que todas as promessas sejam resolvidas.
tobik
Mas, na verdade, não tenho certeza sobre a abordagem "just-in-time". Vários manipuladores de rota aninhados podem corresponder a um url, o que significa que várias promessas terão que ser resolvidas. Não há garantia de que todos acabarão ao mesmo tempo. Se as lojas forem singletons, isso pode causar conflitos. @Esailija você poderia explicar um pouco sua resposta?
tobik
Tenho encanamento automático que coleta todas as promessas .waitedpara uma transição. Depois que todos eles forem atendidos, o .runretorno de chamada é chamado. Antes de .render()coletar todos os dados juntos das promessas e definir os estados de armazenamento singelton, na próxima linha após a chamada de renderização inicializo os armazenamentos singleton de volta. É bastante hacky, mas tudo acontece automaticamente e o código do aplicativo de armazenamento e componente permanece virtualmente o mesmo.
Esailija
0

Sei que provavelmente não é exatamente o que você deseja e pode não fazer sentido, mas me lembro de modificar um pouco o componente para lidar com ambos:

  • renderização no lado do servidor, com todo o estado inicial já recuperado, de forma assíncrona, se necessário)
  • renderizar no lado do cliente, com ajax se necessário

Então, algo como:

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

});

Lamento não ter o código exato em mãos, então isso pode não funcionar imediatamente, mas estou postando para discussão.

Novamente, a ideia é tratar a maior parte do componente como uma visão burra e lidar com a busca de dados o máximo possível fora do componente.

phtrivier
fonte
1
Obrigado. Eu entendi, mas não é o que eu realmente quero. Digamos que eu queira construir um site mais complexo usando React, como bbc.com . Olhando a página, posso ver "componentes" em todos os lugares. Uma seção (esporte, negócios ...) é um componente típico. Como você o implementaria? Onde você pré-buscaria todos os dados? Para projetar um site tão complexo, os componentes (por princípio, como pequenos contêineres MVC) são muito bons (se talvez o único) caminho a percorrer. A abordagem do componente é comum para muitas estruturas típicas do lado do servidor. A questão é: posso usar o React para isso?
tobik
Você pré-buscará os dados no lado do servidor (como provavelmente é feito neste caso, antes de passá-los para um sistema de modelo "tradicional" do lado do servidor); só porque a exibição dos dados se beneficia de ser modular, isso significa que o cálculo dos dados necessariamente tem que seguir a mesma estrutura? Estou bancando o advogado do diabo um pouco aqui, tive o mesmo problema que você tem ao verificar o om. E espero que alguém tenha mais idéias sobre isso do que eu - compor coisas perfeitamente em qualquer lado do fio ajudaria muito.
phtrivier
1
Onde quero dizer onde no código. No controlador? Portanto, o método do controlador que trata da página inicial do bbc conteria uma dúzia de consultas semelhantes, para cada seção um? Esse é um caminho para o inferno. Então sim, eu fazer pensar que a computação deve ser modular também. Tudo empacotado em um componente, em um contêiner MVC. É assim que desenvolvo aplicativos padrão do lado do servidor e estou bastante confiante de que essa abordagem é boa. E o motivo pelo qual estou tão animado com React.js é que há um grande potencial para usar essa abordagem no lado do cliente e do servidor para criar aplicativos isomórficos incríveis.
tobik
1
Em qualquer site (grande / pequeno), você só precisa renderizar do lado do servidor (SSR) a página atual com seu estado de inicialização; você não precisa do estado init para cada página. O servidor pega o estado init, renderiza e passa para o cliente <script type=application/json>{initState}</script>; assim os dados estarão no HTML. Hidrate / vincule eventos de IU à página chamando render no cliente. As páginas subsequentes são criadas pelo código js do cliente (buscando dados conforme necessário) e renderizadas pelo cliente. Dessa forma, qualquer atualização carregará novas páginas SSR e clicar em uma página será CSR. = isomorphic & SEO friendly
Federico
0

Fiquei muito confuso com isso hoje e, embora isso não seja uma resposta para o seu problema, usei essa abordagem. Eu queria usar o Express para roteamento em vez do React Router e não queria usar o Fibers porque não precisava de suporte a threading no nó.

Então, acabei de tomar a decisão de que, para os dados iniciais que precisam ser renderizados para o armazenamento de fluxo na carga, realizarei uma solicitação AJAX e passarei os dados iniciais para o armazenamento

Eu estava usando o Fluxxor para este exemplo.

Então, na minha rota expressa, neste caso, um /products rota:

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

Em seguida, meu método de renderização inicializar que passa os dados para a loja.

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

Sei que esta pergunta foi feita há um ano, mas tínhamos o mesmo problema e o resolvemos com promessas aninhadas derivadas dos componentes que serão renderizados. No final, tínhamos todos os dados do aplicativo e apenas os enviamos.

Por exemplo:

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 no roteador

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

Quero compartilhar com vocês minha abordagem de renderização do lado do servidor usando Flux, pouco ser simplificado, por exemplo:

  1. Digamos que temos componentcom os dados iniciais da loja:

    class MyComponent extends Component {
      constructor(props) {
        super(props);
        this.state = {
          data: myStore.getData()
        };
      }
    }
  2. Se a classe exigir alguns dados pré-carregados para o estado inicial, vamos criar o Loader para MyComponent:

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

    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. Agora basta carregar os dados no roteador:

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

assim como um breve rollup -> GraphQL resolverá isso totalmente para sua pilha ...

  • adicionar GraphQL
  • use apollo e react-apollo
  • use "getDataFromTree" antes de iniciar a renderização

-> getDataFromTree encontrará automaticamente todas as consultas envolvidas em seu aplicativo e as executará, pouplating seu cache de Apollo no servidor e, assim, habilitando o SSR totalmente funcional. BÄM

jebbie
fonte