Do you want to switch to Platzi in English?
Tu lugar en PlatziLive
Tu lugar en PlatziLive
estamos en vivo
17

Usando React.js en el servidor con Django

20657Puntos

hace 2 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: ​
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Mi aplicación sin server render</title>
    <link rel="stylesheets" href="src/style.min.css" />
  </head>
  <body>
    <main id="app"><!-- mi html inicial vacío --></main>
    <script src="src/app.min.js"></script>
  </body>
</html>
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í: ​
<body>
  <main id="app">
    <div>
      <header>
        <h1>Mi aplicación con server render</h1>
      </header>
      <section>
        <article>
          <p>
            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.
          </p>
        </article>
      </section>
    </div>
  </main>
  <script src="src/app.min.js"></script>
</body>
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: ​
npm i -S react react-dom
Instalado esto, necesitamos cargar nuestros módulos: ​
const http = require('http');
const qs = require('querystring');
const path = require('path');
const React = require('react');
const ReactDOM = require('react-dom/server');
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: ​
// creamos el servidor
const server = http.createServer();
// escuchamos las peticiones
server.on('request', handleRequest);
// lo corremos en el puerto 3000
server.listen(3000);
Y para manejar las peticiones definimos esta función: ​
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');
    }
  });
}

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

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

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:
{% load render %}
​
{% render '/ruta/a/el/componente.js' data %}
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

20657Puntos

hace 2 años

Todas sus entradas
Escribe tu comentario
+ 2
1
2056Puntos

Me gusta la cultura de Platzi

0
946Puntos

¡Excelente! Gracias por compartir!