49

Usando React.js en el servidor con Django

21837Puntos

hace 8 años

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. ​

¿Por qué importa el server render?

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:

  • Cuando el usuario entre a nuestro sitio, verá una página en blanco mientras se descarga nuestro códigoJavaScript;
  • Se inicializa la aplicación y solicita los datos necesarios vía AJAX (si no los enviamos como JSON en el HTML)
  • Y, por último, se renderiza nuestra página. ​

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>

Mi aplicación con server render

  </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.​

Usando React.js con Django

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.

Creando el servidor de Node.js

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>

Creando el cliente de Python

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

definimos la URL a donde vamos a hacer las peticiones

settings = {
URL: ‘http://localhost:3000/’,
}

creamos nuestro cliente

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>

Creando un template tag para nuestro cliente

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

programamos el template tag y le damos un nombre

@register.simple_tag(name=“render”)
def render(path, props):
# ejecutamos nuestro cliente y devolvemos el resultado
return render_component(
path,
props
)</pre>

Usandolo en nuestros templates

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. ​

Posibles mejoras

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.

Sergio Daniel
Sergio Daniel
sergiodxa

21837Puntos

hace 8 años

Todas sus entradas
Escribe tu comentario
+ 2
Ordenar por:
25
4149Puntos

seria genial un curso de django y react
😃

6
10040Puntos
4 años

Pienso lo mismo. Aveces confunde mucho el aprender estas dos tecnologías por separo.

6
1004Puntos

Entonces se puede programar el frontend en react y usar django para el backend?

1
5862Puntos

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?

6
4351Puntos
4 años

tengo entendido que hay 3 formas de fusionar django con react:

  1. Usar React como un app en Django [Dificultad: media] (la que propone este tutorial)
  2. Django REST como una API independiente + Reac como una SPA independiente [Dificultad: difícil] (la que tu propones)
  3. Mini React apps dentro de los templates de Django [Dificultad simple]

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.

0
7238Puntos

Que diferencia tiene este metodo con usar webpack para pasar los componentes a django?