ReactJS, renderização no lado do servidor e algumas sutilezas do processamento de meta tags da página

Um dos problemas que você terá que resolver ao escrever um aplicativo de renderização no servidor está trabalhando com as metatags que todas as páginas devem ter, o que ajuda a indexá-las pelos mecanismos de pesquisa.

Começando no google, a primeira solução para a qual você será direcionado é provavelmente o React Helmet .

Uma das vantagens da biblioteca é que ela pode ser considerada isomórfica de alguma forma e pode ser perfeitamente usada no lado do cliente e no servidor.

class Page extends Component { render() { return ( <div> <Helmet> <title>Turbo Todo</title> <meta name="theme-color" content="#008f68" /> </Helmet> {/* ... */} </div> ); } } 

No servidor, o roteador ficará assim:

 app.get('/*', (req, res) => { const html = renderToString(<App />); const helmet = Helmet.renderStatic(); res.send(` <!doctype html> <html ${helmet.htmlAttributes.toString()}> <head> ${helmet.title.toString()} ${helmet.meta.toString()} </head> <body ${helmet.bodyAttributes.toString()}> <div id="app">${html}</div> </body> </html> `); }); 

Ambos os trechos são completamente corretos e eficientes, mas há um MAS, o código acima para o servidor é completamente síncrono e, portanto, completamente seguro, mas se se tornar assíncrono, ocultará erros de depuração difíceis:

 app.get('/*', async (req, res) => { // .... await anyAsyncAction(); //.... const helmet = Helmet.renderStatic(); // ... }); 

O problema aqui está principalmente na própria biblioteca do React Helmet e, em particular, no fato de coletar todas as tags dentro da Árvore do React e colocá-lo de fato em uma variável global, e como o código se tornou assíncrono, o código pode misturar solicitações processadas simultaneamente de usuários diferentes.

A boa notícia é que um garfo foi feito com base nessa biblioteca e agora é melhor dar preferência à biblioteca react-helmet-async . O principal paradigma é que, nesse caso, o contexto do capacete de reação será isolado dentro da estrutura de uma única solicitação, encapsulando o aplicativo React Tree no HelmetProvider:

 import { Helmet, HelmetProvider } from 'react-helmet-async'; app.get('/*', async (req, res) => {​ // ... code may content any async actions const helmetContext = {}; const app = ( <HelmetProvider context={helmetContext}> <App/> </HelmetProvider> ); // ...code may content any async actions const html = renderToString(app); const { helmet } = helmetContext; // ...code may content any async actions }); 

Isso pode ser concluído, mas talvez você vá além na tentativa de reduzir o desempenho máximo e melhorar algumas métricas de SEO. Por exemplo, você pode melhorar a métrica Tempo até o primeiro byte (TTFB) - quando o servidor pode enviar o layout da página com partes conforme são calculadas, em vez de esperar até que seja totalmente calculado. Para fazer isso, você começará a usar o renderToNodeStream em vez de renderToString .

Aqui estamos novamente diante de um pequeno problema. Para obter todas as metatags de que uma página precisa, precisamos passar por toda a árvore de reação do aplicativo, mas o problema é que as metatags devem ser enviadas antes do momento em que começarmos a transmitir conteúdo usando renderToNodeStream. De fato, precisamos calcular a Árvore de Reação duas vezes e é algo como isto:

 app.get('/*', async (req, res) => {​ const helmetContext = {}; let app = ( <HelmetProvider context={helmetContext}> <App/> </HelmetProvider> ); // do a first pass render so that react-helmet-async // can see what meta tags to render ReactDOMServer.renderToString(app); const { helmet } = helmetContext; response.write(` <html> <head> ${helmet.title.toString()}${helmet.meta.toString()} </head> <body> `); const stream = ReactDOMServer.renderToNodeStream(app); stream.pipe(response, { end: false }); stream.on('end', () => response.end('</body></html>')); }); 

Com essa abordagem, a necessidade dessa otimização se torna, em princípio, uma grande questão e é improvável que melhoremos a métrica TTFB que queremos alcançar.

Aqui podemos jogar um pouco de otimização e existem várias opções

  • em vez de renderToString, use renderToStaticMarkup, o que provavelmente ajudará, até certo ponto, a ganhar algum tempo
  • em vez de usar os renderizadores oferecidos pelo react da caixa, crie sua própria versão leve da passagem pela árvore do react, por exemplo, com base na biblioteca react-tree-walker , ou recuse-se a renderizar completamente a árvore e observe apenas o primeiro nível da árvore, sem prestar atenção aos componentes incorporados, portanto diga renderização superficial
  • considere um sistema de cache que às vezes pode pular a primeira caminhada pela árvore de reação

Mas, de qualquer forma, tudo o que é descrito parece muito sofisticado e, em princípio, põe em dúvida essa corrida pela eficiência, quando alguma arquitetura anormalmente complexa é construída em alguns milissegundos.

Parece-me neste caso, para aqueles que estão familiarizados com como extrair dados para renderização para o SSR (e se alguém não souber, este é um excelente artigo sobre este tópico), ajudaremos a seguir o mesmo caminho de extração de metatags para a página.

O conceito geral é que temos um arquivo de configuração para roteadores - essa é uma estrutura JS comum, que é uma matriz de objetos, cada um dos quais contém vários campos do tipo de componente , caminho . Com base no URL da solicitação, encontramos o roteador de que precisamos e o componente associado a ele no arquivo de configuração. Para esses componentes, definimos um conjunto de métodos estáticos, como loadData e, por exemplo, createMetatags para nossas metatags.

Assim, o próprio componente da página se tornará assim:

 class ProductPage extends React.Component { static createMetatags(store, request){ const item = selectItem(store, request.params.product_id); return [] .concat({property: 'og:description', content: item.desc}) .concat({property: 'og:title', content: item.title}) } static loadData(store, request){ // extract external data for SSR and return Promise } // the rest of component } 

Definimos um método estático createMetatags que cria o conjunto necessário de metatags. Com isso em mente, o código no servidor se tornará assim:

 app.get('/*', async (req, res) => {​​ const store = createStore(); const matchedRoutes = matchRoutes(routes, request.path); // load app state await Promise.all( matchedRoutes.reduce((promises, { route }) => { return route.component.loadData ? promises.concat(route.component.loadData(store, req)) : promises; }, []) ); // to get metatags const metaTags = matchedRoutes.reduce((tags, {route}) => { return route.component.createMetatags ? tags.concat(route.component.createMetatags(store, req)): tags }); res.write(`​ <html>​ <head>​ ${ReactDOMServer.renderToString(() => metaTags.map(tag => <meta {...tag}/>) )}​​ </head>​ <body>​ `);​ const stream = ReactDOMServer.renderToNodeStream(app);​ stream.pipe(response, { end: false });​ stream.on('end', () => response.end('</body></html>'));​ }); 

I.e. agora não precisamos renderizar a árvore React duas vezes - podemos extrair imediatamente tudo o que precisamos para trabalhar em um aplicativo isomórfico, por analogia com a extração de dados para uma rota.

Source: https://habr.com/ru/post/pt484456/


All Articles