Representación del lado del servidor para la aplicación React en Express.js

Para escribir este artículo, me llamó la atención la falta de un manual más o menos completo sobre cómo hacer Renderizado del lado del servidor para una aplicación React.

Cuando me encontré con este problema, tenía 2 opciones para hacerlo, ya sea con el marco Next.js o con Express.js .

Se dedicaron aproximadamente 100 horas a la investigación de Next.js para obtenerla para nuestra plataforma OTT grande lista para usar, pero hubo tantos problemas que la rechazamos (escribiré un artículo sobre esto nuevamente), había una opción para una pequeña, Express.js , sobre lo que quiero contar.

El código completo para la demostración en este artículo está aquí .

Comencemos con la tarea inicial y lo que teníamos.

Teníamos en ese momento:


Tareas

  • La solución debe renderizar completamente HTML con datos
  • Esta solución debería afectar mínimamente el código existente, es decir, la arquitectura debería permanecer con cambios mínimos.
  • Continuar usando bibliotecas que ya están en el proyecto.
  • Los metadatos de SEO deben provenir del servidor, junto con la página
  • Las imágenes, estilos y fuentes deben cargarse en el servidor y enviarse al cliente, sin descarga posterior
  • Soporte de importación dinámica del lado del cliente
  • Evite la duplicación de código para precargar en el servidor y al navegar en el lado del cliente

Hemos decidido las tareas, veamos cómo hacerlo.

A partir de la documentación de la reacción, podemos descubrir que para la SSR puede usar los métodos renderToString () e hydrate () , pero ¿qué hacer a continuación?

renderToString : se utiliza para generar HTML en el servidor de nuestra aplicación.
hidrato : se utiliza para la representación universal en el cliente y en el servidor.

Carga de datos


Para descargar datos en el lado del servidor, utilizamos la biblioteca redux-connect , que le permite descargar los datos necesarios antes de llamar al primer render, que es lo que necesitamos. Para hacer esto, use hoc asyncConnect. En el lado del servidor, carga datos y, cuando se enruta, funciona como componentDidMount.

@asyncConnect([ { key: 'usersFromServer', promise: async ({ store: { dispatch } }) => { await dispatch(getUsersData()); return Promise.resolve(); }, }, ]) 

Necesitamos crear una tienda redux en el lado del servidor. Todo está como siempre, solo cree en el archivo server.js.

También en el lado del servidor, usando el método loadOnServer de redux-connect, esperamos la precarga de datos.

Usando renderToString obtenemos el HTML de nuestra aplicación de datos.

Los datos que visitamos pueden recuperarse usando getState () y agregarse mediante la etiqueta <script /> al objeto de la ventana global. A partir de ese momento, en el cliente obtendremos los datos y los configuraremos en la puerta.

Todo se ve así

 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(); }); }); 

Se pasan 2 accesorios al componente ReduxAsyncConnect:

el primero son nuestras rutas, los segundos ayudantes (funciones auxiliares), a los que queremos estar accesibles en toda la aplicación, una especie de análogo de contexto.

Para el enrutamiento del servidor, use StaticRouter.

La biblioteca del casco se utiliza para agregar metaetiquetas seo. Cada componente de la página tiene una descripción con etiquetas.

Para que las etiquetas provengan del servidor de inmediato, se utiliza

 const helmet = Helmet.renderStatic(); helmet.title.toString() helmet.meta.toString() 

El enrutamiento tuvo que reescribirse en una matriz de objetos, se ve así.

 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, }, ]; 

Dependiendo de la url que viene al servidor, reaccionar enrutador devolvió la página de datos deseada.

¿Cómo se ve el cliente?

Aquí está el archivo principal del cliente. Puede agregar análisis para motores de búsqueda y código que debe ejecutarse para cada página en el lado del cliente.

browser / index.js

 import 'babel-polyfill'; import { browserRender } from '../app/app'; browserRender(); 

Archivo 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 el enrutamiento, se utiliza el ConnectedRouter de conectado-reaccionar-enrutador / inmutable .

Para la representación del lado del servidor, no podemos usar r eact-router-dom y describir adecuadamente nuestro enrutamiento a través de Switch:

 <Switch> <Route path="/about"> <About /> </Route> <Route path="/users"> <Users /> </Route> <Route path="/"> <Home /> </Route> </Switch> 

En cambio, como ya se mencionó, tenemos una matriz con las rutas descritas, y para que podamos agregarlas a la aplicación, necesitamos usar 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> ); } } 

Descargue carnets, estilos y fuentes en el servidor


Para los estilos, se utilizó el cargador de estilo isomorfo , ya que el cargador de software habitual no funciona en el paquete web con el destino: "nodo";

Agrega todos los estilos al DOM, por lo tanto, la página hermosa y preparada proviene del servidor. Puede guardar estilos en un archivo separado para que el navegador los pueda almacenar en caché.

Para mostrar imágenes y descargar fuentes en el servidor, se utilizó webpack loader base64-inline-loader .

 { test: /\.(jpe?g|png|ttf|eot|otf|svg|woff(2)?)(\?[a-z0-9=&.]+)?$/, use: 'base64-inline-loader?limit=1000&name=[name].[ext]', }, 

Se incluyó para imágenes y todo tipo de fuentes que utilizamos. Como resultado, recibimos un código del servidor base64 que mostraba una página con fuentes e imágenes sin carga posterior.

Para la compilación del cliente, se usaron el cargador de archivos y el url habituales.

 { test: /\.(eot|svg|otf|ttf|woff|woff2)$/, use: 'file-loader', }, { test: /\.(mp4|webm|png|gif)$/, use: { loader: 'url-loader', options: { limit: 10000, }, }, }, 

Sí, esto aumentó el tamaño de la página cargada, pero no fue muy notable para el usuario en términos de velocidad de descarga.

Importación dinámica en el servidor


React.js usa React.lazy y React.suspense para importar y mostrar dinámicamente el cargador, pero no funcionan para SSR.

Usamos react-loadable , que hace lo mismo.

 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/>; } } 

En el cliente, este código muestra el cargador, en el servidor, para que los módulos se carguen, debe agregar el siguiente código:

 Loadable.preloadAll().then(() => { app.listen(PORT, () => { console.log(` Server is listening on port ${PORT}`); }); }); 

Loadable.preloadAll (): devuelve una promesa que dice que los módulos están cargados.

Todos los aspectos destacados están resueltos.

Hice una mini demostración usando todo lo que se describió en el artículo .

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


All Articles