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.
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:
- Use a mágica
require.context
do webpack para “ler” o diretório e obter uma lista dinâmica que podemos usar - 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=()=> (); render ( {standardPages.map ((page)=> ( ))} {notFound && } , 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=(); hidratar (app, document.querySelector ('# app')); {Páginas .map (página=> ( ))}
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...