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();
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) => {
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 .