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) => {
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) => {
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){
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);
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.