Inicio / Artículos

Renderizado del lado del servidor de una aplicación React 18 con Express y Babel

Convertir un proyecto Create React App a renderizado del lado del servidor con Express y Babel: renderToString, hydrateRoot, MemoryRouter, y luego cómo Remix y Gatsby realizan el SSR.

Editor: REACTAPP.TOP7 min de lectura1712 palabras

Fuentes: A Hands-on Guide for a Server-Side Rendering React 18 App

El renderizado del lado del servidor para React ha cambiado mucho desde la llegada de React 18. La configuración que se muestra a continuación está orientada a React 18, Create React App 5 y React Router 6, y parte de un proyecto base de Create React App que se convierte paso a paso. El renderizado debe ejecutarse en un servidor, por lo que se necesita un proceso Node. Node puro funciona, pero nodemon es útil durante el desarrollo ya que reinicia la aplicación cada vez que se modifica un archivo. La instalación global se realiza de la siguiente manera:

% npm install -g nodemon

CSR versus SSR en pocas líneas

Con el renderizado del lado del cliente (CSR), el navegador recibe una página HTML casi vacía y JavaScript completa el contenido. La primera carga puede ser lenta, pero la navegación posterior es rápida, y el servidor soporta poca carga ya que las bibliotecas de JavaScript realizan el trabajo. La desventaja es la optimización para motores de búsqueda, ya que un rastreador no encuentra contenido estático que leer.

Con el renderizado del lado del servidor, el navegador recibe en cambio una página HTML completa que el servidor ya ha renderizado. Esto beneficia al SEO y la primera carga es rápida. El tradeoff es que los cambios posteriores suelen requerir una recarga completa de la página, lo que puede aumentar la carga en el servidor.

Algunos términos relacionados son útiles aquí:

  • Una aplicación de página única (SPA) utiliza CSR. En lugar de servir HTML separado para cada ruta, dibuja cada ruta dinámicamente dentro del navegador.
  • JavaScript universal funciona tanto en el cliente como en el servidor. Renderiza HTML en el navegador como un SPA, renderiza el mismo HTML en el servidor y lo envía al navegador.
  • El código de React es universal por naturaleza, por lo que el código escrito para CSR puede gestionar SSR con la misma base de código.
  • SSR es en realidad más antiguo que CSR. Ha regresado en una forma moderna basada en JavaScript universal, y eso es lo que la gente suele entender por SSR hoy en día.

    Una aplicación básica de CSR

    Cree un proyecto nuevo:

    % yarn create react-app react-ssr
    % cd react-ssr
    

    Comiéncelo con npm start. En la pestaña Elements del navegador, el logotipo en rotación y el texto aparecen como HTML generado por JavaScript.

    Eso es la CSR típica: los scripts generan el contenido HTML. La pestaña Network muestra lo que realmente envió el servidor.

    El cuerpo contiene un paquete de JavaScript y no tiene contenido real, por lo que un rastreador casi no tiene nada significativo que indexar.

    Servir la versión de producción

    SSR necesita una versión de producción, así que la primera tarea es crear un pequeño servidor Express que sirva la salida compilada de React. La configuración de React lista para producción mencionada anteriormente sienta las bases. El archivo del servidor existe en dos formatos: uno es CommonJS, server/index.js, y el otro es un módulo ES, server/index.mjs.

    Ejecuta npm run build para generar el paquete compilado, y luego inicia el servidor con nodemon server o nodemon server/index.mjs. La pestaña Network ahora muestra un paquete de JavaScript (línea 12) y marcado vacío (línea 17). Todavía no se está generando contenido servido, por lo que sigue siendo CSR.

    Los tres pasos para SSR en Express

    Hacer que funcione el SSR dentro del servidor Express requiere tres pasos.

    Paso 1: hidratar en el cliente. ReactDOM.hydrateRoot() es similar a ReactDOM.createRoot(), con la firma hydrateRoot(container, element[, options]). Está diseñado para un contenedor cuyo HTML ya fue generado por ReactDOMServer. Dado que el nodo ya contiene marcado renderizado en el servidor, React lo mantiene y solo agrega manejadores de eventos, lo que acelera la carga inicial. La llamada se encuentra en la línea 7 de src/index.js.

    Paso 2: renderizar en el servidor. React incluye el objeto ReactDOMServer para convertir componentes en marcado estático, de modo que el navegador reciba una página ya llena de datos. Como el código de React es JavaScript universal, los mismos componentes funcionan en ambos lados.

    Paso 3: atender necesidades específicas de la página. Cada ruta puede requerir atención especial, como se mostrará en secciones posteriores.

    Hacer que Node entienda JSX

    Varios paquetes pueden hacer funcionar SSR. @babel/register es uno de los más sencillos. Instálalo junto con babel-plugin-transform-assets:

    % yarn add -D @babel/register
    % yarn add -D babel-plugin-transform-assets
    

    Ambos se incluyen en devDependencies:

    "devDependencies": {
      "@babel/register": "^7.18.9",
      "babel-plugin-transform-assets": "^1.0.2"
    }
    

    @babel/register compila los archivos en tiempo real y puede configurarse para transformar JSX y recursos. Existe un límite: no puede compilar módulos ES nativos de Node.js en tiempo real, ya que aún no existe una API estable para gestionar la carga de módulos ES. Por eso, los experimentos con SSR aquí utilizan archivos CommonJS.

    React 18 reestructuró sus APIs de servidor y las colocó en react-dom/server. renderToString sigue funcionando, aunque su soporte para Suspense es limitado. Las APIs más recientes, como renderToPipeableStream, brindan soporte completo para Suspense en el servidor, además de SSR por streaming.

    Recorrido por el servidor SSR

    A continuación se explica server/index.js actualizado, línea por línea.

    • Las líneas del 1 al 15 configuran Babel mediante el gancho require, de modo que los archivos se compilan a medida que se cargan.
  • La línea 2 selecciona dos presets. @babel/preset-env permite que el código utilice la versión más reciente de JavaScript sin tener que elegir manualmente las transformaciones sintácticas para cada entorno de destino. @babel/preset-react importa automáticamente las funciones a las que se compila JSX.
  • Las líneas 3 a 14 configuran babel-plugin-transform-assets, que define cómo se transforman los archivos de medios estáticos. Sin él, una etiqueta svg genera el error SyntaxError: Unexpected token '<'.
  • La línea 16 requiere React, la línea 17 requiere ReactDOMServer, y la línea 18 requiere la exportación por defecto de src/App.js.
  • El orden es importante aquí. app.use() en la línea 47 debe ejecutarse después de app.get() en las líneas 25 a 45. Si se ejecutara primero, serviría los archivos estáticos, incluido index.html, para la ruta raíz, y app.get() nunca tendría oportunidad de ejecutarse.

    La línea 26 registra la URL de cada solicitud. En una compilación de Create React App, la lista se ve así:

    Request URL = /
    Request URL = /static/js/main.b92cc87b.js
    Request URL = /static/css/main.073c9b0a.css
    Request URL = /static/media/logo.06e73328.svg
    Request URL = /static/css/main.073c9b0a.css.map
    Request URL = /static/js/main.b92cc87b.js.map
    Request URL = /static/media/logo.6ce24c58023cc2f8fd88fe9d219db6c6.svg
    Request URL = /favicon.ico
    Request URL = /manifest.json
    Request URL = /logo192.png
    

    Las líneas 27 a 29 se encargan de que solo la ruta raíz sea renderizada por app.get(), mientras que los recursos estáticos pasan al siguiente middleware en la línea 47.

    La línea 30 llama a ReactDOMServer.renderToString(element) para generar HTML en el servidor. Sería natural escribir directamente ReactDOMServer.renderToString(), pero entonces JSX tendría que estar en un archivo diferente al donde está configurado @babel-register, de ahí el uso de un elemento separado.

    La línea 31 muestra el marcado generado por el servidor:

    <div class="App"><header class="App-header"><img src="static/media/logo.06e73328.svg" class="App-logo" alt="logo"/><p>Edit <code>src/App.js</code> and save to reload.</p><a class="App-link" href="https://reactjs.org" target="_blank" rel="noopener noreferrer">Learn React</a></header></div>
    

    La línea 33 carga el index.html de producción, y las líneas 34 a 44 leen su contenido. Si no hay errores, el marcado generado (línea 42) se inserta en la etiqueta raíz, y el index.html final se convierte en la respuesta a la primera solicitud.

    Verificar que funcione

    Ejecuta nodemon server y vuelve a consultar la pestaña Network. El documento descargado ahora contiene marcado generado por el servidor. Las líneas 17 a 27 del cuerpo contienen toda la página, lo que proporciona a las herramientas de SEO algo real para analizar.

    Para una prueba más rigurosa, abre chrome://settings/content/javascript y desactiva JavaScript.

    La página sigue renderizándose sin JavaScript.

    Eso es SSR.

    Agregar rutas

    Una aplicación de una sola página es sencilla, por lo que la siguiente cuestión es cómo manejar múltiples rutas. Comienza instalando react-router-dom:

    % yarn add react-router-dom
    

    Se agrega a dependencies:

    "dependencies": {
      "react-router-dom": "^6.11.2"
    }
    

    Luego edite src/App.js. La línea 1 importa React, que es necesario para SSR. La línea 2 importa MemoryRouter en lugar de BrowserRouter. El router del navegador depende de la API de historial HTML5 pushState, la cual Node.js no proporciona. MemoryRouter almacena el historial de URLs en memoria sin afectar la barra de direcciones, lo cual es adecuado para pruebas y entornos que no son navegadores. StaticRouter de react-router-dom/server es otra opción, aunque no está diseñado para rutas complejas. Las líneas 34 a 40 implementan el uso de MemoryRouter.

    Construya nuevamente con npm run build, inicie nodemon server y visite http://localhost:8080.

    SSR funciona, pero con una particularidad: la URL en la barra de direcciones no se actualiza.

    Dónde resulta problemático implementar SSR manualmente

    El enrutamiento funciona teniendo en cuenta esa limitación, pero aún quedan otros problemas por resolver, como la obtención de datos y Redux. El trabajo del lado del servidor no es tan sencillo como el del cliente, y cada página puede requerir un manejo específico.

    No es necesario resolver todo esto a mano, ya que varios frameworks y bibliotecas incluyen SSR. Remix es un framework web full-stack que parte de los fundamentos de la web hasta llegar a la interfaz de usuario, con SSR y otras funcionalidades incluidas, sin código genérico. Su código del cliente llama a hydrateRoot en la línea 4, y su código del servidor llama a renderToPipeableStream en la línea 4.

    React 18 introdujo renderToPipeableStream(element, options). Al pasarle un elemento de React, genera el HTML inicial y devuelve un flujo de datos. Este flujo cuenta con pipe(res) para enviar el contenido resultante y abort() para cancelar la solicitud. Se admite plenamente el modo de suspensión, y el HTML con contenido entregado de forma diferida no presenta efectos visuales repentinos, ya que las etiquetas <script> incrustadas llenan los espacios en blanco posteriormente.

    En la pestaña Network, el cuerpo de la página de inicio de Remix ya contiene toda la página, por lo que se trata de SSR.

    Gatsby, Next.js, NuxtJS, Quasar y SvelteKit también ofrecen SSR y funciones relacionadas de forma predeterminada, sin código adicional, aunque cada uno define el SSR de manera ligeramente distinta. La página de inicio de Gatsby muestra el mismo contenido completo en la pestaña Network.

    Gatsby es un generador de sitios estáticos rápido y robusto que utiliza React para renderizar contenido estático, y te permite modificar el HTML estático mientras se procesa en el servidor.

    SSR o renderizado estático

    Cuando una página contiene solo datos estáticos, el renderizado estático es más rápido. Si la respuesta es dinámica, SSR es la mejor opción. A veces un enfoque híbrido es el más adecuado. SSR se ejecuta bajo demanda, cada vez que se solicita un archivo, mientras que el renderizado estático tiene lugar una sola vez durante la fase de construcción. Ambos métodos son adecuados para SEO, ya que en ambos casos el cuerpo del HTML contiene todo el contenido.

    Resumen

    Configurar SSR en Create React App se reduce a tres elementos clave:

    1. Mostrar el marcado renderizado por el servidor con ReactDOM.hydrateRoot().
    2. Renderizar componentes en marcado estático mediante el objeto ReactDOMServer, cuyas API sirven tanto para SSR como para el renderizado estático.
  • Gestionar todo lo que cada página requiere específicamente.
  • En un proyecto real, la opción sensata es utilizar un framework o biblioteca que ya haga esto.

    Want to Connect?
    
    If you are interested, check out my directory of web development articles.