Se você me perguntasse qual framework tem o melhor modelo de componente, eu diria React sem hesitar.

Existem várias razões para isso. Por um lado, React fabrica componentes sem muita cerimônia e compromissos. O modelo também é bastante independente do React. Com a nova fábrica JSX, você não veria nem mesmo uma importação do React no TypeScript ou Babel.

Obviamente, deve haver uma advertência quando se trata de construir cada aplicativo de front-end exclusivamente no React. Além da óbvia discussão religiosa sobre se React é realmente a maneira certa de fazer frontend, o elefante na sala é que React é… afinal… apenas JavaScript.

É uma biblioteca JavaScript e requer JavaScript para ser executado. Isso significa tempos de download mais longos, uma página mais inchada e, presumivelmente, uma classificação de SEO não muito boa.

Opções para usar React sem JavaScript

Então, o que podemos fazer para melhorar a situação? Uma reação inicial pode ser uma das seguintes:

“Ah, sim, vamos apenas usar um servidor Node.js, fazendo SSR também.”

“Não há algo que já gere marcação estática? Quem é aquele Gatsby? ”

“Hmm, eu quero tudo, mas obviamente uma estrutura SSR fazendo coisas do tipo Gatsby seria bom. Este é o nível Next.js, certo? ”

“Parece muito complicado. Vamos usar o Jekyll novamente. ”

“Por quê ?! Vamos dar as boas-vindas a todos em 2021-servir qualquer página como um SPA é legal. ”

Talvez você tenda a se identificar com um desses. Pessoalmente, dependendo do problema, eu escolheria um dos dois últimos. Claro, fiz bastante renderização do lado do servidor (SSR) com React, mas muitas vezes descobri que a complexidade adicional não valia o esforço.

Da mesma forma, posso ser uma das poucas pessoas que realmente não gosta de Gatsby. Para mim, pelo menos, complicou quase tudo e não vi muito ganho.

Então é isso? Bem, há outra maneira, é claro. Se colocarmos nosso aplicativo em uma arquitetura sólida, poderíamos apenas escrever um pequeno script e realmente executar a geração estática do site (SSG) nós mesmos-sem Gatsby ou qualquer outra coisa necessária. Isso não será tão sofisticado; no entanto, devemos ver alguns bons ganhos.

Agora, o que realmente esperamos aqui?

Definindo as expectativas certas

Neste post, iremos construir uma solução simples para transformar nossa página criada usando React em um conjunto totalmente pré-gerado de sites estáticos. Ainda seremos capazes de hidratar isso e deixar nosso site dinâmico.

Nosso objetivo era melhorar o desempenho de renderização inicial. Em nosso teste do Farol, vimos que nossa página inicial nem sempre foi tão bem vista quanto esperávamos.

Reagir SSR antes de

O que não faremos é otimizar as páginas estáticas para que tenham apenas pequenos fragmentos de JavaScript. Sempre hidrataremos a página com o JavaScript completo (que ainda pode ser lento, mas falaremos mais sobre isso depois).

Mesmo que uma pré-renderização estática de um SPA possa ser benéfica para o desempenho percebido, não nos concentraremos nas otimizações de desempenho. Se estiver interessado em otimização de desempenho, você deve dar uma olhada neste guia detalhado para otimização de desempenho com webpack .

Um aplicativo básico do React

Como padrão para este artigo, usamos um tipo de aplicativo React bastante simples, mas bastante comum. Instalamos várias dependências de desenvolvimento (sim, usaremos TypeScript para fins de transpilação):

 npm i webpack webpack-dev-server webpack-cli typescript ts-loader file-loader html-webpack-plugin @ types/react @ types/react-dom @ types/react-router @ types/react-router-dom--save-dev

E, é claro, algumas dependências de tempo de execução:

 npm i react-dom react-router-dom react-router--save

Agora configuramos um webpack.config.js adequado para empacotar o aplicativo.

 module.exports={ modo:'produção', devtool:'mapa-fonte', entrada:'./src/index.tsx', resultado: { nome do arquivo:'app.js', }, resolver: { extensões: ['.ts','.tsx','.js'], }, módulo: { regras: [ {test:/\.tsx?$/, loader:'ts-loader'}, {test:/\.(png|jpe?g|gif)$/i, loader:'file-loader'}, ], },
};

Isso suportará TypeScript e arquivos como ativos. Observe que, para um aplicativo da web maior, podemos precisar de muitas outras coisas, mas isso é suficiente para nossa demonstração.

Também devemos adicionar algumas páginas com um pouco de conteúdo para ver isso funcionando. Em nossa estrutura, usaremos um arquivo index.tsx para agregar tudo. Este arquivo pode ser tão simples quanto:

 import * as React from'react';
import {BrowserRouter, Route, Switch} de'react-router-dom';
import {render} from'react-dom'; const HomePage=React.lazy (()=> importar ('./pages/home'));
const FirstPage=React.lazy (()=> import ('./pages/first'));
const NotFoundPage=React.lazy (()=> import ('./pages/not-found')); const App=()=> (       
); render (, document.querySelector ('# app'));

O problema com essa abordagem é que precisamos editar esse arquivo para cada nova página. Se usarmos a convenção de armazenar as páginas no diretório src/pages , podemos criar algo melhor. Temos duas opções:

  1. Use a mágica require.context do webpack para “ler” o diretório e obter uma lista dinâmica que podemos usar
  2. Use um módulo especial gerado pelo tempo de compilação que nos mostra o carregamento lento e as rotas

A segunda abordagem oferece a grande vantagem de que as rotas podem fazer parte das páginas. Para esta abordagem, precisaremos de outro carregador de webpack:

 npm i parcel-codegen-loader--save-dev

Isso agora nos permite refatorar o arquivo index.tsx para se parecer com:

 import * as React from'react';
import {BrowserRouter, Route, Switch} de'react-router-dom';
import {render} from'react-dom';
importar Layout de'./Layout'; páginas const=require ('./toc.codegen');
const [notFound]=pages.filter ((m)=> m.route==='*');
const standardPages=pages.filter ((m)=> m!==notFound); const App=()=> (    {standardPages.map ((page)=> (  ))} {notFound && }   
); render (, document.querySelector ('# app'));

Agora, todas as páginas são totalmente determinadas por toc.codegen , que é um módulo que é gerado durante o empacotamento. Além disso, adicionamos um componente Layout para dar às nossas páginas um pouco de estrutura compartilhada.

O gerador de código para o módulo toc se parece com este:

 const {getPages}=require ('./helpers'); module.exports=()=> { const pageDetails=getPages (); const pages=pageDetails.map ((page)=> { const meta=[ `"content": lazy (()=> import ('./$ {page.folder}/$ {page.name}'))`, `$ {JSON.stringify ('route')}: $ {JSON.stringify (page.route)}`, ]; return `{$ {meta.join (',')}}`; }); return `const {lazy}=require ('react');
module.exports=[$ {pages.join (',')}]; `;
};

Nós apenas iteramos em todas as páginas e geramos um novo módulo, exportando uma matriz de objetos com as propriedades route e content .

Neste ponto, nosso objetivo é pré-renderizar este aplicativo simples (mas completo).

O básico do SSG com React

Se você conhece SSR com React, já sabe tudo o que precisa para fazer alguns SSG básicos. Basicamente, usamos a função renderToString de react-dom/server em vez de render de react-dom . Supondo que temos uma página aninhada em um layout, o seguinte código pode já funcionar:

elemento

 const=(     
);
const content=renderToString (elemento);

No snippet acima, assumimos que o layout é totalmente fornecido em um componente Layout . Também assumimos que o React Router é usado para roteamento do lado do cliente. Portanto, precisamos fornecer um contexto de roteamento adequado. Felizmente, realmente não importa se usamos HashRouter , BrowserRouter ou MemoryRouter -todos fornecem um contexto de roteamento.

Agora, o que é Page ? Página refere-se ao componente que realmente exibe a página a ser pré-renderizada. Você pode ter mais componentes que gostaria de trazer em cada página. Um bom exemplo seria um componente ScrollToTop , que normalmente seria integrado como:

 

Mas este seria um componente que deixaríamos para a hidratação quando nossa página estática se tornasse dinâmica. Não há necessidade de pré-renderizar isso.

Caso contrário, o próprio aplicativo normalmente seria semelhante a:

 const app=(     {Páginas .map (página=> (  ))}    
); hidratar (app, document.querySelector ('# app'));

Isso é muito próximo ao snippet para geração estática. A principal diferença é que aqui incluímos todas as páginas (e hidratamos), enquanto no cenário SSG acima, reduzimos todo o conteúdo da página a uma única página fixa.

Armadilhas do SSG

Bem, até agora tudo parece simples e fácil, certo? Mas os problemas estão ocultos.

Suporte para ativos

Como nos referimos a ativos? Digamos que usamos algum código como:

 

Isso pode funcionar, visto que foo.png existe em nosso servidor. Usar URLs completos seria um pouco mais confiável, por exemplo, http://example.com/foo.png . No entanto, abriríamos mão de alguma flexibilidade em relação ao meio ambiente.

Frequentemente, deixamos esses ativos para empacotadores como o webpack de qualquer maneira. Nesse caso, teríamos um código como:

 

Aqui, foo.png é resolvido localmente no momento da construção, então hash, otimizado e copiado para o diretório de destino. Está tudo bem. Para o processo descrito acima, no entanto, apenas exigir o módulo em Node.js não funcionará. Encontraremos uma exceção de que foo.png não é um módulo válido.

Resolver isso não é complicado. Felizmente, podemos registrar extensões adicionais para módulos em Node.js:

 ['.png','.svg','.jpg','.jpeg','.mp4','.mp3','.woff','.tiff','.tif','.xml']. forEach (extension=> { require.extensions [extension]=(módulo, arquivo)=> { module.exports='/'+ nome de base (arquivo); };
});

O código acima assume que o arquivo já está/será copiado para o diretório raiz. Portanto, transformaríamos require ('../assets/foo.png') em "foo.png".

E quanto à parte do hashing? Se já temos uma lista de arquivos de ativos com seu hash, poderíamos usar um código como este:

 const parts=basename (file).split ('.');
const ext=parts.pop ();
const front=parts.join ('.');
const ref=files.filter (m=> m.startsWith (front) && m.endsWith (ext)). pop () ||'';
module.exports='/'+ ref;

Isso tentaria encontrar um arquivo começando com “foo” e terminando com “png,” como foo.23fa6b.png . Descanse como acima.

Suporte para TypeScript

Agora que sabemos como lidar com ativos genéricos, também podemos fornecer um mecanismo de tratamento para arquivos .ts e .tsx . Afinal, eles também podem ser transpilados para JS na memória. Todo esse trabalho é totalmente desnecessário, entretanto, uma vez que ts-node já faz isso. Portanto, nosso trabalho se reduz essencialmente a:

 npm i ts-node--save-dev

Em seguida, registre o manipulador para as respectivas extensões de arquivo usando:

 require ('ts-node'). register ({ compilerOptions: { módulo:'commonjs', alvo:'es6', jsx:'react', importHelpers: true, moduleResolution:'node', }, transpileOnly: true,
});

Agora, isso permite apenas exigir módulos que são definidos como arquivos .tsx .

Carregamento lento

Se escrevemos um React SPA otimizado, também agruparemos a divisão de nosso aplicativo no nível de roteamento. Isso significa que cada página recebe seu próprio subconjunto. Como tal, haverá um pacote central/comum (geralmente contendo o mecanismo de roteamento, o próprio React, algumas dependências compartilhadas, etc.) e um pacote para cada página. Se tivéssemos 10 páginas, acabaríamos com 11 (ou mais) pacotes.

Mas usando uma abordagem de pré-renderização, isso não é tão ideal. Afinal, já estamos pré-renderizando páginas individuais. E se tivéssemos um subcomponente em uma página (por exemplo, um controle de mapa) que seria compartilhado, mas é tão grande que gostaríamos de colocá-lo em seu próprio pacote?

Ou seja, temos um componente como:

 const ActualMap=React.lazy (()=> import ('./actual-map')); Mapa const=(props)=> (  Carregando mapa... 
}> );

Onde o componente real será carregado lentamente. Nesse cenário, atingiremos uma parede bastante drástica. O React não sabe como pré-renderizar lazy .

Felizmente, poderíamos redefinir preguiçoso para apenas exibir algum espaço reservado (também poderíamos enlouquecer aqui e realmente carregar ou pré-renderizar o conteúdo, mas vamos mantê-lo simples e assumir que tudo o que fica preguiçoso-carregado aqui deve ser carregado lentamente mais tarde):

 React.lazy=()=> ()=> React.createElement ('div', indefinido,'Carregando...');

A outra parte que não está disponível para uso em renderToString é o Suspense . Bem, honestamente, uma implementação disso não faria mal, mas vamos fazer isso nós mesmos.

Tudo o que precisamos fazer aqui é agir como se Suspense não estivesse lá. Portanto, apenas o substituímos por um fragmento usando os filhos fornecidos.

 React.Suspense=({children})=> React.createElement (React.Fragment, undefined, children);

Agora lidamos com o carregamento lento com bastante eficiência, embora, dependendo do cenário, possamos (e talvez até queiramos) fazer muito mais do que isso.

Outras coisas

Além de coisas como preguiçoso e Suspense , outras partes podem estar faltando no React também. Um bom exemplo é useLayoutEffect . No entanto, como useLayoutEffect normalmente só se aplicaria em tempo de execução, a maneira mais fácil de oferecer suporte é evitá-lo por completo.

Uma implementação bastante simples é substituí-lo por uma função autônoma. Dessa forma, useLayoutEffect não está no nosso caminho e é simplesmente ignorado:

 React.useLayoutEffect=()=> {};

Alguns dos módulos que exigimos não estarão no formato CommonJS. Isto é um grande problema. No momento em que este artigo foi escrito, Node.js oferece suporte apenas a módulos CommonJS ( módulos ES ainda são experimentais ).

Felizmente, existe o pacote esm , que nos dá suporte para módulos ES usando import... e export... instruções. Ótimo!

Como com ts-node , começaríamos instalando a dependência:

 npm i esm--save-dev

E então realmente usando. Nesse caso, precisamos substituir o require “normal” por uma nova versão dele. O código lê:

 require=require ('esm') (módulo);

Agora também temos suporte para módulos ES. A única coisa que está faltando pode ser alguma funcionalidade relacionada ao DOM.

Embora na maioria das vezes devamos colocar condicionais em nosso código da seguinte maneira:

 if (typeof window!=='undefined') { //...
}

Também poderíamos falsificar alguns outros globais. Por exemplo, nós (ou alguma dependência que usamos direta ou indiretamente) podemos nos referir a XMLHttpRequest , XDomainRequest ou localStorage . Nesses casos, poderíamos zombar deles estendendo o objeto global .

Com a mesma lógica, podemos simular o documento , ou pelo menos partes dele. Existe, é claro, o pacote jsdom , que já ajuda com a maioria deles . Mas vamos mantê-lo simples e direto ao ponto por enquanto.
Apenas um exemplo de zombaria:

 global.XMLHttpRequest=class {};
global.XDomainRequest=class {};
global.localStorage={ getItem () { return undefined; }, setItem () {},
};
global.document={ título:'amostra', querySelector () { Retorna { getAttribute () { Retorna''; }, }; },
};

Agora temos tudo para começar a pré-renderizar nosso aplicativo sem muitos problemas.

Reunindo tudo

Existem muitas maneiras de generalizar e usar os snippets acima. Pessoalmente, gosto de usá-los em um módulo separado, que é avaliado/usado por página usando fork . Dessa forma, sempre obtemos processos isolados, que também podem ser paralelizados.

Um exemplo de implementação é mostrado abaixo:

 const path=require ('path');
const {readFileSync, readdirSync}=require ('fs');
const {fork}=require ('child_process');
const {getPages}=require ('./arquivos'); function generatePage (página, arquivos, html, dist) { const modPath=path.resolve (__ dirname,'ssg-kernel.js'); console.log (`Processando a página"$ {page.name}"...`); retornar nova promessa ((resolver, rejeitar)=> { const ps=fork (modPath, [], { cwd: process.cwd (), stdio:'ignore', destacado: verdadeiro, }); ps.on ('mensagem', ()=> { console.log (`Página de processamento finalizada"$ {page.name}".`); resolver(); }); ps.on ('erro', ()=> rejeitar (`Falha ao processar a página"$ {page.name}".`)); ps.send ({ fonte: page.path, target: page.route, arquivos, html, dist, }); });
} function generatePages () { const dist=path.resolve (__ dirname,'dist'); índice const=path.resolve (dist,'index.html'); arquivos const=readdirSync (dist); const html=readFileSync (index,'utf8'); const baseDir=path.resolve (__ dirname,'..'); páginas const=getPages (); return Promise.all (pages.map (page=> generatePage (page, files, html, dist)));
} if (módulo require.main===) { generatePages () .então( ()=> 0, ()=> 1, ) .então (processo.exit);
} outro { module.exports={ generatePage, generatePages, };
}

Esta implementação pode ser usada como uma lib e também diretamente por meio do . É por isso que distinguimos entre os dois casos usando require.main como discriminador. No caso de uma lib, exportamos as duas funções generatePages e generatePage .

A única coisa que resta aqui é a definição de getPages . Para isso, poderíamos apenas usar a definição que especificamos na seção introdutória sobre um aplicativo React básico.

O módulo ssg-kernel.js conteria o código acima. Embrulhado em um envelope adequado para uso em um processo bifurcado, terminamos com:

//... process.on ('mensagem', msg=> { const {origem, destino, arquivos, html, dist}=msg; setupExtensions (arquivos); setTimeout (()=> { const {content, outPath}=renderApp (fonte, destino, dist); makePage (outPath, html, conteúdo); process.send ({ conteúdo, }); }, 100);
});

Onde setupExtensions configuraria as modificações necessárias para require , enquanto renderApp faz a geração de marcação real. makePage usa a saída de renderApp para realmente escrever a página gerada.

Usando essa configuração, poderíamos pré-renderizar nosso site e obter um aumento significativo no desempenho, conforme confirmado pelo Lighthouse:

Reagir SSR depois

Como um bom efeito colateral, nosso próprio site agora também funciona-até certo ponto-para usuários sem JavaScript. O aplicativo de exemplo completo pode ser encontrado no GitHub .

Conclusão

Usar o React para criar ótimas páginas estáticas não é grande coisa. Não há necessidade de recorrer a estruturas inchadas e complicadas. Dessa forma, podemos controlar muito bem o que entra e o que precisa ficar de fora.

Além disso, aprendemos um pouco sobre as partes internas do Node.js, React e, potencialmente, algumas das dependências que usamos. Saber o que realmente usamos é mais do que um exercício acadêmico, mas vital em caso de bugs ou outros problemas.

O ganho de desempenho de pré-renderizar um SPA com esta técnica pode ser bom. Mais importante, nossa página se torna mais acessível e tem uma classificação mais elevada para fins de SEO.

Onde você vê o brilho da pré-renderização? É apenas um exercício sem sentido ou o React também pode ser um bom modelo de desenvolvimento para escrever componentes reutilizáveis ​​sem interatividade?

A postagem Geração de site estático com React do zero apareceu primeiro no LogRocket Blog .

Source link