Node.js
Node.js es una plataforma software que permite utilizar JavaScript en el lado del servidor, para ello internamente utiliza la máquina virtual V8, la misma que utiliza Google Chrome. Además viene con un conjunto de módulos que nos permiten realizar tareas básicas como trabajar con ficheros, sockets o http. Por lo tanto Node.js son dos cosas, un runtime y una librería. La primera versión de Node.js se publicó en 2011 y desde entonces se ha creado una comunidad de desarrolladores muy importante que han creado un gran ecosistema para desarrollar aplicaciones.
Las aplicaciones Node.js están pensadas para maximimar el rendimiento y la eficiencia utilizando entrada y salida no bloqueantes y eventos asíncronos. Las aplicaciones utilizan un único hilo aunque internamente se utilizan varios hilos para trabajar con ficheros o eventos de red. Dada su naturaleza asíncrona los aplicaciones son muy utilizadas para realizar aplicaciones real time.
Instalación
Para la instalación de node en Mac OSX o Windows puede descargar el instalador en la página oficial. En el caso de linux, debemos añadir un repositorio de donde se descargará los paquetes. Puedes seguir estas instrucciones.
Hola Mundo
Crea un fichero hello.js
console.log('Hello world')
Para ejecutar la aplicación simplemente ejecuta el comando
> node hello.js
Node trae integrados algunos módulos, por ejemplo el módulo http
que nos permite trabajar con peticiones http.
Crea un fichero que se llame helloServer.js y copia este contenido
var http = require('http');
http.createServer(function (req, res) {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end('Hello World\n');
}).listen(1337, "127.0.0.1");
console.log('Server running at http://127.0.0.1:1337/');
> node helloServer.js
Para comprobar si funciona, abre en un navegador la url http://127.0.0.1:1337/
A diferencia del primer ejemplo, esta vez el programa no termina, sino que se queda a la espera de nuevas peticiones. Como comentamos en la introducción, tenemos que tener en cuenta que nuestro programa se ejecuta en un úncio hilo, pero aún así, es capaz de responder a peticiones simultaneas.
Non-blocking I/O
Una de las razones que hizo al creador de Node.js inclinarse a utilizar JavaScript es que era un lenguaje que no contaba con una API de entrada y salida. Esto le permitió diseñar la API desde cero haciendola asíncrona por defecto, permitiendo que las aplicaicones escritas en Node.js sean muy eficientes.
La entrada y salida tradicional es algo parecido a esto
var fileContent = file.read('file.txt');
// wait
process(fileContent);
otherProcess();
El problema con la entrada y salida tradicional es la espera. ¿Por qué esperar a ejecutar el método otherProcess
si no depende del contenido del fichero? Las operaciones de entrada y salida son muy costosas.
Latencia relativa | |
---|---|
Register | 1 |
Cache | 10 |
Memory | 100 |
Harddisk | 10 000 000 |
Utilizando una entrada y salida no bloqueante, el código sería
file.read('file.txt', function (fileContenet) {
process(fileContent);
});
otherProcess();
En este caso no hay necesidad de esperar a la lectura de disco para seguir ejecutando el código. A pesar de que el código sea asíncrono tenemos que tener en cuenta que nuestra aplicación se ejecuta en un único hilo
file.read('file.txt', function () {
// este código nunca se va a ejecutar
});
while(true) {
// bloqueo del proceso
}
CommonJS modules
CommonJS es el sistema de módulos que incorpora Node.js. Con el sistema de módulos podemos incluir otros ficheros, librerías del sistema o modulos desarrolladores por terceros.
El código para crear un módulo es muy sencillo. Únicamente tenemos que especificar qué métodos queremos exportar.
hello.js
exports.world = function () {
return 'Hello World';
};
main.js
var hello = require('./hello');
console.log(hello.world());
En el caso de que queramos exportar un objeto completo, podemos utilizar
module.exports = function () {
return 'Hello World';
};
main.js
var hello = require('./hello');
console.log(hello());
Para utilizar alguno de los módulos que trae Node por defecto, basta con usar require. En la documentación tienes el listado de módulos disponibles.
var fs = require("fs");
fs.writeFile('message.txt', 'Hello Node', function (err) {
if (err) throw err;
console.log('It\'s saved!');
});
Node.js viene con un gestor de paquetes llamado NPM que nos permite gestionar librerías de terceros. El comando para instalar un paquete es
npm install nombrePaquete
Por ejemplo, si quisieramos instalar underscore
npm install underscore
Este comando se bajara el módulo con todas sus dependencias dentro de la carpeta node_modules
y ya lo tendremos disponible para incluir desde nuestro código.
var _ = require("underscore");
va result = _.map([1, 2, 3], function(num){ return num * 3; });
console.log(result);
Podemos definir los módulos en un fichero .json para que en todo momento sepamos qué modulos y qué versiones utiliza la aplicación. Para ello definimos un fichero package.json
{
"name": "myModule",
"dependencies": {}
}
La proxima vez que instalemos un módulo, utilizaremos la opción --save
para añadir la dependencies al fichero package.json en la sección dependencies.
npm install underscore --save
Ejercicio: node-fs
Escribe una función que devuelva la lista de ficheros de un directorio junto con tamaño, ordenados por nombre.
Callback hell
La naturaleza asíncrona de Node.js hace que trabajar con funciones asíncronas sea muy común. El uso constante de callbacks se puede complicar cuando queremos encadenar varias tareas una detras de otra y podemos terminar teniendo lo que se denomina un Callback Hell.
fs.readFile('file1.txt', function (file1) {
fs.readFile('file2.txt', function (file2) {
fs.readFile('file3.txt', function (file3) {
fs.readFile('file4.txt', function (file4) {
fs.readFile('file5.txt', function (file5) {
process(file1, file2, file3, file4, file5);
}
}
}
}
}
Existen varias fórmulas para este problema. La primera y la más sencilla es no abusar de las funciones anónimas.
var process = function (file1, file2) {}
var readfile2 = function (cb) { fs.readFile('file2.txt', process); }
var readfile1 = function (cb) { fs.readFile('file1.txt', readfile2);}
readfile1();
Otra posible solución es utilizar promisas
var fs = require("fs");
var path = require("path");
var _ = require("underscore");
var Promise = require('es6-promise').Promise;
function readfile(file) {
var promise = new Promise(function (resolve, reject) {
fs.readFile(file, function (err, content) {
if (err) return reject(err);
resolve(content.toString());
})
});
return promise;
}
Promise
.all([
readfile("file1"),
readfile("file2"),
readfile("file3")
])
.then(function (result) {
console.log(result);
})
.catch(function (err) {
console.log(err);
});
Otra posible solución es utilizar la librería async que provee una serie de utilidades para trabajar con funciones asíncronas.
async.map(['file1','file2','file3'], fs.stat, function(err, results){
});
Ejerciocio: node-fs-async
Escribe el mismo código del ejercicio anterior utilizando la librería async
Express
En uno de los ejemplos anteriores vimos como el módulo http nos permite crear un servidor web simple. Esta es la base para módulos como express que añaden las funcionalidades básicas de cualquier framework web.
Durante el desarrollo del material del curso, express lanzó una nueva versión, la versión 4. En el curso trabajaremos con la versión 3. En el cambio de versión hay algunas cosas que no son compatibles pero la migración es sencilla. Por lo tanto la documentación que seguiremos es la de la versión 3.x.
Para instalar una versión concreta de un paquete lo podemos hacer especificándolo en el fichero package.json o desde la consola utilizando el comando
npm install express@3.5.1
El mismo código de hola mundo del ejemplo anterior, en express sería este código
var express = require('express');
var app = express();
app.get('/', function(req, res){
res.send('hello world');
});
app.listen(3000);
Para ejecutar la aplicación, simplemente
node app.js
Express utiliza el módulo debug internamente para hacer logging. En el caso de que queramos habilitar los logs podemos utilizar el comando
DEBUG=express:* node app.js
La arquitectura de express se basa en el concepto de middleware. Un middleware no es más que una función encargada de manejar peticiones. La aplicación configura una middleware por los que pasaran las peticiones entrantes. Los middlewares se pueden configurar de forma global o por path. El middleware más simple que podemos definir es el siguiente
function uselessMiddleware(req, res, next) {
next();
}
Un middleware recibe como parámetro los objetos req
y res
que representan la request y la response. La función next
indica que se ejecute el siguiente middleware dentro de la cadena. Dentro del middleware podemos manejar los objetos req
y res
para añadir funcionalidades. Por ejemplo, para mostrar un log por cada una de las peticiones
app.use(function(request, response, next) {
console.log("In comes a " + request.method + " to " + request.url);
next();
});
También podemos especificar un path como primer parámetro
app.use("/admin", middleware);
Express utiliza internamente connect y expone todos sus middlewares. Por ejemplo, ya existe un middleware de logging como el que acabamos de crear. Para utilizarlo simplemente
app.use(express.logger());
Las rutas se comportan de manera similar a los middleware y están asociados a los verbos http(get, post, put, delete)
app.get("/", function (req, res) {
res.send("hello world");
});
app.put(/^\/commits\/(\w+)(?:\.\.(\w+))?$/, function (req, res) {
});
app.post("/post/:id", function (req, res) {
})
Las funcionalidades más básicas en un framework web son, parsear los parámetros de las peticiones y generar una respuesta
app.get("/users/:name", function (req, res) {
req.params.name // GET /users/axel
req.query.order // GET /users/axel?order=desc
});
app.use(bodyParser());
app.post("/users", function () {
req.body.users.name // POST user[name]=tobi&user[email]=tobi@learnboost.com
req.body.name // POST { "name": "tobi" }
});
req.param("") // busca en req.params, req.body, req.query
res.redirect('/foo/bar');
res.send(new Buffer('whoop'));
res.send({ some: 'json' });
res.send('some html');
res.send(404, 'Sorry, we cannot find that!');
res.send(500, { error: 'something blew up' });
res.send(200);
res.json({user: 'tobi'})
res.jsonp({ user: 'tobi' })
res.render('index', {name : "Tobi"})
La ultima opción, res.render
permite a express integrarse con sistema de plantillas. Podemos configurar el sistema de plantillas que vamos a utilizar con
app.engine('jade', require('jade').__express);
Ejercicio: Crea la api rest de twitter
En este ejercicio vamos a crear una API rest con funcionalidades parecidas a la de twitter.
GET /tweets
[
{id:2, text: "Hi"},
{id:1, text: "First tweet"}
]
GET /tweets/:id
{id:2, text: "Hi"}
POST /tweets
request {text: 'Hi'}
response {id: 20, text: 'Hi'}
GET /search?q=axel
[
{id:15, {text: "axel is a good teacher"}}
]
Para simplificar un poco las cosas no vamos a utilizar una base de datos, sino que vamos a guardar los tweets en memoria. Para que puedas comprobar si el ejercicio está correcto cree unos tests de integración que atacan a la api directamente. Para ejecutar lo test, npm test
.
Una vez tengas lista la aplicación, prueba a desplegarlo en heroku o nodejitsu.
Auto reload
A diferencia de otros framework web, como podría ser rails o play. Los cambios no se recargan en caliente. Esto no supone mucho problema, la aplicación se levanta muy rápido por lo que no perdemos tiempo reiniciando el servidor. El único tiempo que podemos perder es si realizamos la tarea a mano, por eso vamos a utilizar un módulo que realiza esta tarea de forma automática.
npm install node-dev --save-dev
Para que el auto reload funcione tenemos que arrancar la aplicación con node-dev.
$ node-dev app.js
Prueba a hacer algún cambio y comprueba cómo se actualizan.
Debugging
Depurar aplicaciones en JavaScript es bastante sencillo gracias a las herramientas de desarrollo de los navegadores. Depurar aplicaciones node no es tan sencillo.
La opción más simple para depurar aplicaciones
console.log("log");
Ahora en serio, si queremos poner break points o inspeccionar el valor de las variables en caliente podemos utilizar un IDE como IntelliJIDEA que cuenta con debugger integrado. En el caso de que estemos usando un simple editor de texto, podemos usar node-inspector
$ npm install -g node-inspector
$ node-debug app.js
Websockets
WebSocket es un protocolo que permite comunicación bidireccional entre el cliente el servidor. Inicialmente fue pensada para la comunicación entre navegador y servidor web pero puede ser utilizado con cualquier cliente o servidor de aplicaciones. Esta es la tecnología que permite crear la web en tiempo real. El servidor podrá avisar directamente a los clientes cuando se produzcan eventos nuevos. Los navegadores modernos ya implementan este standard.
Socket.io es una librería para construir aplicaciones en tiempo real con Node.js, Webscockets y fallbacks para los navegadores que no lo soportan. Los tispos de transporte que soporta son:
- WebSocket
- Adobe® Flash® Socket
- AJAX long polling
- AJAX multipart streaming
- Forever Iframe
- JSONP Polling
Ejemplos de código
var io = require('socket.io');
var server = http.createServer(app);
io.listen(server);
server.listen(app.get('port'), function () {
console.log('Express server listening on port ' + app.get('port'));
});
Servidor
io.sockets.on('connection', function (socket) {
socket.emit('news', { hello: 'world' });
socket.on('my other event', function (data) {
console.log(data);
});
});
Cliente
var socket = io.connect('http://localhost');
socket.on('news', function (data) {
console.log(data);
socket.emit('my other event', { my: 'data' });
});
Ejercicio:
- Revisa el código del servidor de chat, que vimos en la sección de backbone
- Sigue los pasos de esta presentación para crear un videojuego multijugador en tiempo real