Livro - Aplicações web real-time com Node.js

Estudar sempre é bom, nós desenvolvedores vivemos constantemente estudando novas linguagens, conceitos e boas práticas de código.

Hoje apresentarei a vocês o mais recente livro brasileiro sobre Node.js. E para estimular os seus estudos no final deste artigo há um cupom de descontos de cortesia da editora Casa do Código.

Sobre o livro

Livro: Aplicações web real-time com Node.js

O livro aborda de um forma prática, os conceitos por trás do Node.js. Nele você aprenderá sobre os recursos nativos e boas práticas de desenvolvimento com Node.js. No decorrer dos capítulos você também criará uma aplicação do zero, utilizando os principais frameworks da plataforma.

A aplicação criada no livro é uma agenda de contatos que permite conversar com os demais contatos onlines via web-chat que roda em tempo-real. Os frameworks utilizados são: Express, Socket.IO, Mongoose, Node-Redis, Mocha, SuperTest, Clusters, Forever e EJS. Um dos destaques do livro esta no capítulo final, em que é apresentado diversas dicas para preparar sua aplicação para um ambiente de produção. Até é abordado como integrar Node.js com Nginx aplicando proxy para o Node e deixando o Nginx servir arquivos estáticos.

O livro é recomendado para todos que tenham conhecimentos básicos de Javascript e domine os conceitos básicos de arquitetura cliente-servidor.

Nodejs e MongoDB - Introdução ao Mongoose

Mongoose é uma biblioteca do Nodejs que proporciona uma solução baseada em esquemas para modelar os dados da sua aplicação. Ele possui sistema de conversão de tipos, validação, criação de consultas e hooks para lógica de negócios.

Mongoose fornece um mapeamento de objetos do MongoDB similar ao ORM (Object Relational Mapping), ou ODM (Object Data Mapping) no caso do Mongoose. Isso significa que o Mongoose traduz os dados do banco de dados para objetos JavaScript para que possam ser utilizados por sua aplicação.

Obs: Este artigo vai assumir que você tem o Nodejs e o MongoDB instalados. Você precisa estar com o MongoDB rodando em seu computador para poder fazer os seguintes exemplos.

Instalando o pacote Mongoose

Utilizando o [NPM][] via linha de comando é muito simples fazer a instalação dos pacotes do Nodejs. Para fazer a instalação do Mongoose, execute a seguinte linha de comando que o NPM cuidará de instalar a versão estável mais recente do pacote requerido:

npm install Mongoose

Conectando com o MongoDB

Neste artigo vamos conectar o Mongoose com o banco de dados de testes que o MongoDB define quando você o inicializa pelo console e vamos garantir que qualquer erro de conexão seja impresso no console.

var Mongoose = require('Mongoose');

var db = Mongoose.connection;

db.on('error', console.error);
db.once('open', function() {
  console.log('Conectado ao MongoDB.')
  // Vamos adicionar nossos Esquemas, Modelos e consultas aqui
});

Mongoose.connect('mongodb://localhost/test');

Ao executar nossa aplicação de exemplo podemos observar no console do MongoDB que foram abertas 5 conexões simultâneas. Isto ocorre porque o Mongoose usa um conjunto de 5 conexões simultâneas por padrão que são compartilhadas por toda sua aplicação. Para melhorar o desempenho das nossas aplicações vamos deixá-las abertas, porém você pode alterar o comportamento padrão adicionando parâmetros opcionais ao Mongoose.connect() - o parâmetro poolSize define a quantidade de conexões simultâneas. Veja abaixo a saída do meu MongoDB exibindo que foram abertas 5 conexões simultâneas:

Sat Aug 31 11:12:21.827 [initandlisten] connection accepted from 127.0.0.1:64413 #2 (1 connection now open)
Sat Aug 31 11:12:21.830 [initandlisten] connection accepted from 127.0.0.1:64414 #3 (2 connections now open)
Sat Aug 31 11:12:21.831 [initandlisten] connection accepted from 127.0.0.1:64415 #4 (3 connections now open)
Sat Aug 31 11:12:21.831 [initandlisten] connection accepted from 127.0.0.1:64416 #5 (4 connections now open)
Sat Aug 31 11:12:21.832 [initandlisten] connection accepted from 127.0.0.1:64417 #6 (5 connections now open)

Esquemas e Modelos

Para começar vamos precisar de um esquema e um modelo para que possamos trabalhar com os dados que serão persistidos em nosso banco de dados MongoDB. Esquemas definem a estrutura dos documentos dentro de uma coleção e modelos são usados para criar instâncias de dados que serão armazenados em documentos. Nós vamos fazer um banco de dados de filmes para acompanhar os filmes que tem cenas extras após os créditos finais (também chamado de ‘credit cookies’). O código abaixo mostra nosso esquema movieSchema e nosso modelo Movie criado.

var movieSchema = new Mongoose.Schema({
  title: { type: String },
  rating: String,
  releaseYear: Number,
  hasCreditCookie: Boolean
});

var Movie = Mongoose.model('Movie', movieSchema);

A última linha deste código compila o modelo Movie utilizando o esquema movieSchema como estrutura. O Mongoose também cria uma coleção no MongoDB chamada Movies para estes documentos.

Você pode notar que o modelo Movie está em letra maiúscula, isto porque quando um modelo é compilado é retornado uma função construtora que será usada para criar as instâncias do modelo. As instâncias criadas pelo construtor do modelo são documentos que serão persistidos pelo MongoDB ao utilizar a função save.

Criar, Recuperar, Atualizar e Deletar (CRUD)

Criar um novo documento Movie é fácil agora que já definimos o modelo, basta instancializar o modelo Movie e salvar esta instância na base. Atualizar é igualmente fácil, faça suas modificações e então chame a função save do seu documento.

var thor = new Movie({
  title: 'Thor',
  rating: 'PG-13',
  releaseYear: '2011',  // Note o uso de String ao inves de Number
  hasCreditCookie: true
});

thor.save(function(err, thor) {
  if (err) return console.error(err);
  console.dir(thor);
});

Observe que utilizamos uma String ao invés de um Número no campo releaseYear e o Mongoose vai se encarregar de converter o dado para tipo especificado no esquema.

Quando adicionamos estes dois códigos dados à nossa aplicação e executamos vemos que a função save vai fornecer um documento recém criado, observado pelo que foi impresso no console de nossa aplicação.

{ __v: 0,
  title: 'Thor',
  rating: 'PG-13',
  releaseYear: 2011,
  hasCreditCookie: true,
  _id: 5222012cb65eddf003000001 }

Coleções no MongoDB possuem esquemas flexíveis, isto quer dizer que coleções não impõem a estrutura dos documentos. Na prática isto significa que documentos da mesma coleção não precisam ter o mesmo conjunto de campos ou estrutura, e campos comuns em documentos de uma coleção podem carregar diferentes tipos de dados. Como foi visto em nosso exemplo, utilizando o Mongoose para mapear nossa base, ele padroniza os documentos de um mesmo esquema a fim garantir que instâncias do modelo que compilou aquele esquema sempre tenham o mesmo tipo de dados nos atributos especificados pelo esquema.

MongoDB também possui uma estrutura de dados chamada índice que permite você localizar rápidamente documentos baseado nos valores armazenados em certos campos específicos. Fundamentalmente, índices no MongoDB é similar à índices em outros sistemas de banco de dados. Em nosso exemplo o índice do modelo criado é _id.

Recuperando um documento da base

Diferentes maneiras podem ser utilizadas para recuperar um documento existente na base de dados. Você pode buscar documentos baseado em qualquer propriedade do esquema e você pode buscar qualquer quantidade de documentos. Utilize findOne para limitar os resultados a um único documento.

// Buscando um unico filme pelo nome
Movie.findOne({ title: 'Thor' }, function(err, thor) {
  if (err) return console.error(err);
  console.dir(thor);
});

// Buscando todos os filmes
Movie.find(function(err, movies) {
  if (err) return console.error(err);
  console.dir(movies);
});

// Buscando todos os filmes que possuem 'credit cookie'.
Movie.find({ hasCreditCookie: true }, function(err, movies) {
  if (err) return console.error(err);
  console.dir(movies);
});

Mongoose também permite você criar funções auxiliares estáticas para buscar seus dados. Para isso você deve atribuir sua função estática ao esquema antes de ser feita compilação para o modelo.

movieSchema.statics.findAllWithCreditCookies = function(callback) {
  return this.find({ hasCreditCookie: true }, callback);
};

var Movie = Mongoose.model('Movie', movieSchema);

// Utilizadno a funcao auxiliar estatica do modelo 'Movie' compilado
Movie.findAllWithCreditCookies(function(err, movies) {
  if (err) return console.error(err);
  console.dir(movies);
});

Isso é tudo que precisa fazer para manipular os dados em MongoDB. Mongoose faz esta tarefa muito simples então você pode desenvolver seus serviços rapidamente. Usando isso junto com suas habilidades com Express, você pode desenvolver um aplicativo web muito bom e funcional.

Abaixo está um código utilizando os conceitos aqui apresentados que implementa a função auxiliar estática, para buscar todos filmes com ‘credit cookies’, salva 3 filmes do Thor com ‘credit cookies’ e em seguida cria um Timeout para buscar os filmes utilizando a função auxiliar 1 segundo depois - Isto é feito pois como estamos trabalhando com IO não bloqueante o Node não aguarda o tempo de salvar os filmes antes de prosseguir, por isso damos um tempo para se certificar que eles foram salvos antes de buscá-los.

var Mongoose = require('Mongoose');

var db = Mongoose.connection;

db.on('error', console.error);
db.once('open', function() {
  // Create your schemas and models here.
  console.log('Conectado.')
  
  
  
  var movieSchema = new Mongoose.Schema({
    title: { type: String },
    rating: String,
    releaseYear: Number,
    hasCreditCookie: Boolean
  });
  
  
  movieSchema.statics.findAllWithCreditCookies = function(callback) {
    return this.find({ hasCreditCookie: true }, callback);
  };

  var Movie = Mongoose.model('Movie', movieSchema);

  
  for (var i =1; i<=3; i++){
    var thor = new Movie({
      title: 'Thor ' + i,
      rating: 'PG-13',
      releaseYear: '2011',  // Note o uso de String ao inves de Number
      hasCreditCookie: true
    });

    thor.save(function(err, thor) {
      if (err) return console.error(err);
      console.log('Salvo: ')
      console.dir(thor);
    });
  }
  
  setTimeout(function(){
    // Utilizadno a funcao auxiliar estatica do modelo 'Movie' compilado
    Movie.findAllWithCreditCookies(function(err, movies) {
      if (err) return console.error(err);
      console.log('Buscado: ')
      console.dir(movies);
    });
  }, 1000);
});

Mongoose.connect('mongodb://localhost/test');

Mineração de dados e as funções map, reduce e filter

Este artigo vai mostrar como é simples fazer uma mineração de dados na Internet com o Nodejs e demonstrando como utilizar funções muito úteis do JavaScript: map, reduce e filter.

Bancos de dados não-relacionais (NoSQL), como o CouchDB e o mongoDB, utilizam o MapReduce, que é uma combinação das funções map e reduce apresentadas aqui, para efetuarem suas consultas na base de dados. Eles implementam funções de maneira muito similares às do JavaScript e este é o melhor motivo para compreender o funcionamento desta operação.

O texto também serve de base para quem deseja fazer um web crawler, também chamado de web spider, que são programas que acessam páginas da web e extraem informações relevantes de seu conteúdo. Os motores de busca, como a Google e o Bing, utilizam esta técnica para manterem suas bases de dados atualizadas. Os crawlers também são utilizados para varrer a Internet a fim de minerar endereços de email e dados pessoais.

Buscando o conteúdo na Web

A fonte de dados utilizada será o Feed RSS do HackerNews, como o RSS está em XML podemos tratá-lo da mesma forma que um documento HTML.

Para dar início ao nosso projeto temos que criar nosso aplicativo e fazer com que ele busque todo o Feed para que seja possível fazer a extração dos dados. Para isso será utilizado a função get do módulo https. Observe como o aplicativo exemplo app.js ficou.

// app.js

var https = require('https');

function getCallback(response){
  var body = '';
  console.log("Temos uma resposta: " + response.statusCode);
  response.on('data', function (chunk) {
    body += chunk;
  });
  response.on('end', function(){
    console.log("Corpo da mensagem: " + body.slice(0,200) + '...');
  });
}

https.get('https://news.ycombinator.com/bigrss', getCallback)
  .on('error', function(e){
    console.log("Ocorreu um erro: " + e.message);
  });

Vamos acompanhar o que é feito neste código passo a passo. Primeiro importamos o módulo https que buscará o endereço da web que queremos. Depois definimos a função getCallback que, como o nome sugere, será a função callback do nosso https.get(), ela recebe o objeto response como argumento e é adicionado um event listner para o evento data, que concatena a resposta na variável body, e um event listner para o evento end, que chama a função console.log() quando a resposta de nossa requisição está completa. Por último executamos o método https.get() passando como parâmetro o endereço do Feed que vamos buscar e a função callback que tratará a resposta, adicionamos também um event listner para o evento error que nos indicará se ocorreu algum erro durante nossa requisição.

É interessante ressaltar que o Node trabalha com eventos, então cada pacote recebido como resposta é um evento que deve ser tratado no nosso objeto response, por isso temos que concatenar toda a resposta a fim de trabalhar com o corpo completo da página requisitada.

Minerando os dados

Agora que temos o corpo da página temos que fazer a extração dos dados que queremos e do jeito que queremos. Neste exemplo vamos demonstrar as habilidades das funções map, reduce e filter desenvolvendo um contador de palavras dos títulos dos posts. Ele contará o número de ocorrências de cada palavrá e retornará uma lista de túplas [palavra, nro_ocorrências] das palavras que ocorreram mais de 10 vezes nos títulos.

Como foi dito, a base de dados que vamos minerar está em RSS então ela pode ser tratada da mesma forma que uma página HTML. Para isso podemos utilizar o jQuery para extrair a informação formatada.

Para utilizar as funções do núcleo do jQuery no servidor podemos tanto utilizar a própria biblioteca jQuery, desenvolvida para o navegador, como utilizar a biblioteca Cheerio, que é uma implementação rápida, flexível e limpa do core do jQuery designada específicamente para o servidor. Matthew Mueller diz que seus benchmarks sugerem que Cheerio seria 8 vezes mais rápida que a implementação do jQuery para o navegador.

Antes de utilizá-la, vamos fazer a instalação do pacote do Cheerio para em nosso aplicativo. Para isso basta instalá-lo utilizando a NPM via linha de comando:

npm install cheerio

Vamos, então, importar o módulo para nosso aplicativo e criar uma função para minerar os dados da página requisitada. Vamos chamar nossa função de minerarDados() e passar para ela o corpo da resposta recebida pela função getCallback(). O código do nosso exemplo ficaria assim:

var https = require('https'),
    cheerio = require('cheerio');

function minerarDados(body){
  var $ = cheerio.load(body);
  var titles = $('title');
  console.log(titles.slice(0,2));
}

function getCallback(response){
  var body = '';
  console.log("Got response: " + response.statusCode);
  response.on('data', function (chunk) {
    body += chunk;
  });
  response.on('end', function(){
    minerarDados(body);
  });
}

https.get('https://news.ycombinator.com/bigrss', getCallback)
  .on('error', function(e){
    console.log("Ocorreu um erro: " + e.message);
  });

Agora nossa função minerarDados() conseguiu extrair os objetos do tipo tag de nome title e imprime dois deles no console. Ok, ela extrai todos os títulos do Feed RSS, porém temos um problema, $('title') retorna uma lista de nós (NodeList). Vamos então aplicar o MapReduce para extrair a informação que desejamos e por último filtrar os resultados indesejados.

Para converter-mos nossa lista de nós em um Array contendo apenas o conteúdo de texto dos nomes vamos primeiro transformar nossa NodeList em um Array de objetos utilizando a função Array.prototype.slice.call() e neste array de objetos vamos aplicar a função map() do JavaScript, o código ficaria assim.

var titles = Array.prototype.slice.call($('title')).map(
  function(node, index, context) {
    return $(node).text();
});

array.map(callback[, thisArg])

A função map() recebe dois parâmetros mas normalmente apenas o primeiro é especificado. O primeiro parâmetro é uma função de callback que será chamada sobre todos os elementos no Array. O segundo parâmetro é utilizado para especificar o valor para o objeto this durante a execução da função. O mais importante são os parâmetros passados para a função de callback, eles são: o elemento do Array em sí, o índice do Array, e todo o Array (contexto). A assinatura da função de callback se parece com essa:

var callback = function(elemento, indice, contexto) { /* omitido */ }

Porém também podemos utilizar a função map() do jQuery implementada pelo Cheerio, mas para isso é preciso ter atenção pois a assinatura da função callback do jQuery é ligeiramente diferente, em seus parâmetros primeiro vem o índice do elemento seguido do elemento em sí. Sua assinatura é assim:

var callback = function(indice, elemento) { /* omitido */ }

Para implementar utilizando a função map() do jQuery é preciso fazer ligeiras modificações, o código deverá se parecer com isto:

var titles = $('title').map(
  function(index, node) {
    return $(node).text();
});

Eliminando pontuações desnecessárias

Agora que já temos um Array de strings contento o texto de todos os títulos da nossa base de dados que estamos explorando. Você já deve ter notado que os títulos não contém apenas letras, eles também contém números e pontuações. Estes são desnecessários em nosso contador de palavras, então vamos tratar de removê-los. Isto pode ser feito nesta mesma chamada map() utilizando expressão regular.

var words = $('title')
  .map(function(index, node) {
    return $(node).text().toLowerCase().match(/([a-z]+)/g);
  });

Se você já utilizou RegEx (expressões regulares) no JavaScript você sabe que isso gera um efeito colateral: Esta função retorna Arrays com todas as palavras que passaram na RegEx individualmente. Contudo esse é um efeito colateral útil para nós pois só precisamos das palavras, no entanto temos mais um problema: o Array words retornado é de duas dimensões, isso que significa que temos que achatá-lo para apenas uma dimensão. Este é um bom momento para fazer uso da função reduce() do JavaScript.

var words = $('title')
  .map(function(index, node) {
    return $(node).text().toLowerCase().match(/([a-z]+)/g);
  })
  .reduce(function(last, now){
    return last.concat(now)
  }, []);

array.reduce(callback[, initialValue])

Assim como map, reduce recebe dois argumentos. O primeiro é novamente a função de callback, que será chamada para cada elemento no Array. O segundo parâmetro é o initialValue que será de fácil entendimento quando você ver a assinatura da função de callback:

var callback = function(valorAnterior, valorAtual, indice, contexto) { /* omitted */ }

Como você pôde ver, o primeiro parâmetro passado para a função de callback é o valor anterior. Se você precisar somar um Array de números, isso não será problema. Mas em nosso caso nós precisamos retornar um Array, então nós especificamos um valor inicial para o valorAnterior, neste caso um Array vazio [] onde serão adicionados os elementos de nossa antiga Array bi-dimensional.

Contando as palavras

Para esta tarefa podemos utilziar reduce novamente, mas antes de fazê-lo, vou mostrar como o resultado vai se parecer:

[['the', 'on', 'news', 'hacker', ...], [50, 66, 20, 19, ...]]

Isso será uma Array de duas dimensões novamente. São dois Arrays dentro de um Array, e o índice da palavra no primeiro Array corresponderá com o índice do número de vezes que ela ocorre nos títulos no segundo Array. Para fazer isso vamos utilizar reduce e vamos especificar uma Array 2d vazia como valor inicial: [[], []].

var scores = $('title')
  .map(function(index, node) {
    return $(node).text().toLowerCase().match(/([a-z]+)/g);
  })
  .reduce(function(last, now){
    return last.concat(now)
  }, [])
  .reduce(function(last, now){
    var index = last[0].indexOf(now);
    if (index === -1) {
      last[0].push(now);
      last[1].push(1);
    } else {
      last[1][index] += 1;
    }
    return last;
  }, [[], []]);

Comprimindo os Arrays

Estamos próximos de concluirmos nossa coleção de dados. Tudo que devemos fazer agora é combinar as duas Arrays em uma. O formato final do Array será este:

[['the', 50], ['on', 66], ['news', 20], ['hacker', 19], ...]

O JavaScript não implementa a função zip() nativamente, nós poderíamos utilziar esta função do pacote Underscore mas como esta função é muito simples de ser implementada vamos implementar nós mesmos a mesclagem dos valores destas listas.

var zip = [];
scores[0].forEach(function(word, i) {
  zip.push([word, scores[1][i]])
});

Filtrando os dados

Lembrando que nossa aplicação só se interessa por palavras que ocorreram mais de 10 vezes, a fim de filtrar as palavras menos importantes da lista, então vamos implementar esta última funiconalidade. Para isto podemos utilizar a função nativa do JavaScript filter da seguinte maneira:

var filtered = zip.filter(function (element){
  return element[1] >= 10
});

array.filter(callback[, thisObject])

Os parâmetros da função filter são exatamente os mesmos da função map já apresentada, ela possui dois parâmetros porém normalmente só o primeiro é especificado. O primeiro parâmetro é uma função de callback que será chamada sobre todos os elementos no Array. O segundo parâmetro é utilizado para especificar o valor para o objeto this durante a execução da função. O importante é a função de callback, seus parâmetros são: o elemento do Array em sí, o índice do Array e todo o Array (contexto). A assinatura da função de callback se parece com essa:

var callback = function(elemento, indice, contexto) { /* omitido */ }

Esta função retorna verdadeiro ou falso para filtrar os elementos que continuam no Array e os elementos que são excluídos do Array. Quando fizemos element[1] >= 10 utilizamos o operador lógico de comparação que retorna verdadeiro para todos os elementos cujo valor do índice 1 seja maior ou igual a 10 e falso caso contrário - o índice 1 guarada a quantidade de vezes que a palavra ocorreu nos títulos.

Conclusão

Agora nosso exemplo está concluido e conseguímos fazer a mineração dos dados que queríamos dos Feeds do HackerNews. O código final ficou assim:

var https = require('https'),
    cheerio = require('cheerio');

function minerarDados(body){
  var $ = cheerio.load(body);
  var scores = $('title')
    .map(function(index, node) {
      return $(node).text().toLowerCase().match(/([a-z]+)/g);
    })
    .reduce(function(last, now){
      return last.concat(now)
    }, [])
    .reduce(function(last, now){
      var index = last[0].indexOf(now);
      if (index === -1) {
        last[0].push(now);
        last[1].push(1);
      } else {
        last[1][index] += 1;
      }
      return last;
    }, [[], []]);
  var zip = [];
  scores[0].forEach(function(word, i) {
    zip.push([word, scores[1][i]])
  });
  var filtered = zip.filter(function (element){
    return element[1] >= 10
  });
  console.log(filtered.slice(0,20));
}

function getCallback(response){
  var body = '';
  console.log("Got response: " + response.statusCode);
  response.on('data', function (chunk) {
    body += chunk;
  });
  response.on('end', function(){
    minerarDados(body);
  });
}

https.get('https://news.ycombinator.com/bigrss', getCallback)
  .on('error', function(e){
    console.log("Ocorreu um erro: " + e.message);
  });

O exemplo apresentado funcionou corretamente e conseguiu extrair a informação da base da Internet escolhida e imprimir os 20 primeiros elementos do nosso Array no console console.log(filtered.slice(0,20));.

O artigo teve como finalidade ensinar os leitores como é fácil fazer tal operação utilizando o Nodejs e as funções nativas do JavaScript, podendo ser utilizado como base para desenvolvimento de aplicações mais complexas como robôs de web crawling entre outros.

Como evitar o inferno de callbacks

Este artigo é um guia para se escrever programas assíncronos em JavaScript que expõe dicas para se evitar o ‘inferno de callbacks’.

O bom entendimento de callbacks é essencial para a programação orientada a eventos do Node, este é o nome dado a funções que serão executadas de modo assíncrono, ou posteriormente. Caso tenha dúvidas do que é uma função callback você pode ler o artigo explicando callbacks em Node.

O que é o ‘inferno de callbacks’

JavaScript assíncrono, ou JavaScript que usa callbacks é difícil de se entender intuitivamente. Muitos códigos acabam se parecendo como este:

fs.readdir(source, function(err, files) {
  if (err) {
    console.log('Error finding files: ' + err)
  } else {
    files.forEach(function(filename, fileIndex) {
      console.log(filename)
      gm(source + filename).size(function(err, values) {
        if (err) {
          console.log('Error identifying file size: ' + err)
        } else {
          console.log(filename + ' : ' + values)
          aspect = (values.width / values.height)
          widths.forEach(function(width, widthIndex) {
            height = Math.round(width / aspect)
            console.log('resizing ' + filename + 'to ' + height + 'x' + height)
            this.resize(width, height).write(destination + 'w' + width + '_' + filename, function(err) {
              if (err) console.log('Error writing file: ' + err)
            })
          }.bind(this))
        }
      })
    })
  }
})

Você consegue ver quantas functions e }) têm neste código? Isto é carinhosamente conhecido como o inferno de callbacks.

Escrever códigos melhores não é difícil, você só precisa saber algumas coisas.

Nomeie suas funções

Aqui esta um confuso código em JavaScript que roda no navegador que utiliza browser-request para fazer uma requisição AJAX para o servidor.

var form = document.querySelector('form')
form.onsubmit = function(submitEvent) {
  var name = document.querySelector('input').value
  request({
    uri: "http://example.com/upload",
    body: name,
    method: "POST"
  }, function(err, response, body) {
    var statusMessage = document.querySelector('.status')
    if (err) return statusMessage.value = err
    statusMessage.value = body
  })
}

O código possui duas funções anônimas, observe como fica o código quando damos nomes a estas funções.

var form = document.querySelector('form')
form.onsubmit = function formSubmit(submitEvent) {
  var name = document.querySelector('input').value
  request({
    uri: "http://example.com/upload",
    body: name,
    method: "POST"
  }, function postResponse(err, response, body) {
    var statusMessage = document.querySelector('.status')
    if (err) return statusMessage.value = err
    statusMessage.value = body
  })
}

Como você pode ver nomear as funções é muito fácil e traz algumas coisas boas para seu código:

  • Facilita a leitura do código
  • Quando acontecem exceções você pega rastreamento da pilha (stacktraces) que referencia o nome atual da função ao invés de “anonymous”
  • Permite você manter seu código mais raso, ou não aninhado profundamente

Mantenha seu código raso

Aproveitando o exemplo apresentado, vamos um pouco mais longe e se livrar do nível triplo de aninhamento que há no código.

function formSubmit(submitEvent) {
  var name = document.querySelector('input').value
  request({
    uri: "http://example.com/upload",
    body: name,
    method: "POST"
  }, postResponse)
}

function postResponse(err, response, body) {
  var statusMessage = document.querySelector('.status')
  if (err) return statusMessage.value = err
  statusMessage.value = body
}

document.querySelector('form').onsubmit = formSubmit

Códigos como este facilitam a leitura e a manutenção posterior.

Modularize seu código

Esta é a parte mais importante: Qualquer pessoa é capaz de criar módulos (biblioteca AKA). Citando Isaac Schlueter (do projeto do Node): “Escreva módulos pequenos onde cada um faça uma coisa, e monte-os em outros módulos que fazem coisas maiores. Você não entra no inferno de callback se você não ir para lá.”

Vamos pegar o exemplo apresentado e transformá-lo em um módulo dividindo-o em um par de arquivos. Aqui será apresentado um método simples que funciona tanto no navegador como no servidor.

Vamos criar um arquivo chamado formuploader.js que contém nossas duas funções apresentadas anteriormente.

function formSubmit(submitEvent) {
  var name = document.querySelector('input').value
  request({
    uri: "http://example.com/upload",
    body: name,
    method: "POST"
  }, postResponse)
}

function postResponse(err, response, body) {
  var statusMessage = document.querySelector('.status')
  if (err) return statusMessage.value = err
  statusMessage.value = body
}

exports.submit = formSubmit

A variável exports da última linha é um exemplo do sistema de módulos CommonJS. O bom deste sistema de módulos é que ele é muito simples - você só precisa definir o que deve ser compartilhado quando o módulo for importado, para isto é utilizado a variável local exports do módulo. Para compreender melhor como funciona a importação de módulos você pode ler o artigo que explica como funciona a função require do Node.js;

O Node tem um sistema simples de carregamento de módulos que utiliza o padrão CommonJS e de fato a maior parte das funções do núcleo do Node é implementada utilizando módulos escritos em JavaScript. Compreender profundamente o sistema de módulos do Node é crucial para implementação de códigos legíveis e de fácil manutenção, fundamental para a maior parte das aplicações. Para entender melhor este assunto acesse o artigo que aborda detalhadamente os módulos em Node.js.

Para usar módulos no padrão CommonJS no navegador, você pode utilizar a biblioteca browserify. Esta biblioteca basicamente permite você utilizar a função require para carregar módulos no seu programa.

Agora que temos o formuploader.js e ele foi carregado na página utilizando a tag HTML script, nós só precisamos importá-lo e usá-lo. Veja como ficou o código da nossa aplicação agora:

var formUploader = require('formuploader')
document.querySelector('form').onsubmit = formUploader.submit

Agora nossa aplicação exemplo apresentada só possui duas linhas e tem os seguintes benefícios:

  • Facilita o entendimento de novos desenvolvedores - Eles não precisam empacar tentando ler todas as funções formuploader para entender o que está acontecendo aqui
  • As funções do formuploader agora podem ser usadas em outros lugares sem a duplicação de código e pode facilmente ser compartilhaada no GitHub
  • O código em sí é agradável, simples e fácil de ler

Há muitos padrões de módulos para o navegador e para o servidor. Alguns deles são muito complicados. Os mostrados aqui são os considerados mais simples de se entender.

E o Promises?

Promises é o padrão mais abstrato para trabalhar com código assíncrono em JavaScript.

O escopo deste artigo é mostrar como escrever um código agradável em JavaScript. Se você utiliza uma biblioteca de terceiros que adiciona abstração para seu JavaScript, então certifique-se de estar disposto a forçar todos que contribuem para a sua biblioteca a também terem os mesmos pontos de vistas sobre o JavaScript que você.

Max Ogden expõe que utiliza callbacks em 90% do código assíncrono que escreve e quando as coisas se complicam ele utiliza algo como a biblioteca async, que é um módulo que fornece funções poderosas para trabalhar com código JavaScript assíncrono, ela foi projetada para utilizar como o Node.js mas também pode ser utilizada diretamente no navegador.

Leitura adicional

Módulos em Node.JS

Node tem um sistema simples de carregamento de módulos, a utilização de módulos permite incluir outros arquivos JavaScript em sua aplicação, este sistema utiliza o formáto de módulos CommonJS e de fato grande maioria das funcionalidades do núcleo do Node é implementada utilizando módulos escritos em Javascript, o que significa que você pode ler o código fonte das bibliotecas do núcleo (core) no Github.

Módulos são cruciais para construção de aplicações em Node pois eles permitem incluir bibliotecas externas, como bibliotecas de acesso ao banco de dados, e ajudam a organizar seu código em partes separadas com responsabilidades limitadas. Você deve tentar indentificar partes reusáveis do seu código e transformá-las em módulos separados para reduzir a quantidade de código por arquivo e para ficar mais fácil de ler e manter seu código.

Utilizar módulos em Node é simples, você usa a função require(), que recebe um argumento: o nome da biblioteca do core ou o caminho do arquivo do módulo que você quer carregar. É aconselhável compreender como funciona a função require do Node.js. Outro artigo aqui também já tratou como utilizar módulos em sua aplicação Node.js.

A mais expressiva e simples solução para módulos em JavaScript foi adotada como padrão pelo Node, o padrão CommonJS, que é usado de forma levemente diferente no Node. CommonJS te dá uma boa sintaxe, e mais importante, módulos de primeira classe. E se você estiver se perguntando porque módulos de primeira classe são importantes, a resposta é “pela mesma razão que funções de primeira classe são importantes”.

Lembrando que na teoria de linguagens de programação, diz-se que algo é um objeto de primeira classe em uma linguagem quando ele pode ser construído em tempo de execução, passado como parâmetro, atribuído a uma variável, devolvido como resultado de uma função, incluído em uma estrutura de dados maior, etc. Na maioria das linguagens de programação os números e as strings são objetos de primeira classe, mas em muitas linguagens funções não são objetos de primeira classe. Em Javascript, Python, Ruby, C# e muitas outras linguagens, sabemos que funções são objetos de primeira classe, e essa facilidade acaba sendo chamado de “funções de primeira classe”.

Em Node, arquivos e módulos tem correspondência de um para um. Observe o exemplo a seguir onde nosso módulo calculo.js carrega o módulo circulo.js salvo no mesmo diretório.

// calculo.js
var circulo = require('./circulo.js');
console.log( 'Um circulo de raio 4 tem area de '
             + circulo.area(4));
// circulo.js
var PI = Math.PI;

exports.area = function (r) {
  return PI * r * r;
};

exports.circunferencia = function (r) {
  return 2 * PI * r;
};

Neste exemplo o módulo circulo.js exportou as funções area() e circunferencia() . Aqui é apresentado o principal conceito do sistema de carregamento módulos do Node, para um módulo exportar um objeto, ou um construtor, basta adicioná-lo no objeto especial exports . Todas as demais variáveis declaradas dentro do módulo serão variáveis privadas do módulo e não serão exportadas, neste exemplo a variável PI é privada do módulo circulo.js e não é acessível no escopo de calculo.js .

É importante frisar que exports é uma referência para module.exports utilizado apenas para o acréscimo de objetos, caso você queira exportar um único item, como um construtor, você vai precisar usar o objeto module.exports diretamente. Acompanhe o exemplo do nosso módulo construtor.js apresentado a seguir.

// construtor.js
function MeuConstrutor (opts) {
  //...
}

// CUIDADO A LINHA ABAIXO NAO EXPORTA NADA
exports = MeuConstrutor;

// A linha abaixo exporta o construtor
module.exports = MeuConstrutor;

Note que a linha exports = MeuConstrutor; não irá ter o comportamento esperado, caso o exemplo parasse nesta linha apenas um objeto vazio seria exportado. Para exportar um único objeto, ou um construtor como neste exemplo, deve-se utilizar a variável module.exports .

O sistema de módulos é implementado no módulo require("module") .

Importações cíclicas

Quando temos chamadas cíclicas de módulos utilizando a função require() um módulo não é retornado enquanto não tiver sido executado por completo, e o módulo será salvo em cache a primeira vez que for carregado.

Acompanhe a situação apresentada a seguir para compreender o dinâmica das importações cíclicas.

// a.js
console.log('a iniciando');
exports.pronto = false;
var b = require('./b.js');
console.log('dentro de a, b.pronto = %j', b.pronto);
exports.pronto = true;
console.log('a pronto');
// b.js
console.log('b iniciando');
exports.pronto = false;
var a = require('./a.js');
console.log('dentro de b, a.pronto = %j', a.pronto);
exports.pronto = true;
console.log('b pronto');
// principal.js
console.log('principal iniciando');
var a = require('./a.js');
var b = require('./b.js');
console.log('dentro de principal, a.pronto=%j, b.pronto=%j', a.pronto, b.pronto);
console.log('principal pronto');

Quando o módulo principal.js carrega a.js, por sua vez a.js carrega b.js, neste ponto b.js tenta carregar a.js . Para evitar um cíclo infinito, de a.js carregando b.js e vice-versa, uma cópia incompleta do objeto exports de a.js é retornada para o módulo b.js . Então b.js completa seu carregamento, e seu objeto exports é fornecido para o módulo a.js .

Quando principal.js terminar de carregar, ambos os módulos estarão prontos. A saída deste programa seria assim:

$ node principal.js
principal iniciando
a iniciando
b iniciando
dentro de b, a.pronto = false
b pronto
dentro de a, b.pronto = true
a pronto
dentro de principal, a.pronto=true, b.pronto=true
principal pronto

Esta foi a forma que os desenvolvedores do Node utilizaram para resolver o problema de importações cíclicas, e caso você tenha dependências cíclicas de módulos no seu programa, se certifique de planejar corretamente para que elas funcionem como o esperado.

Módulos do núcleo do Node

O Node tem muitos módulos compilados em arquivos binários. Estes módulos estão descritos em detalhes na documentação da API do Node - em Inglês.

Os módulos do núcleo (em Inglês core) estão dentro da pasta lib/ na raiz do Node.

O Node sempre dá preferência em carregar os módulos do núcleo se seu identificador é passado para a função require() . Por exemplo require('http') irá sempre retornar o módulo HTTP embutido no núcelo do Node, mesmo que tenha um arquivo com o mesmo nome nesta pasta.

Arquivo dos Módulos

Caso o nome do arquivo passado para a função require() não for encontrado, então o Node irá tentar carregar um arquivo com o nome passado adicionando a extensão ` .js, .json, e por fim .node` .

Notar que arquivos ` .js são interpretados como arquivos de texto **JavaScript** e arquivos .json são analisados como arquivos de texto **JSON**. Já arquivos .node são interpretados como um módulo compilado carregado com dlopen` .

Um módulo com prefixo ` / ` será buscado no caminho absoluto sistema de arquivos. Por exemplo, require('/home/node/exemplo.js') irá carregar o arquivo presente em ` /home/node/exemplo.js` .

Um módulo com prefixo ` ./ ` será buscado no caminho relativo ao módulo que chamou a função require() . Como no exemplo apresentado, circulo.js deve estar no mesmo diretório de calculo.js para que require('./circulo.js') encontre ele.

Já um módulo com prefixo ` ../ ` será buscado na pasta superior relativa ao módulo que chamou a função require() .

Caso o módulo requisitado não contenha o prefixo ` / ` ou ` ./ ` para indicar a localização do arquivo o módulo será considerado ou um módulo do núcleo ou um módulo instalado na pasta node_modules, gerenciada pela NPM .

Agora se o caminho do arquivo não existir, require() vai emitir um Erro com sua propriedade code igua a 'MODULE_NOT_FOUND' .

Carregando da pasta node_modules

Se o identificador passado para require() não é um módulo nativo, e não começa com ` / , ../ , ou ./ , então Node começa a buscar no diretório pai do módulo atual, e adiciona /node_modules`, e espera carregar o módulo requisitado neste local. Se ele não for encontrado lá, então ele move para o diretório pai do diretório atual, e assim sucessivamente, até atingir o diretório raíz do sistema.

Por exemplo, se o arquivo ` /home/node/projetos/exemplo.js chamou require(‘meu_modulo.js’)`, então o Node deve procurar nos seguintes locais, nesta ordem:

/home/node/projetos/node_modules/meu_modulo.js
/home/node/node_modules/meu_modulo.js
/home/node_modules/meu_modulo.js
/node_modules/meu_modulo.js

Pastas como Módulos

É conveniente organizar programas e bibliotecas dentro de seus diretórios independentes, e então fornecer um único acesso para esta biblioteca. Há três maneiras que uma pasta pode ser passada para a função require() como um argumento.

A primeira é criando um arquivo package.json na raíz da pasta, que especifica o módulo principal - main - e um nome - name . Veja um exemplo do arquivo package.json a seguir:

{ "name" : "some-library",
  "main" : "./lib/uma-biblioteca.js" }

Se este exemplo estivesse na pasta ` ./uma-biblioteca, então uma chamada require(‘./uma-biblioteca’) tentaria carregar o arquivo ./uma-biblioteca/lib/uma-biblioteca.js` .

Caso não haja um arquivo package.json presente no diretório, então node tentaria carregar um arquivo index.js ou index.node presentes neste diretório. Se no exemplo dado não existisse o arquivo package.json, então uma chamada a require('./uma-biblioteca') tentaria carregar o arquivo ` ./uma-biblioteca/index.js ou ./uma-biblioteca/index.node` .

Módulos salvos em Cache

Os módulos são salvos em Cache após a primeira vez que eles são carregados. Isto significa, entre outras coisas, que toda chamada require('modulo') vai retornar exatamente o mesmo objeto retornado na primeira chamada, se ela fosse importar o mesmo arquivo.

Múltiplas chamadas para require('modulo') não pode fazer com que o código do módulo seja executado múltiplas vezes. Esta é uma característica importante. Com isso, objetos podem ser retornados, permitindo assim que dependências transitivas possam ser carregadas mesmo se elas causarem ciclos - isto é, tiverem importações cíclicas.

Caso você precise que um módulo execute um código várias vezes, então exporte uma função contendo este código e faça chamadas para esta função.

Advertências para Módulos salvos em Cache

Um módulo é salvo no Cache baseado no seu endereço em disco, por exemplo ` /home/node/projetos/node_modules/meu_modulo.js . Uma vez que uma chamada para a função require(‘meu_modulo’) faz uma busca pelo módulo em vários diretórios, seguindo a sequência passada, caso ela encontre o módulo em uma pasta diferente da já salva em Cache, por exemplo no diretório atual ./meu_modulo.js`, então o módulo será considerado um módulo novo e não será usado a referência já salva em Cache.

O objeto module

Em cada módulo module é uma variável re referencia para o objeto que representa o módulo atual. Em particular module.exports é acessível através do módulo global exports . O objeto modulo na realidade não é global, mas um objeto local de cada módulo.

module.exports

O objeto module.exports é criado pelo sistema de módulos do Node e a variável exports aponta para este objeto. Seu módulo pode retornar vários objetos e funções simplesmente adicionando-os a variável export, por exemplo exports.falar = function(){ console.log('Bom dia!') }; . Porém algumas vezes gostaríamos que nosso módulo retorne a instância de uma classe. Atribua o objeto desejado para ser exportado em module.exports . Atenção, neste caso não utilize a variável exports . Por exemplo suponha que estivéssemos fazendo um módulo chamado a.js .

// a.js
var EventEmitter = require('events').EventEmitter;

module.exports = new EventEmitter();

// Executar algum trabalho, e apos algum tempo emitir
// o evento 'pronto' para o proprio modulo.
setTimeout(function() {
  module.exports.emit('pronto');
}, 1000);

Então em outro arquivo nós poderíamos fazer:

// main.js
var a = require('./a');
a.on('pronto', function() {
  console.log('O modulo a esta pronto!');
});

É importante saber que atribuições a module.exports devem ser feitas imediatamente, elas não podem ocorrer em nenhum callback. O exemplo a seguir não irá funcionar.

// x.js

setTimeout(function() {
  module.exports = { a: "Ola" };
}, 0);
// y.js

var x = require('./x');
console.log(x.a);

Um módulo Node tem variáveis disponíveis por padrão no escopo de cada módulo, acompanhe a lista abaixo contendo as mais interessantes:

  • __filename: O nome do arquivo do código que está sendo executado
  • __dirname: O nome do diretório que está salvo o script que está sendo executado
  • process: Um objeto que é associado ao presente processo em execução. Além de variáveis, este objeto tem métodos como process.exit, process.cwd e process.uptime
  • process.argv: Um array contendo os argumentos de linha de comando. O primeiro elemento será node, o segundo elemento será o nome do arquivo JavaScript, e os próximos serão todos os argumentos de linha de comandos adicionais, caso sejam atribuídos
  • process.stdin, process.stout, process.stderr: Streams que correspondem à entrada padrão, a saída padrão, e a saída de erro padrão do processo atual
  • process.env: Um objeto contendo as variáveis de ambiente do usuário do processo atual
  • require.main: Quando um arquivo é executado diretemente pelo Node, require.main é atribuído à este module .

Execute o exemplo a seguir para que você acompanhar a utilização destas variáveis dentro de um módulo, você pode executar o código abaixo salvando-o em um arquivo exemplo.js e executando através da linha de comando node exemplo.js .

// exemplo.js

console.log('__filename: ', __filename);
console.log('__dirname: ', __dirname);
console.log('process.argv: ', process.argv);
console.log('process.env: ', process.env);
if(module === require.main) {
  console.log('Este e o modulo principal sendo executado.');
}

Carregando seu módulo de pastas globais

Se a variável de ambiente NODE_PATH é definida com uma lista de caminhos absolutos delimitados por dois pontos, então o node vai buscar nestes caminhos por módulos se eles não forem encontrados em nenhum dos locais anteriormente buscados. Lembrando que no Windows para definir uma variável de ambiente você deve acessar as propriedades do Meu Computador, acessar Configurações avançadas do sistema e entrar nas Variáveis de Ambiente, e para adicionar a variável NODE_PATH clique Novo…, adicione o nome da variável como NODE_PATH e no valor da variável você pode inserir uma lista de caminhos absolutos delimitados por ponto e virgula - no caso do Windows.

Adicionalmente, node vai buscar nas seguintes localizações:

$HOME/.node_modules
$HOME/.node_libraries
$PREFIX/lib/node

Onde $HOME é o diretório Home do usuário e $PREFIX é a configuração node_prefix do Node.

Estes são, em maior parte, por razões históricas. Sendo que você sempre será altamente encorajado à colocar suas dependências localmente na pasta node_modules . Elas serão carregadas mais rápido e com mais segurança.

Identificando o módulo principal

Quando um arquivo é executado diretamente no Node, require.main é definido como seu module . Isso significa que você pode determinar quando um arquivo está sendo executado diretamente fazendo o teste:

require.main === module

Para o exemplo apresentado principal.js, esta operação retornará true se executado diretamente via node principal.js, porém retornará false se executado através de uma importação require('./principal.js')

Pelo fato do objeto module fornecer a propriedade filename (normalmente equivalente à variável global - diferente para cada módulo - __filemane), você também pode obter o endereço do ponto de entrada da aplicação, o módulo principal, através da variável require.main.filename .

Note que se você importar um módulo através do REPL do Node a variável require.main naturalmente retornará undefined, porque não há um módulo principal sendo executado, e sim a linha de comando do Node.

NPM

Não se pode falar em módulos em Node sem falar do NPM, ele é o gerenciador de pacotes incluso dentro do pacote do Node. Não vou entrar neste artigo, mas vocês perceberão que ele é fantástico e você deve usá-lo. Você pode ler mais neste artigo introdutório sobre a NPM e neste outro artigo que fala mais sobre a NPM e como instalar pacotes utilizando ela.

Se você não está usando a NPM e os módulos do Node para implementar sua aplicação, você pode estar fazendo isso errado!

– Modularizar: Escrever pequenos módulos que fazem apenas uma coisa, e integrá-los para fazer coisas mais complexas – Usar o gerenciador de pacotes: Usar a NPM para administrar suas dependências e parar de se preocupar com elas – Utilizar módulos em Node: É um sistema de módulos simples e expressivo que facilita a modularização de sua aplicação em Node e te proporciona módulos paramétricos de primeira classe