0

Manejo de datos en tiempo real con RethinkDB y ReactJS

1953Puntos

hace 5 años

Ya habíamos hablado antes de las bondades de RethinkDB y ReactJStrabajando en conjunto para facilitar el manejo de datos en tiempo real. Y ya que aprendimos la teoría, ¡es hora de ponerlo en practica! En este artículo te enseñaré, paso a paso, cómo armar el full stack de una aplicación en tiempo real. Se dice que una base de datos es tan buena como su sistema de querys; y RethinkDB posee uno tan robusto que nos permite hacer búsquedas avanzadas y conectar el resultado en tiempo real a nuestra app. Es decir, si algún cambio en los datos modifica el resultado de la búsqueda, estos se actualizan en los clientes en tiempo real. RethinkDB nos permite realizar querys avanzadas en el servidor y los clientes se enfocan principalmente en mostrar los cambios y enviar nuevos datos. Este es un escenario ideal para el uso de ReactJS y el flujo de datos de Flux; pues podemos crear un flujo unidireccional en el que ReactJS sólo se preocupa por mostrar los datos actuales, enviar los datos nuevos y realizar las actualizaciones necesarias en el DOM cuando se realicen cambios en el Modelo. Con esta arquitectura MVC, el backend se encarga del modelo y los controladores; y el front-end se encarga casi exclusivamente de la visualización. Esto se ve reflejado en un excelente performance que será consistente incluso en dispositivos de gama baja. Desde su versión 1.16, RethinkDB fue mejorada para optimizar el manejo de datos en tiempo real. Entre algunas de las mejoras, ahora puedes usar el comando .changes( ) para que la base de datos envíe una notificación a tu servidor indicando los cambios. Al mismo tiempo, este puede usar WebSockets para notificarle los cambios a los usuarios en un JSON como el siguiente: [code] { "old_val":"old_version" , "new_val":"new_version" } [/code] Gracias a que RethinkDB tiene muy presentes los detalles en escalabilidad y performance; te permite limitar el tiempo entre actualizaciones. Así es posible seleccionar qué tan seguido deseas que se actualicen los datos. Además, si hay muchos cambios en un mismo periodo, RethinkDB los une en un solo paquete. Este ajuste te permite afinar el performance de tus servidores y usuarios en un solo paso y de acuerdo a tus necesidades. Incluso puedes usar la propiedad squash para limitar las actualizaciones a ciclos de 150 milisegundos. Esto se vería así: [code] .changes(squash=0.15) [/code] Si estás buscando una base de datos para un videojuego, RethinkDB ofrece soluciones muy interesantes. Por ejemplo, si quieres integrar un tablero en tiempo real que muestre los 3 primeros lugares pero que no impacte el performance; puedes enviar los cambios cada tres segundos con un comando así: [code] order_by(index=r.desc(‘points’)).changes({squash:3}).limit(3) [/code] Es posible que deseemos calibrar el periodo de squash a no menos que el promedio de latencia entre el servidor y nuestros usuarios. Y RethinkDB también puede usarse para crear una base de datos distribuida en hasta 16 servidores, si nuestra aplicación lo requiere.

En el backend

Lo primero que necesitamos es nuestro servidor de RethinkDB. Te recomiendo usar Socket.io como tu backend. Si no estás seguro de cómo hacerlo, tenemos una serie de tutoriales que te enseñarán a montar tu API, a usar WebSockets y a montar un servidor de archivos estáticos con Express. Antes de escribir el código de nuestro servidor, debemos crear un archivo para guardar la configuración general. Le llamaremos config.json y lo vamos a colocar en el directorio raíz. Es importante definir la IP de nuestro servidor de RethinkDB en el campo host. Si estás usando Linux, puedes revisarla con el comando ip addr show. También es importante que le asignes un nombre a la base de datos y la tabla que vas a usar. También es recomendable agregar un tableIndex para indexar tu base de dato. En nuestro ejemplo usaremos la fecha de creación de cada objeto. config.json [code] { "rethinkdb" : {     "host" : "127.0.0.1",     "port" : 28015,     "authKey" : "",     "db" : "test"     "table" : "list",     "tableIndex" : "createdAt"   },   "express" : {     "port" : 80   } } [/code] En Node.js empecemos por invocar las dependencias e importar las configuraciones que usaremos en este proyecto:
server.js
[javascript] // Serve-Static  var serveStatic = require('serve-static'); // Body-Parser var bodyParser = require('body-parser'); // Multer var multer = require('multer') // RethinkDB var r = require('rethinkdb'); // Express var express = require('express'); var app = express(); // Socket Server var server = require('http').Server(app); // Socket.IO var io = require('socket.io')(server); // Configuration File var config = require('./config.json') [/javascript] Ahora creemos un método para iniciar nuestro servidor de archivos estáticos:
server.js
[javascript] var startServer = function() {   server.listen(config.express.port) } [/javascript] Ahora creemos uno para iniciar RethinkDB: [javascript] var initializeRTDB = function(conn) { r.table(config.rethinkdb.table).indexWait('createdAt').run(conn) .then(function(result) { startServer(); conn.close() .then(function(){ console.log("RethinkDB connection closed") }) }) .error(function(error){ console.log("The table doesn't exist."); console.log("Initializing table: "+config.rethinkdb.table); r.tableCreate(config.rethinkdb.table).run(conn) .finally(function(){ console.log("Initializing index: "+config.rethinkdb.tableIndex); r.table(config.rethinkdb.table).indexCreate(config.rethinkdb.tableIndex).run(conn) .finally(function(){ console.log("DB Initialized"); conn.close() .then(function(){ console.log("RethinkDB connection closed") }); startServer(); }) }) }) } [/javascript] Y ahora crearemos un método para conectarnos a nuestra base de datos:server.js [javascript] r.connect(config.rethinkdb) .then(function(conn) { r.dbList().run(conn) .then(function(dbList){ if (dbList.indexOf(config.rethinkdb.db) > -1){ initializeRTDB(conn); } else { console.log("The DB doesn't exist."); console.log("Initializing DB "+config.rethinkdb.db); r.dbCreate(config.rethinkdb.db).run(conn) .then(initializeRTDB(conn)) } }) }) .error(function(error){ console.log("Could not open a connection to initialize the database "+config.rethinkdb.db); console.log(error.message); process.exit(1); }) [/javascript] Es momento de definir los métodos que vamos a llamar con nuestros end-points. Empecemos por uno para manejar los errores tipo 500: [javascript] var handleError = function(res) { return function(error){ res.send(500,{error: error.message}); conn.close(); } } [/javascript] Pasemos a algo más interesante. En esta ocasión, crearemos un método que baje todos los elementos de nuestra tabla y los ordene por la fecha en que fueron creados ¿Recuerdas el tableIndex en nuestro archivo config.json?
server.js
[javascript] var list = function(request, res, next) { r.connect(config.rethinkdb) .then(function(conn) { r.table(config.rethinkdb.table).orderBy({index: config.rethinkdb.tableIndex}).run(conn) .then( function(data) { if (data._responses[0]){ var query = data._responses[0].r; res.send(query); } conn.close() }) .error(handleError(res)) }) } [/javascript] Ahora que tenemos cómo mostrar el contenido de una tabla, necesitamos un método para insertar contenido en ella:
server.js
[javascript] var add = function(request, res, next) { var element = request.body; element.createdAt = r.now(); r.connect(config.rethinkdb) .then(function(conn) { r.table(config.rethinkdb.table).insert(element).run(conn) .then( function() { conn.close() }) .error(handleError(res)) }) } [/javascript] Hasta este punto tenemos lo básico. Ahora agreguemos un método para vaciar toda la tabla y así reiniciarla de una manera sencilla.
server.js
[javascript] var empty = function (request, res, next) { r.connect(config.rethinkdb) .then(function(conn) { r.table(config.rethinkdb.table).delete({returnChanges: true}).run(conn) .then( function(changes) { console.log(changes); conn.close() }) .error(handleError(res)) }) } [/javascript] Ya casi tenemos listo nuestro backend. Es momento de poner en uso una de las características más poderosas de RethinkDB:emitir notificaciones de los cambios en tiempo real a nuestros clientes. Para lograrlo usaremos Web Sockets. Con React.js lo mas efectivo es notificar al cliente que hubo un cambio para que este actualice la lista. Para hacerlo, vuelve a llamar a la API y reinicia el proceso de carga y parseo de datos. En otra situación este proceso no sería eficiente, pero gracias a la arquitectura Flux es la mejor opción. La arquitectura Flux nos permite ahorrar código y evitar errores gracias al VDOM de React.js. Es decir,sólo se actualizarán las partes del DOM que tengan diferencias con la copia actualizada en el VDOM. En este ejemplo usaremos Socket.io para notificar a nuestro cliente cuando ocurre un cambio en la tabla. Pon especial atención en el objeto squash:1, pues indica el periodo mínimo entre actualizaciones. En este caso es de 1 segundo. Este código también envía una notificación para verificar que la conexión fue establecida y lo transmite en checkConnection.
server.js
[javascript] io.on('connection', function (socket) { this.socket = socket; var webSocket = this.socket; webSocket.emit('checkConnection', { result: 'Web Socket OK' }); // Listen to test conection r.connect(config.rethinkdb.table) .then(function(conn) { r.table(config.rethinkdb.table).changes({squash:1}).run(conn, function(error,cursor){ cursor.on("data",function(change){ webSocket.emit('change',{change:change}); }); cursor.on("error",function(error){ webSocket.emit('error',{error:error}); cursor.close(); }) }) }) }) [/javascript] Ahora sólo queda iniciar el servidor de datos estáticos:
server.js
[javascript] // Data parsing app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: true })); app.use(multer()); // Define main routes app.route('/api/list').get(list); app.route('/api/add').post(add); app.route('/api/empty').post(empty); // Static files server app.use(serveStatic('./public')); [/javascript] Te invito a que veas el código completo en el repositorio de GitHub. Incluye logs adicionales que te brindan mas información en la consola de tu servidor.

En el Front-end

Ahora que ya tenemos nuestro backend, es hora de armar nuestro frontend con ReactJS. Es importante siempre empezar planeando nuestros componentes y el flujo de datos. A la larga, esto nos ahorrará mucho tiempo y hará el proceso mas fácil. Veamos nuestro ejemplo y pensemos cómo dividirlo de forma eficiente:Componentes Empecemos por definir el componente más sencillo: SimpleListRow. Este se encarga de mostrar una fila de la lista por cada elemento.

reactApp.jsx

[javascript] var SimpleListRow = React.createClass({ render: function() { var rows = this.props.simpleList; return ( <ol> {rows.map(function(element) { return ( <li>{element.row}</li> ); })} </ol> ) } }) [/javascript] Subimos un nivel en la jerarquía de nuestra aplicación y tenemos a SimpleList;que se encarga de recibir los elementos de la lista y los datos para filtrarlos y pasarlos a SimpleListRow en los props simpleList y userInput.

reactApp.jsx

[javascript] var SimpleList = React.createClass({ render: function() { return ( <span> <p><strong>Pasos para dominar un nuevo lenguaje de programación:</strong></p> <SimpleListRow simpleList ={this.props.simpleList} userInput ={this.props.userInput}/> </span> ) } })[/javascript] Es momento de regresar a y agregar un poco de código adicional. Este comparará el contenido de los props para decidir si esa línea debe aparecer o no.

reactApp.jsx

[javascript] var SimpleListRow = React.createClass({ render: function() { var rows = this.props.simpleList; var userInput = this.props.userInput; return ( <ol> {rows.map(function(element) { if (element.row){ if (element.row.toLowerCase().search(userInput.toLowerCase()) > -1){ return ( <li>{element.row}</li> ) } } })} </ol> ) } }) [/javascript] En el siguiente paso vamos a armar SimpleFilterableList, que será nuestro componente principal. Este se encargará de recibir los datos y pasarlos a los niveles inferiores. Aquí también vamos a colocar las entradas de texto. Empecemos creando un método para iniciar un Web Socket.

reactApp.jsx

[javascript] var socket = null; var startSocket = function() { socket = io(window.location.hostname) } [/javascript] SimpleFilterableList es el objeto más complejo, pues contiene la lógica de conexión al servidor. Empecemos a ver sus partes una por una; la primera es componentDidMount. Este es llamado una vez que el componente ya cargó. Pero primero mandamos llamar a startSocket() para iniciar una nueva conexión por la que vamos a recibir las notificaciones de los cambios en el modelo. Después creamos un método para bajar la lista por AJAX a través de nuestra API. reactApp.jsx [javascript] var SimpleFilterableList = React.createClass({ componentDidMount: function() { startSocket(); var instance = this; var downloadData = function(){ $.ajax({ url: '/api/list', dataType: 'json', success: function(data) { instance.setState({simpleList: data}); }.bind(instance), error: function(xhr, status, err) { console.log('Data error:'); console.error(instance.props.url, status, err.toString()) }.bind(instance) }); }; downloadData(); socket.on('change', function (data) { downloadData(); }) } }); [/javascript] Ahora vamos a asignarle los valores por defecto a nuestros datos con getInitialState.

reactApp.jsx

[javascript] getInitialState : function() { return { userInput : "", simpleList : [ { row : 'cargando ...' } ] }; } })[/javascript] Lo siguiente que necesitamos es un método al que llamaremos updateUserInput. Este mantendrá actualizado su stateuserInput que usará SimpleListRowcomo filtro.

reactApp.jsx

[javascript] updateUserInput : function(input){ this.setState({ userInput : input.target.value }) } [/javascript] Continuaremos con el método sendNewElement, que se encargará de enviar los nuevos elementos a RethinkDB usando nuestra API.

reactApp.jsx

[javascript] sendNewElement: function(key){ if (key.key == "Enter"){ $.ajax({ url : "/api/add", type : "post", data : { "row" : document.getElementById('newElement').value } }); document.getElementById('newElement').value = ''; document.getElementById("userInput").focus(); } } [/javascript] Y finalmente tenemos render, que se encargará de crear la salida de HTML de nuestra aplicación. Ahí debemos colocar el objeto SimpleList y las entradas de texto. A cada una debemos asignarle id y un callBack con onChange.reactApp.jsx [javascript] render: function(){ return ( <div> <input id = 'userInput' type = 'text' placeholder = 'Filtrar...' onChange = {this.updateUserInput}> </input> <SimpleList simpleList = {this.state.simpleList} userInput = {this.state.userInput}/> <input id = 'newElement' type = 'text' placeholder = '+' onKeyPress = {this.sendNewElement} onClick = {this.favToInput} className ='fav'> </input> </div> ) } [/javascript] Ahora lo único que queda es colocar todos estos elementos juntos en SimpleFilterableList y compilar nuestro JSX. Recuerda que esto se hace desde la carpeta raíz de tu archivo con el comando jsx -x jsx -w jsx/ js/. No olvides que debes levantar tu servidor de RethinkDB antes de correr tu aplicación en NodeJS. Si eres usuario de Windows, este tutorial te ayudará a lograrlo. ¡Felicidades! ya tienes todo lo necesario para armar el full stack de una aplicación en tiempo real que puede escalar de forma profesional de acuerdo a las necesidades de tus proyectos. Te invito a que veas el código completo de este demo en el repositorio de Github. Pero si quieres descubrir el verdadero potencial de React.js regístrate al Curso de ReactJS  en Platzi y empieza a aprender desde ahora para convertirte en un profesional de una de las principales tecnologías del futuro.
Cesar
Cesar
@reicek

1953Puntos

hace 5 años

Todas sus entradas
Escribe tu comentario
+ 2
0
4192Puntos

Deberían colocar mas bonito este post, con toda ese código no se entiende mucho