Una de las grandes ventajas de React.js es que es posible renderizar nuestros componentes en el servidor sin complicaciones. Sin embargo, hacer esto normalmente implica empezar a usar Node.js por lo que en aplicaciones escritas en otras tecnologías lo normal es no usar esta característica. En Platzi usamos Python + Django para nuestro backend (entre otras tecnologías), por lo que en primera instancia parecía que no íbamos a poder hacer uso del server render con React.js… Pero, luego de mucho investigar y experimentar, pudimos encontrar una forma de implementar esta característica, y así darle una mejor experiencia a nuestros usuarios.
Antes de ver como implementarlo, veamos de qué nos sirve usar server render, y por qué necesitabamos usarlo. Cuando hacemos una aplicación con renderizado de vistas en el navegador (por ejemplo, con Angular.js), el código HTML que enviamos al navegador del usuario suele ser algo como esto:
<pre class=“EnlighterJSRAW” data-enlighter-language=“html”>
<meta charset="utf-8">
<title>Mi aplicación sin server render</title>
<link rel="stylesheets" href="src/style.min.css">
<main id=“app”></main>
</pre>
Esto quiere decir que:
Imaginemos que recibir el HTML inicial se tarda medio segundo; bajar el JS y los estilos, otro segundo y medio; y traerse los datos vía AJAX es otro medio segundo más. En suma, son dos segundos hasta que el usuario ve nuestra página. Que esto ocurra afecta negativamente a la experiencia de usuario de nuestro sitio, haciéndolo parecer muy lento. Para solucionar esto, debemos volver a algo que se hace desde el principio de la web: enviar el HTML con el contenido desde el servidor. Y resulta que React.js hace esto fácil, al renderizar nuestra aplicación a un string con el HTML, que luego podemos enviar al navegador. De esta forma, nuestro HTML inicial pasaría a ser algo así:
<pre class=“EnlighterJSRAW” data-enlighter-language=“null”>
<main id=“app”>
<div>
<header>
</header>
<section>
<article>
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Recusandae accusantium fuga ratione quos modi maiores, ipsum, ipsa delectus cupiditate, velit dignissimos neque! Repellendus voluptatibus quibusdam itaque ipsam quod sapiente saepe.
</article>
</section>
</div>
</main>
</pre>
Si renderizamos la página en el servidor, toma un tiempo — supongamos - de medio segundo (para simplificar el ejemplo); esto quiere decir que, ahora, recibir el HTML inicial se tarda un segundo entero, pero el usuario va a ver algo en pantalla en la mitad del tiempo. Así, damos la sensación de que la página carga el doble de rápido. Luego de otros dos segundos, una vez se descargue el JS+CSS y se traigan los datos por AJAX, ya podemos iniciar nuestra aplicación en el navegador, incluso una Single-Page Application.
Entendiendo el por qué renderizar en el navegador, nos chocamos con que no teníamos forma de renderizar React.js en Django, ya que este es Python, mientras que necesitábamos un servidor de Node.js para que funcione, supuestamente. Después de probar varias ideas, como la de usar un programa por línea de comandos que se ejecutara en cada petición, en el equipo de desarrollo de Platzi llegamos a la conclusión que lo mejor era crear una aplicación de Node.js muy rápida y optimizada que renderizara React.js, y a la que Django le pida el HTML cuando lo necesite. La comunicación entre Node.js y Django decidimos hacerla por **HTTP **usando el método POST, y en el cuerpo de nuestras peticiones, indicar la ruta del componente a renderizar y un JSON con los datos que va a usar el componente. ¿Por qué? Iniciar un proceso de Node.js se tomaba hasta el doble del tiempo necesario para iniciar y completar el render; mientras, un servidor nos permitía iniciarlo una sola vez y mantenerlo esperando peticiones.
Tenemos la idea: ahora vamos a programarla. Lo primero es nuestro servidor de Node.js; para esto, vamos a instalar primero dos dependencias:
<pre class=“EnlighterJSRAW” data-enlighter-language=“shell”>npm i -S react react-dom</pre>
Instalado esto, necesitamos cargar nuestros módulos:
<pre class=“EnlighterJSRAW” data-enlighter-language=“js”>const http = require(‘http’);
const qs = require(‘querystring’);
const path = require(‘path’);
const React = require(‘react’);
const ReactDOM = require(‘react-dom/server’);</pre>
Con http
vamos a iniciar nuestro servidor; con querystring
, vamos a parsear el body
de la petición POST. react
es necesario siempre que usemos algún componente, y react-dom/server
para renderizar a strings. Ahora, para iniciar nuestro servidor, usamos este código:
<pre class=“EnlighterJSRAW” data-enlighter-language=“js”>// creamos el servidor
const server = http.createServer();
// escuchamos las peticiones
server.on(‘request’, handleRequest);
// lo corremos en el puerto 3000
server.listen(3000);</pre>
Y para manejar las peticiones definimos esta función:
<pre class=“EnlighterJSRAW” data-enlighter-language=“js”>function handleRequest(request, response) {
// validamos que el método sea POST
if (request.method !== ‘POST’) {
// enviamos un error si no es POST
response.writeHead(405, {
‘Content-Length’: 33,
‘Content-Type’: ‘application/json’,
})
response.end(’{“message”:“Invalid HTTP method”}’);
}
// iniciamos la variable donde vamos
// a guardar el cuerpo de nuestra petición
let body = ‘’;
// guardamos los datos de la petición
request.on(‘data’, data => body += data);
// cuando se termina de recibír la petición
request.on(‘end’, () => {
// usamos el try/catch para evitar
// que el servidor se muera por
// un error y poder saber que paso
try {
// obtenemos los datos necesarios de la petición
const data = qs.parse(body);
const component = data.component;
const props = data.props ? JSON.parse(data.props) : {};
// definimos el path de nuestro componente
const path = path.resolve(component);
// creamos un factory de React para usarlo sin JSX
// y requerimos el componente
const Component = React.createFactory(require(path));
// generamos el string con el html
const html = ReactDOM.renderToString(Component(props));
// definimos la cabecera de la respuesta
response.writeHead(200, {
‘Content-Type’: ‘text/html’,
});
// enviamos el HTML
return response.end(html, ‘utf-8’);
} catch(error) {
// imprimimos el error en consola (para debugging)
console.error(error);
// armamos un string con un JSON del error
const message = {"message":"${error.message}"}
;
// definimos la cabecera de la respuesta
response.writeHead(400, {
‘Content-Length’: message.length,
‘Content-Type’: ‘application/json’
});
// enviamos la respuesta
return response.end(message, ‘utf-8’);
}
});
}</pre>
Una vez creado el servidor de render en Node.js, necesitamos crear un cliente capaz de comunicarse con él desde Python. Para ello, iniciamos un módulo de Python creando un directorio (por ejemplo, render) con un archivo __init__.py
y un react.py
, donde estará nuestro cliente.
<pre class=“EnlighterJSRAW” data-enlighter-language=“python”># cargamos los módulos a usar
import json
import requests
from requests.exceptions import ConnectionError
settings = {
URL: ‘http://localhost:3000/’,
}
def render_component(component, props = {}):
# definimos un string vacío donde vamos a guardar el HTML
result = ''
try:
# hacemos una petición POST a la URL pasándole
# el path al componente y un JSON con los props
response = requests.post(
settings.URL,
data={
‘component’: component,
‘props’: json.dumps(props)
}
)
# si recibimos una código de status 200
# devolvemos el contenido de la respuesta
if (response.status_code == 200):
result = response.content
except ConnectionError:
# si recibimos un error de conexión acá manejamos el error
# por ejemplo mandándolo a Sentry
# retornamos result
(con HTML si funcionó o vacío si no)
return result</pre>
Dentro del directorio de nuestro módulo, vamos a crear un directorio llamado templatetags; dentro de éste creamos un nuevo__init__.py
y un archivo render.py
donde estará el código de nuestro template tag de Django.
<pre class=“EnlighterJSRAW” data-enlighter-language=“python”># cargamos los módulos
from django import template
from render.react import render_component
register = template.Library()
@register.simple_tag(name=“render”)
def render(path, props):
# ejecutamos nuestro cliente y devolvemos el resultado
return render_component(
path,
props
)</pre>
Por último, sólo nos queda ir a nuestros templates HTML de Django, cargar el template tag y usarlo entregándole los datos necesarios:
<pre class=“EnlighterJSRAW” data-enlighter-language=“html”>{% load render %}
{% render ‘/ruta/a/el/componente.js’ data %}</pre>
De esta forma, podemos ejecutar render
indicándole la ruta y los datos: una variable con un diccionario enviados desde nuestra vista de Django (views.py
). Cada vez que el usuario entre a nuestra aplicación, el template tag ejecutará el cliente que le pide al servidor de Node.js que renderice el componente, y nos devuelva el HTML que el usuario recibirá en su navegador. Un detalle importante a considerar es que esta implementación no sirve con código escrito en JSX, y usando características de ECMAScript que Node.js no soporte. Por lo mismo, estamos obligados a usar Babel para convertirlo a código compatible antes de usar server render. ¿La razón para no agregar soporte para Babel? Es lento, y hacerlo en cada petición retrasaría el tiempo de respuesta del servidor de render, por lo que perderíamos rendimiento, el objetivo tras esta implementación.
La solución que te presento hoy puede mejorar, por ejemplo, implementando Redis como caché, para así evitar pedirle al servidor que vuelva a renderizar un componente con ciertos datos que fueron usados antes. De esta forma, podrías reducir los tiempos de carga aún más, mejorando la experiencia del usuario de paso. Otra mejora posible de implementar es indicarle al servidor de render si queremos que genere HTML estático (sin los atributos data-reactid
) para reducir el tamaño del archivo generado.
seria genial un curso de django y react
😃
Pienso lo mismo. Aveces confunde mucho el aprender estas dos tecnologías por separo.
Entonces se puede programar el frontend en react y usar django para el backend?
Apelando al hecho de que soy un completo ignorante de django… no era mas sencillo usar solo la parte django rest framework para exponer servicios rest y que sean consumidos desde react?
tengo entendido que hay 3 formas de fusionar django con react:
La opción 1 se usa mucho cuando tu estas creando un sitio web similar a una aplicación y tiendes a usar demasiadas peticiones AJAX además tienes la ventaja de que al usar React dentro de Django obtienes todos los beneficios del sistema de autenticación, osea, todo esto => https://docs.djangoproject.com/en/2.2/topics/auth/default/#module-django.contrib.auth.views
La opcion 2, es buena, cuándo quieres exponer tu api. Porque ya involucras JWT en el camino. Bueno para proyectos donde la app móvil es mas demandado que el sitio web.
La opcion 3, hmmm, lo dejaria para proyectos muy pequeños.
Así que no hay un camino ideal, la respuesta correcta es usar la mejor opción dependiendo de tu necesidad.
Que diferencia tiene este metodo con usar webpack para pasar los componentes a django?