Para escrever este artigo, fui solicitado pela falta de um manual mais ou menos completo sobre como fazer a renderização no lado do servidor para um aplicativo React.
Quando me deparei com esse problema, eu tinha duas opções para fazer isso, com a estrutura
Next.js. ou usando o
Express.js .
Foram investidas cerca de 100 horas na pesquisa Next.js. para obtê-lo em nossa grande plataforma OTT pronta, mas houve muitos problemas que a recusamos (escreverei um artigo sobre isso novamente). Havia uma opção para uma pequena,
Express.js . sobre o que eu quero contar.
O código completo da demonstração deste artigo está
aqui .
Vamos começar com a tarefa inicial e o que tínhamos.
Nós tínhamos naquele tempo:
Tarefas:
- A solução deve renderizar completamente HTML renderizado com dados
- Essa solução deve afetar minimamente o código existente, ou seja, a arquitetura deve permanecer com alterações mínimas.
- Continue usando bibliotecas que já estão no projeto
- Os metadados de SEO devem vir do servidor, junto com a página
- Imagens, estilos e fontes devem ser carregados no servidor e enviados ao cliente, sem download subsequente
- Suporte à importação dinâmica do lado do cliente
- Evite a duplicação de código para pré-carregamento no servidor e ao navegar no lado do cliente
Decidimos as tarefas, vamos descobrir como fazer isso.
Na documentação da reação, podemos descobrir que, para o SSR, você pode usar os métodos
renderToString () e
hydrate () , mas o que fazer a seguir?
renderToString - usado para gerar HTML no servidor de nosso aplicativo.
hydrate - usado para renderização universal no cliente e no servidor.
Carregamento de dados
Para baixar dados no lado do servidor, usamos a biblioteca
redux-connect , que permite baixar os dados necessários, antes de chamar a primeira renderização, é disso que precisamos. Para fazer isso, use hoc asyncConnect. No lado do servidor, ele carrega dados e, ao rotear, funciona como componentDidMount.
@asyncConnect([ { key: 'usersFromServer', promise: async ({ store: { dispatch } }) => { await dispatch(getUsersData()); return Promise.resolve(); }, }, ])
Precisamos criar uma loja redux no lado do servidor. Tudo está como sempre, basta criar no arquivo server.js.
Também no lado do servidor, usando o método loadOnServer da
redux-connect, aguardamos o pré-carregamento dos dados.
Usando renderToString, obtemos o HTML do nosso aplicativo de dados.
Os dados que visitamos podem ser recuperados usando getState () e adicionados via tag <script /> ao objeto da janela global. A partir daí, no cliente, obteremos os dados e os definiremos no portão.
Tudo se parece com isso
app.get('*', (req, res) => { const url = req.originalUrl || req.url; const history = createMemoryHistory({ initialEntries: [url], }); const store = configureStore(initialState, history); const location = parseUrl(url); const helpers = {}; const indexFile = path.resolve('./build/main.html'); store.runSaga(sagas).toPromise().then(() => { return loadOnServer({ store, location, routes, helpers }) .then(() => { const context = {}; if (context.url) { req.header('Location', context.url); return res.send(302) } const css = new Set();
2 objetos são passados para o componente ReduxAsyncConnect:
a primeira são as nossas rotas, os segundos auxiliares (funções auxiliares), que queremos que sejam acessíveis em todo o aplicativo, uma espécie de análogo de contexto.
Para roteamento de servidor, use StaticRouter.
A biblioteca de
capacete é usada para adicionar metatags seo. Cada componente da página possui uma descrição com tags.
Para que as tags venham do servidor imediatamente, elas são usadas
const helmet = Helmet.renderStatic(); helmet.title.toString() helmet.meta.toString()
O roteamento teve que ser reescrito para uma matriz de objetos, parece com isso.
export const StaticRoutesConfig = [ { key: 'usersGender', component: UsersGender, exact: true, path: '/users-gender/:gender', }, { key: 'USERS', component: Users, exact: true, path: '/users', }, { key: 'main', component: Users, exact: true, path: '/', }, { key: 'not-found', component: NotFound, }, ];
Dependendo da URL que chega ao servidor, o roteador de reação retornou a página de dados desejada.
Como é o cliente?
Aqui está o arquivo principal do cliente. Você pode adicionar análises aos mecanismos de pesquisa e código que deve ser executado para cada página no lado do cliente.
browser / index.js import 'babel-polyfill'; import { browserRender } from '../app/app'; browserRender();
Arquivo
App.js const initialState = !process.env.IS_SERVER ? window.__INITIAL_DATA__ : {}; const history = process.env.IS_SERVER ? createMemoryHistory({ initialEntries: ['/'], }) : createBrowserHistory(); const store = configureStore(initialState, history); if (!process.env.IS_SERVER) { window.store = store; } const insertCss = (...styles) => {
Para o roteamento,
é usado o ConnectedRouter do
router de reação conectado / imutável .
Para renderização no servidor, não podemos usar r
eact-router-dom e descrever adequadamente nosso roteamento via Switch:
<Switch> <Route path="/about"> <About /> </Route> <Route path="/users"> <Users /> </Route> <Route path="/"> <Home /> </Route> </Switch>
Em vez disso, como já mencionado, temos uma matriz com as rotas descritas e, para poder adicioná-las ao aplicativo, precisamos usar o
react-router-config :
App / index.js import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { Helmet } from 'react-helmet'; import { renderRoutes } from 'react-router-config'; import { getRouterLocation } from './selectors/router'; @connect(state => ({ location: getRouterLocation(state), }), null) export default class App extends Component { static propTypes = { location: PropTypes.shape().isRequired, route: PropTypes.shape().isRequired, }; render() { const { route } = this.props; return ( <div> {renderRoutes(route.routes)} </div> ); } }
Baixe carnets, estilos e fontes no servidor
Para estilos,
foi usado o carregador de estilo isomórfico , pois o
carregador de slyle usual não funciona no webpack com o destino: "nó";
Ele adiciona todos os estilos ao DOM, assim a página bonita e pronta vem do servidor. Você pode salvar estilos em um arquivo separado, para que eles possam ser armazenados em cache pelo navegador.
Para exibir imagens e baixar fontes no servidor, o webpack loader
base64-inline-loader foi usado .
{ test: /\.(jpe?g|png|ttf|eot|otf|svg|woff(2)?)(\?[a-z0-9=&.]+)?$/, use: 'base64-inline-loader?limit=1000&name=[name].[ext]', },
Foi incluído para imagens e todos os tipos de fontes que usamos. Como resultado, recebemos um código do servidor base64 que exibia uma página com fontes e imagens sem carregamento subsequente.
Para a construção do cliente,
foram usados o carregador de URL e o carregador de arquivos habituais.
{ test: /\.(eot|svg|otf|ttf|woff|woff2)$/, use: 'file-loader', }, { test: /\.(mp4|webm|png|gif)$/, use: { loader: 'url-loader', options: { limit: 10000, }, }, },
Sim, isso aumentou o tamanho da página carregada, mas não foi muito perceptível para o usuário em termos de velocidade de download.
Importação dinâmica no servidor
O React.js usa
React.lazy e React.suspense para importar e exibir dinamicamente o carregador, mas eles não funcionam para SSR.
Usamos
load-react , que faz a mesma coisa.
import Loadable from 'react-loadable'; import Loader from './Loader'; const LoadableComponent = Loadable({ loader: () => import('./my-component'), loading: Loader, }); export default class App extends React.Component { render() { return <LoadableComponent/>; } }
No cliente, esse código exibe o carregador, no servidor, para que os módulos sejam carregados, é necessário adicionar o seguinte código:
Loadable.preloadAll().then(() => { app.listen(PORT, () => { console.log(` Server is listening on port ${PORT}`); }); });
Loadable.preloadAll () - retorna uma promessa que diz que os módulos estão carregados.
Todos os destaques estão resolvidos.
Fiz uma mini demonstração usando tudo o que foi
descrito no artigo .