Renderização no lado do servidor para o React App no ​​Express.js

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(); // CSS for all rendered React components const insertCss = (...styles) => styles.forEach(style => css.add(style._getCss())); const dynamicRoutes = [...routes]; dynamicRoutes[0].routes = [...dynamicRoutes[0].routes, ...StaticRoutesConfig]; const appContent = ReactDOMServer.renderToString( <StyleContext.Provider value={{ insertCss }}> <Provider store={store} key="provider"> <StaticRouter location={location} context={context}> <ReduxAsyncConnect routes={dynamicRoutes} helpers={helpers}/> </StaticRouter> </Provider> </StyleContext.Provider> ); const helmet = Helmet.renderStatic(); fs.readFile(indexFile, 'utf8', (err, data) => { if (err) { console.log('Something went wrong:', err); return res.status(500).send('Oops, better luck next time!'); } data = data.replace('__STYLES__', [...css].join('')); data = data.replace('__LOADER__', ''); data = data.replace('<div id=app></div>', `<div id=app>${appContent}</div>`); data = data.replace('<div id="app"></div>', `<div id="app">${appContent}</div>`); data = data.replace('<title></title>', helmet.title.toString()); data = data.replace('<meta name="description" content=""/>', helmet.meta.toString()); data = data.replace('<script>__INITIAL_DATA__</script>', `<script>window.__INITIAL_DATA__ = ${JSON.stringify(store.getState())};</script>`); return res.send(data); }); }); store.close(); }); }); 

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) => { // eslint-disable-next-line no-underscore-dangle const removeCss = styles.map(style => style._insertCss()); return () => removeCss.forEach(dispose => dispose()); }; export const browserRender = () => { const dynamicRoutes = [...routes]; dynamicRoutes[0].routes = [...dynamicRoutes[0].routes, ...StaticRoutesConfig]; hydrate( <StyleContext.Provider value={{ insertCss }}> <Provider key="provider" store={store} > <ConnectedRouter history={history}> <ReduxAsyncConnect helpers={{}} routes={dynamicRoutes} /> </ConnectedRouter> </Provider> </StyleContext.Provider>, document.getElementById('app'), ); }; 

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 .

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


All Articles