Saltar al contenido

Una introducción práctica a RethinkDB

Vamos a construir una simple aplicación de votación. El tema aquí son las películas, pero en realidad puedes introducir cualquier texto que quieras. Esta aplicación no tendrá autenticación, ni control de los votos, ni clasificación de los votos.

Se ha mantenido simple a propósito para demostrar la integración de la API de RethinkDB y cómo desarrollar una aplicación en tiempo real.

Una introducción práctica a RethinkDB
Una introducción práctica a RethinkDB

La pila también es simple: RethinkDB + Node.js + Expresss + EJS (como lenguaje de plantillas) + Socket.IO + jQuery.

El código completo y las instrucciones de instalación están en este repositorio Github.

La aplicación final se ve así:

Esta es la estructura de directorios y archivos de la aplicación:

  • controladores/index.js contiene la configuración de la ruta de la aplicación. Recibe las solicitudes y llama al modelo para enviar una respuesta.
  • models/movies.js se comunica con RethinkDB para implementar las operaciones de la base de datos.
  • node_modules contiene las dependencias de la aplicación.
  • public/style.css contiene las clases CSS para estilizar la página web.
  • public/voting.js contiene el javascript del lado del cliente para responder a eventos de click, envío de formularios y recibir eventos de Socket.IO.
  • views/index.ejs es la plantilla de la página web.
  • app.js es el archivo principal de la aplicación Node.js.
  • config.js contiene la información para conectarse a la base de datos y el puerto donde el servidor escuchará las conexiones.

Comencemos por crear un directorio para el proyecto, repensar el ejemplo de BD (o como quieras llamarlo) y grabar un CD en él:

12$ mkdir rethinkdb-example$ cd rethinkdb-example

bash

Ejecutar npm init para inicializar el directorio como un proyecto Node.js e introducir alguna información sobre el proyecto:

123456789101112131415161718192021222324252627282930313233343536$ npm init Esta utilidad te guiará en la creación de un archivo package.json. Sólo cubre los elementos más comunes, e intenta adivinar los valores por defecto más sensatos. Consulte `npm help json` para obtener documentación definitiva sobre estos campos y saber exactamente lo que hacen.use `npm install <pkg> --save` después para instalar un paquete y guardarlo como una dependencia en el archivo package.json.presione ^C en cualquier momento para salir de la versión de.name: (rethinkdb-example): (1.0.0) descripción: Punto de entrada de la aplicación de ejemplo de RethinkDB y Node.js: (index.js) comando app.jstest: repositorio git: palabras clave: autor: licencia: (ISC) MITAsobre escribir en /home/eh/rethinkdb-example/package.json:{ "nombre": "rethinkdb-example", "version": "1.0.0", "descripción": "RethinkDB y Node.js aplicación de ejemplo", "principal": "app.js", "scripts": "test": "eco": "error: no se ha especificado la prueba" && exit 1" }, "autor": "", "licencia": "MIT": ¿Está bien así? (sí)

Esto creará un archivo package.json que contiene la configuración del proyecto. Ahora, agreguemos las dependencias del proyecto a este archivo ejecutando este comando:

1npminstall body-parser ejs express path rethinkdb socket.io --save

bash

Además de descargar las últimas versiones de todas estas dependencias al directorio node_modules, la opción –save de este comando las añadirá al paquete.json:

1234567891011{ ... "dependencias": {"cuerpo-parser": "1.15.1", "ejs": "2.4.2", "express": "4.13.4", "path": "0.12.7", "rethinkdb": "2.3.2", "socket.io": "1.4.6"}}

json

En este archivo, también puedes configurar un atajo para iniciar la aplicación, así que en lugar de ejecutar el nodo app.js, ejecutas el comando más genérico npm start:

12345678{ ... "scripts":{"start": "node app.js", "test": "echo N "Error: no se ha especificado ninguna pruebaN" && exit 1"}, ...

json

Para el archivo config.js, definamos (y exportemos) las siguientes variables:

123456789module.exports={ database:{ db: process.env.RDB_DB|||"voting", host: process.env.RDB_HOST|||"localhost", port: process.env.RDB_PORT||28015}, port: process.env.APP_PORT||3000}

javascript

Si no están definidas como variables de entorno, toman los valores por defecto proporcionados en este archivo (cámbielos si quiere).

En el archivo principal de la aplicación, app.js, importa los módulos que usaremos:

12345678var express =require($0027express$0027);var app =express();var server =require($0027http$0027).createServer(app);var io =require($0027socket. io$0027)(servidor);var path =require($0027path$0027);var bodyParser =require($0027body-parser$0027);var config =require($0027./config$0027);var model =require($0027./models/movies$0027);

javascript

A continuación, configure el Express para indicar que recibiremos JSON del cliente y defina el directorio donde se pueden encontrar las plantillas de vistas, la ruta para los recursos estáticos y el motor de vistas a utilizar (EJS, que es un lenguaje de plantillas simple para generar HTML):

12345app.use(bodyParser.json());app.use(bodyParser.urlencoded({ extended:true}));app.set($0027views$0027, __dirname +$0027/views$0027);app.use(express.static(path.join(__dirname,$0027public$0027));app.set($0027view engine$0027,$0027ejs$0027);

javascript

A continuación, definimos las rutas de la aplicación, contenidas en los controladores/índice de archivos:

1var rutas =require($0027./controladores/index$0027)(app);

javascript

Las rutas son:

  • / Eso presentará la página principal de la aplicación, que presenta las películas registradas.
  • /Para registrar una nueva película a través de una solicitud POST
  • /movie/like/:id Gustar una película dado su identificador
  • /movie/like/:id To a diferencia de una película dado su identificador

Podemos ver esas rutas implementadas en los controladores/índice de archivos:

1234567891011121314151617module.exports=function(app){ app.get($0027/$0027,function(req, res){...}); app.post($0027/movie$0027,function(req, res){. ..}); app.put($0027/movie/like/:id$0027,function(req, res){...}); app.put($0027/movie/unlike/:id$0027,function(req, res){...});};

javascript

De vuelta en app.js, entonces inicializamos el servidor para escuchar las conexiones en el puerto especificado en el archivo config.js:

1234server.listen(config.port,function(){console.log($0027Servidor arriba y escuchando en el puerto %d$0027, config.port);...}

javascript

Para evitar errores y minimizar nuestro número de comandos, vamos a automatizar el proceso de creación de la base de datos y la tabla utilizada por la aplicación.

En el archivo models.movie.js tendremos un método de configuración para realizar estas acciones que tomará una llamada como argumento. La devolución de llamada se ejecutará al final cuando todo esté configurado… más sobre eso en un momento.

123456var model = module.exports;model.setup=function(callback){console.log("Setting up RethinkDB...");}

javascript

También importemos el módulo de RethinkDB y el archivo de configuración:

12var r =require($0027rethinkdb$0027);var config =require($0027../config$0027);

javascript

Ahora conectémonos con la base de datos de RethinkDB:

123456789model.setup=function(callback){console.log("Setting up RethinkDB..."); r.connect(config.database).then(function(conn){}).error(function(error){throw error;});}

javascript

La base de datos config.data (definida en config.js) resulta tener el mismo formato requerido por el método connect() para conectarse a una base de datos. Para verificar si la base de datos existe, intentemos crearla. Si se produce un error, significa que la base de datos ya existe:

12345678910r.connect(config.database).then(function(conn){// ¿Existe la base de datos? r.dbCreate(config.database.db).run(conn).then(function(result){console.log("Base de datos creada...");}).error(function(error){console.log("Base de datos ya creada...");});}).error(function(error){throw error;});

javascript

Después de esto, podemos estar seguros de que estamos conectados a la base de datos. Pero estamos trabajando con promesas (una operación que se completará en el futuro), por lo que tenemos que utilizar el método finally(), que se ejecutará después de que se cumpla la promesa, incluso en presencia de un error:

1234567891011121314151617/// ¿Existe la base de datos? r.dbCreate(config.database.db).run(conn).then(function(result){console.log("Base de datos creada...");}).error(function(error){console.log("Base de datos ya creada...");}). finally(function(){// ¿Existe la tabla? r.table(MOVIES_TABLE).limit(1).run(conn,function(error, cursor){var promise;if(error){console.log("Creación de la tabla..."); promise = r.tableCreate(MOVIES_TABLE).run(conn);}else{ promise = cursor.toArray();});});

javascript

Comprobamos si existe una tabla realizando una simple consulta. El nombre de la tabla se mantiene en la variable MOVIES_TABLE. Esta vez, usando una llamada de retorno en lugar de una promesa, crearemos la tabla (en caso de un error) u obtendremos el resultado de la consulta (en caso de que la tabla ya exista).

La estructura de los documentos almacenados en la mesa de la película es muy simple, sólo almacenaremos el título y el número de gustos y aversiones, por ejemplo:

123456{"id": "11ec4e37-2987-4277-b62d-cc798238d69d", "likes":2, "title": "Fight Club", "unlikes":1}

json

Ahora, recuerde que estamos trabajando con operaciones asíncronas, por lo que necesitamos una forma de saber cuando se realiza alguna de estas operaciones. Como tal, necesitamos guardar la promesa que nos devuelven estas operaciones. Entonces, con las referencias de la promesa en la mano, podemos conectar el código que se ejecutará cuando sea seguro asumir que la tabla de películas existe.

Este último pedazo de código establecerá un cambio que monitoreará la tabla para los cambios:

123456789101112131415161718192021// ¿Existe la tabla? r.table(MOVIES_TABLE).limit(1).run(conn,function(error, cursor){var promise;if(error){console.log("Creating table..."); promise = r.tableCreate(MOVIES_TABLE).run(conn);}else{ promise = cursor. toArray();}// La tabla existe, configurar la actualización listenerpromise.then(function(result){console.log("Configurar la actualización listener..."); r.table(MOVIES_TABLE).changes().run(conn).then(function(cursor){ cursor.each(function(error, row){callback(row);});}).error(function(error){throw error;});

javascript

En una notificación de cambio, la llamada pasada a la función setup() como argumento se ejecutará, pasándole el cambio real.

Esta función de devolución de llamada está definida en app.js:

123456789101112server.listen(config.port,function(){console.log($0027Servidor arriba y escuchando en el puerto %d$0027, config.port); model.setup(function(data){if((data.new_val!=null)&&(data. old_val!=null)){// like/unlike update io.emit($0027updates$0027, data.new_val);}elseif((data.new_val!=null)&&(data.old_val==null)){// new movie io.emit($0027movies$0027, data.new_val);}});

javascript

El trabajo de esta función de devolución de llamada es emitir un evento Socket.IO al cliente cuando el changefeed de RethinkDB nos notifica un cambio. Recuerden de la sección anterior que el objeto devuelto por el changefeed tiene dos campos, old_val y new_val. Si ambos campos están establecidos, estamos recibiendo una actualización. Si sólo se establece new_val, estamos recibiendo una nueva película. Basándonos en esto, enviamos un evento diferente.

Estos eventos son recibidos por el cliente. public/voting.js es el archivo que contiene el código javascript del lado del cliente. Usando jQuery, configuraremos Socket.IO para escuchar los eventos cuando el documento HTML esté listo:

1234567891011121314151617181920212223$(documento).ready(function(){var socket =io(); socket.on($0027updates$0027,function(movie){$($0027#$0027+ movie.id+$0027 .likes$0027). text(movie.likes);$($0027#$0027+ movie.id+$0027 .unlikes$0027).text(movie.unlikes);}); socket.on($0027movies$0027,function(movie){$(".movies").append("<li id=$0027"+ movie. id+"$0027> "+"<span fa-thumbs-up fa-2x$0027</i>/span> "+"span likes$0027> "+ movie.likes+"</span> "+"</div;*; "+"<div fa-thumbs-down fa-2x$0027*;/i>; "+"span unlikes$0027*; "+"span unlikes$0027*; "+ movie. unlikes+"</span> "+"</div; "+"</div; "+"<span movie.title+"</span>});});

javascript

Como pueden ver, cuando se recibe un evento de actualización, establecemos el número de gustos y disgustos. Cuando se recibe una nueva película, se añade un elemento HTML al contenedor de la película.

De esta manera, cuando un usuario navega a http://localhost:3000, se ejecuta el siguiente código (de controladores/index.js):

<pre>1234567app.get('/',function(req, res){ model.getMovies(function(result){ res.render('index',{ movies: result });});</pre>
javascript

model.getMovies() obtiene los documentos de la mesa de cine:

<pre>1234567891011121314model.getMovies=function(callback){ r.connect(config.database).then(function(conn){ r.table(MOVIES_TABLE).run(conn).then(function(cursor){ cursor. toArray(function(error, results){if(error)throw error;callback(results);});}).error(function(error){throw error;});}).error(function(error){throw error;});}</pre>
javascript

toArray() es una función que convierte un cursor en un array, que se pasa a una llamada de retorno que renderiza el archivo index.ejs con él:

<pre>1234567891011121314151617181920212223242526272829303132333435363738<! DOCTYPE html><html><cabeza><metacharset="utf-8"/& gt;<title>Movie Voting</título><link><scriptsrc="http: //código. jquery.com/jquery-2.2.4.min. js"></script><scriptsrc="/socket.io/socket.io.js"></script><scriptsrc="votación. js"></script><link></head><cuerpo> <h1>Voto de la película</h1><ulclass='películas'> <lt;% películas. forEach(function(movie, index){ %> <liclass='movie'id='<%= movie. id %>'<spanclass='posición'><%= index+1 %>& amp;lt;/span><divclass='voto'><divclass='btnVoto'& gt;<spanclass='btnComo'><iclass='fa fa-thumbs-up fa-2x'>& amp;lt;/i></span><spanclass='numVotes likes'><%= movie. likes %></span></div><divclass='btnVote'><spanclass='btnUnlike'>& lt;iclass='fa fa-thumbs-down fa-2x'></i></span><spanclass='numVotes unlikes'><%= movie. no me gusta %></span></div></div><spanclass='title'><%= película. title %></span></li> <% }) %> </ul><formid='form'method=' post'><h1>Añadir película</h1><inputtype='text'id='title'placeholder='Título de la película. .. '/><buttonid='btnSubmit'type='submit'>Add& lt;/botón></form></cuerpo></html></pre>
html

index.ejs es un simple archivo HTML que utiliza EJS como lenguaje de plantillas y presenta las películas recuperadas (sin ningún orden en particular) junto con un formulario para publicar una nueva película.

En EJS, el <% … %> etiqueta se utiliza para controlar el flujo. En este caso, para iterar sobre la colección de películas:

<pre>123<% movies.forEach(function(movie, index){%>…<%})%></pre>
javascript

Mientras tanto, la etiqueta <%= … %> produce el valor (escapado) de una variable:

<pre>123…<li id='<%= movie.id %>'>…</pre>
javascript

Al enviar el formulario, este fragmento de código en public/voting.js se ejecuta:

<pre>1234567891011121314151617181920$('#form').on('submit',function(event){ event.preventDefault();var inp

Por último, cuando hacemos clic en los botones de igual o diferente, el evento es recibido por el código javascript en el lado del cliente (en el caso de un similar, pero para un diferente es casi el mismo código):

1234567$($0027.movies$0027).on($0027click$0027,$0027span.btnLike$0027,function(e){var movieId =$(this).parent($0027div$0027).parent($0027div$0027).parent($0027li$0027)[0].id; $.ajax({ type:$0027PUT$0027, url:$0027/movie/like/$0027+ movieId });});

javascript

Dado que el elemento (la película) podría no existir cuando en el momento en que se vinculó el evento de clic (porque se añadió más tarde), tenemos que vincular el evento al contenedor (.movie), que siempre existe, y luego seleccionar el hijo de ese elemento que desencadenará el evento de clic (span.btnLike). De esta forma, el evento se recibirá correctamente.

Entonces, la línea var movieId = $(this).parent($0027div$0027).parent($0027div$0027).parent($0027li$0027)[0].id; navegará a través del DOM (Modelo de Objeto de Documento) para encontrar el elemento que contiene el identificador de la película. Dado que el botón «Me gusta» es la fuente del evento de clic, la búsqueda se inicia en relación con este elemento (por eso tenemos que subir tres niveles).

Una vez que la solicitud llega al servidor, se ejecuta la siguiente ruta:

123app.put($0027/movie/like/:id$0027,function(req, res){vote(req, res,$0027likes$0027);});

javascript

Dado que «like» y «unlike» son muy similares, extraemos la funcionalidad de ambos a la función privada (porque no forma parte de la sección exportada del módulo) vote():

12345678910111213varvote=función(req, res, acción){var movie ={ id:req.params.id}; model.updateMovie(movie, action,function(success, result){if(success) res.json({ status:$0027OK$0027});else res.json({ status:$0027Error$0027});})

javascript

El parámetro de acción contiene el nombre del campo a actualizar, así que en el método model.updateMovie():

12345678910111213model.updateMovie=function(movie, field, callback){ r.connect(config.database).then(function(conn){ r.table(MOVIES_TABLE).get(movie.id).update(function(movie){return r. object(field,movie(field).add(1));}).run(conn).then(function(results){callback(true, results);}).error(function(error){callback(false, error);}).error(function(error){callback(false, error);});}

javascript

Para realizar la actualización, usamos una función en lugar de un objeto. Recuerda que en ReQL, algunas operaciones pueden tomar una función como argumento . Como resultado, podemos especificar dinámicamente el campo con el que actualizar:

1r.object(field,movie(field).add(1));

javascript

El documento de la película que se va a actualizar se pasa como argumento a la función para que podamos obtener el valor actual y añadir uno («como» o «a diferencia de») para establecer el nuevo valor.

Esto cubre toda la funcionalidad de nuestra aplicación básica. Para ejecutarla, inicie RethinkDB y luego escriba npm start.