Ir al contenido principal
Traducción Beta No Oficial

Esta página fue traducida por PageTurner AI (beta). No está respaldada oficialmente por el proyecto. ¿Encontraste un error? Reportar problema →

Renderizado del Lado del Servidor

El caso de uso más común para el renderizado del lado del servidor es manejar el renderizado inicial cuando un usuario (o un rastreador de motores de búsqueda) solicita nuestra aplicación por primera vez. Cuando el servidor recibe la solicitud, renderiza los componentes necesarios en una cadena HTML y luego la envía como respuesta al cliente. A partir de ese momento, el cliente se encarga del renderizado.

En los siguientes ejemplos usaremos React, pero las mismas técnicas pueden aplicarse a otros frameworks de vistas que soporten renderizado en el servidor.

Redux en el Servidor

Al usar Redux con renderizado del lado del servidor, debemos enviar el estado de nuestra aplicación en la respuesta para que el cliente pueda usarlo como estado inicial. Esto es crucial porque si pre-cargamos datos antes de generar el HTML, queremos que el cliente también tenga acceso a ellos. De lo contrario, el marcado generado en el cliente no coincidirá con el del servidor, y el cliente tendría que cargar los datos nuevamente.

Para enviar los datos al cliente, necesitamos:

  • crear una nueva instancia del store de Redux en cada solicitud;

  • opcionalmente despachar algunas acciones;

  • extraer el estado del store;

  • y luego pasar este estado al cliente.

En el lado del cliente, se creará un nuevo store de Redux inicializado con el estado proporcionado por el servidor. La única función de Redux en el servidor es proporcionar el estado inicial de nuestra aplicación.

Configuración

En esta guía veremos cómo configurar el renderizado del lado del servidor. Usaremos la sencilla aplicación de Contador como ejemplo, mostrando cómo el servidor puede renderizar el estado anticipadamente según la solicitud.

Instalar Paquetes

Para este ejemplo usaremos Express como servidor web básico. También necesitamos instalar los bindings de React para Redux, ya que no vienen incluidos por defecto en Redux.

npm install express react-redux

El Lado del Servidor

Este es el esquema básico de nuestro servidor. Configuraremos un middleware de Express usando app.use para manejar todas las solicitudes. Si no estás familiarizado con Express o los middlewares, simplemente entiende que nuestra función handleRender se invocará en cada solicitud al servidor.

Además, como usamos sintaxis moderna de JS y JSX, necesitaremos compilar con Babel (ver este ejemplo de servidor Node con Babel) y el preset de React.

server.js
import path from 'path'
import Express from 'express'
import React from 'react'
import { createStore } from 'redux'
import { Provider } from 'react-redux'
import counterApp from './reducers'
import App from './containers/App'

const app = Express()
const port = 3000

// Serve static files
app.use('/static', Express.static('static'))

// This is fired every time the server side receives a request
app.use(handleRender)

// We are going to fill these out in the sections to follow
function handleRender(req, res) {
/* ... */
}
function renderFullPage(html, preloadedState) {
/* ... */
}

app.listen(port)

Manejo de la Solicitud

Lo primero que debemos hacer en cada solicitud es crear una nueva instancia del store de Redux. El único propósito de este store es proporcionar el estado inicial de nuestra aplicación.

Al renderizar, envolveremos <App /> (nuestro componente raíz) dentro de un <Provider> para que el store esté disponible en todo el árbol de componentes, como vimos en "Fundamentos de Redux" Parte 5: UI y React.

El paso clave en el renderizado del lado del servidor es generar el HTML inicial de nuestro componente antes de enviarlo al cliente. Para esto usamos ReactDOMServer.renderToString().

Luego obtenemos el estado inicial de nuestro store de Redux usando store.getState(). Veremos cómo pasar este estado en nuestra función renderFullPage.

import { renderToString } from 'react-dom/server'

function handleRender(req, res) {
// Create a new Redux store instance
const store = createStore(counterApp)

// Render the component to a string
const html = renderToString(
<Provider store={store}>
<App />
</Provider>
)

// Grab the initial state from our Redux store
const preloadedState = store.getState()

// Send the rendered page back to the client
res.send(renderFullPage(html, preloadedState))
}

Inyectar HTML inicial del componente y estado

El último paso en el lado del servidor es inyectar el HTML inicial de nuestro componente y el estado inicial en una plantilla para su renderizado en el cliente. Para transferir el estado, añadimos una etiqueta <script> que adjuntará preloadedState a window.__PRELOADED_STATE__.

El preloadedState estará entonces disponible en el cliente accediendo a window.__PRELOADED_STATE__.

También incluimos nuestro archivo bundle para la aplicación del cliente mediante una etiqueta de script. Este será el resultado que proporcione tu herramienta de bundling para el punto de entrada del cliente. Puede ser un archivo estático o una URL a un servidor de desarrollo con hot reloading.

function renderFullPage(html, preloadedState) {
return `
<!doctype html>
<html>
<head>
<title>Redux Universal Example</title>
</head>
<body>
<div id="root">${html}</div>
<script>
// WARNING: See the following for security issues around embedding JSON in HTML:
// https://redux.js.org/usage/server-rendering#security-considerations
window.__PRELOADED_STATE__ = ${JSON.stringify(preloadedState).replace(
/</g,
'\\u003c'
)}
</script>
<script src="/static/bundle.js"></script>
</body>
</html>
`
}

El lado del cliente

El lado del cliente es muy sencillo. Solo necesitamos obtener el estado inicial de window.__PRELOADED_STATE__ y pasarlo a nuestra función createStore() como estado inicial.

Veamos nuestro nuevo archivo de cliente:

client.js

import React from 'react'
import { hydrate } from 'react-dom'
import { createStore } from 'redux'
import { Provider } from 'react-redux'
import App from './containers/App'
import counterApp from './reducers'

// Create Redux store with state injected by the server
const store = createStore(counterApp, window.__PRELOADED_STATE__)

// Allow the passed state to be garbage-collected
delete window.__PRELOADED_STATE__

hydrate(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)

Puedes configurar tu herramienta de compilación preferida (Webpack, Browserify, etc.) para generar un archivo bundle en static/bundle.js.

Cuando la página cargue, el archivo bundle se iniciará y ReactDOM.hydrate() reutilizará el HTML renderizado por el servidor. Esto conectará nuestra nueva instancia de React con el DOM virtual usado en el servidor. Como tenemos el mismo estado inicial para nuestro almacén Redux y usamos el mismo código para nuestros componentes de vista, el resultado será el mismo DOM real.

¡Y eso es todo! Eso es lo único necesario para implementar el renderizado en el lado del servidor.

Pero el resultado es bastante básico. Básicamente renderiza una vista estática a partir de código dinámico. Lo siguiente que necesitamos es construir un estado inicial de forma dinámica para que la vista renderizada pueda ser dinámica.

información

Recomendamos pasar window.__PRELOADED_STATE__ directamente a createStore y evitar crear referencias adicionales al estado precargado (por ejemplo, const preloadedState = window.__PRELOADED_STATE__) para que pueda ser recolectado por el recolector de basura.

Preparar el estado inicial

Como el cliente ejecuta código continuamente, puede comenzar con un estado inicial vacío y obtener cualquier estado necesario bajo demanda y progresivamente. En el servidor, el renderizado es síncrono y solo tenemos una oportunidad para renderizar nuestra vista. Debemos ser capaces de compilar nuestro estado inicial durante la solicitud, que deberá reaccionar a la entrada y obtener estado externo (como el de una API o base de datos).

Procesar parámetros de solicitud

La única entrada para el código del servidor es la solicitud realizada al cargar una página de tu aplicación en el navegador. Puedes configurar el servidor durante su arranque (como al ejecutarse en entorno de desarrollo vs producción), pero esa configuración es estática.

La solicitud contiene información sobre la URL solicitada, incluyendo parámetros de consulta, útiles al usar algo como React Router. También puede contener encabezados con entradas como cookies o autorización, o datos del cuerpo POST. Veamos cómo configurar el estado inicial del contador basado en un parámetro de consulta.

server.js

import qs from 'qs' // Add this at the top of the file
import { renderToString } from 'react-dom/server'

function handleRender(req, res) {
// Read the counter from the request, if provided
const params = qs.parse(req.query)
const counter = parseInt(params.counter, 10) || 0

// Compile an initial state
let preloadedState = { counter }

// Create a new Redux store instance
const store = createStore(counterApp, preloadedState)

// Render the component to a string
const html = renderToString(
<Provider store={store}>
<App />
</Provider>
)

// Grab the initial state from our Redux store
const finalState = store.getState()

// Send the rendered page back to the client
res.send(renderFullPage(html, finalState))
}

El código lee del objeto Request de Express pasado a nuestro middleware. El parámetro se analiza como número y se establece en el estado inicial. Si visitas http://localhost:3000/?counter=100 en tu navegador, verás que el contador comienza en 100. En el HTML renderizado, verás el contador mostrando 100 y la variable __PRELOADED_STATE__ tendrá ese valor.

Obtención asíncrona de estado

El problema más común con el renderizado en el servidor es manejar estados que llegan de forma asíncrona. El renderizado en servidor es inherentemente síncrono, por lo que es necesario transformar cualquier obtención asíncrona en una operación síncrona.

La forma más sencilla de hacer esto es pasar un callback a tu código síncrono. En este caso, será una función que referencie el objeto de respuesta y envíe el HTML renderizado al cliente. No te preocupes, no es tan complicado como parece.

Para nuestro ejemplo, imaginaremos que hay un almacén de datos externo que contiene el valor inicial del contador (Counter As A Service, o CaaS). Simularemos una llamada a este servicio y construiremos nuestro estado inicial con el resultado. Comenzaremos creando nuestra llamada API:

api/counter.js

function getRandomInt(min, max) {
return Math.floor(Math.random() * (max - min)) + min
}

export function fetchCounter(callback) {
setTimeout(() => {
callback(getRandomInt(1, 100))
}, 500)
}

Nuevamente, esto es solo una API simulada, así que usamos setTimeout para simular una solicitud de red que tarda 500 milisegundos en responder (esto debería ser mucho más rápido con una API real). Pasamos un callback que devuelve un número aleatorio de forma asíncrona. Si usas un cliente API basado en Promesas, ejecutarías este callback en el manejador then.

En el lado del servidor, simplemente envolvemos nuestro código existente en fetchCounter y recibimos el resultado en el callback:

server.js

// Add this to our imports
import { fetchCounter } from './api/counter'
import { renderToString } from 'react-dom/server'

function handleRender(req, res) {
// Query our mock API asynchronously
fetchCounter(apiResult => {
// Read the counter from the request, if provided
const params = qs.parse(req.query)
const counter = parseInt(params.counter, 10) || apiResult || 0

// Compile an initial state
let preloadedState = { counter }

// Create a new Redux store instance
const store = createStore(counterApp, preloadedState)

// Render the component to a string
const html = renderToString(
<Provider store={store}>
<App />
</Provider>
)

// Grab the initial state from our Redux store
const finalState = store.getState()

// Send the rendered page back to the client
res.send(renderFullPage(html, finalState))
})
}

Como llamamos a res.send() dentro del callback, el servidor mantendrá abierta la conexión y no enviará datos hasta que se ejecute el callback. Notarás que ahora se añade un retraso de 500ms a cada solicitud del servidor debido a nuestra nueva llamada API. Un uso más avanzado manejaría los errores de la API correctamente, como respuestas incorrectas o tiempos de espera.

Consideraciones de seguridad

Al introducir más código que depende de contenido generado por usuarios (UGC) e input, hemos aumentado la superficie de ataque de nuestra aplicación. Es crucial asegurarse de que cualquier entrada se sanitice adecuadamente para prevenir ataques como cross-site scripting (XSS) o inyecciones de código.

En nuestro ejemplo, tomamos un enfoque rudimentario de seguridad. Al obtener los parámetros de la solicitud, usamos parseInt en el parámetro counter para asegurar que este valor sea un número. Si no hiciéramos esto, podrías introducir datos peligrosos en el HTML renderizado proporcionando una etiqueta script en la solicitud. Podría verse así: ?counter=</script><script>doSomethingBad();</script>

Para nuestro ejemplo simplista, convertir nuestra entrada en un número es suficientemente seguro. Si manejas entradas más complejas, como texto libre, deberías procesar esa entrada con una función de sanitización adecuada, como xss-filters.

Además, puedes añadir capas adicionales de seguridad sanitizando la salida del estado. JSON.stringify puede ser vulnerable a inyecciones de scripts. Para contrarrestar esto, puedes depurar la cadena JSON de etiquetas HTML y otros caracteres peligrosos. Esto se puede hacer con un simple reemplazo de texto en la cadena, ej. JSON.stringify(state).replace(/</g, '\\u003c'), o mediante bibliotecas más sofisticadas como serialize-javascript.

Pasos siguientes

Puedes leer Redux Fundamentals Parte 6: Lógica asíncrona y obtención de datos para aprender más sobre cómo expresar flujos asíncronos en Redux con primitivas como Promesas y thunks. Ten en cuenta que todo lo que aprendas allí también se aplica al renderizado universal.

Si usas algo como React Router, también podrías expresar tus dependencias de obtención de datos como métodos estáticos fetchData() en tus componentes manejadores de rutas. Estos pueden devolver thunks, para que tu función handleRender pueda emparejar la ruta con las clases de componentes manejadores de ruta, despachar el resultado de fetchData() para cada uno, y renderizar solo después de que las Promesas se hayan resuelto. De esta manera, las llamadas API específicas requeridas para diferentes rutas se ubican junto con las definiciones de los componentes manejadores de ruta. También puedes usar la misma técnica en el lado del cliente para evitar que el enrutador cambie de página hasta que sus datos se hayan cargado.