Por que eu devo ler este artigo:Quando se trata de desenvolvimento front-end, o Node.js é disparado uma das tecnologias que mais intrigam e impressionam a comunidade de desenvolvimento, por suas várias características fundamentais, como poder usar JavaScript no servidor, bem como o papel que ele exerce nessa comunicação.

Para o leitor que já está familiarizado com JavaScript, Node.js é só mais um curto passo ao aprendizado. Neste artigo veremos desde conceitos de configuração, ambiente, características, posicionamento atual, até o as vantagens da programação assíncrona, foco central que discute como implementar um bom código que evite a criação do famoso anti-patter callback-hell do JavaScript.


Guia do artigo:


Tudo na web se trata de consumismo e produção de conteúdo. Ler ou escrever blogs, assistir ou enviar vídeos, ver ou publicar fotos, ouvir músicas e assim por diante.

Isso fazemos naturalmente todos os dias na internet. E cada vez mais aumenta a necessidade dessa interação entre os usuários com os diversos serviços da web.

De fato, o mundo inteiro quer interagir mais e mais na internet, seja através de conversas com amigos em chats, jogando games online, atualizando constantemente suas redes sociais ou participando de sistemas colaborativos.

Esses tipos de aplicações requerem um poder de processamento extremamente veloz para que seja eficaz a interação em tempo real entre cliente e servidor.

E mais, isto precisa acontecer em uma escala massiva, suportando de centenas a milhões de usuários.

Então o que nós, desenvolvedores, precisamos fazer? Precisamos criar uma comunicação em tempo real entre cliente e servidor — que seja rápida, atenda muitos usuários ao mesmo tempo e utilize recursos de I/O (dispositivos de entrada e saída) de forma eficiente. Qualquer pessoa com experiência em desenvolvimento web sabe que a versão atual do HTTP 1.1 não foi projetada para suportar tais requisitos.

E pior, infelizmente existem aplicações que adotam de forma incorreta o uso deste protocolo, implementando soluções workaround ("Gambiarras") que requisitam constantemente o servidor de forma assíncrona, geralmente aplicando a técnica de long-polling.

Para sistemas trabalharem em tempo real, servidores precisam enviar e receber dados utilizando comunicação bidirecional, ao invés de utilizar intensamente request/response do HTTP aplicando Ajax. E também temos que manter esse tipo comunicação de forma leve e rápida para permitir escalabilidade em uma aplicação.

O problema das arquiteturas bloqueantes

Os sistemas para web desenvolvidos sobre plataformas como .NET, Java, PHP, Ruby ou Python possuem uma característica em comum: eles paralisam um processamento enquanto utilizam um I/O no servidor. Essa paralisação é conhecida como modelo blocking-thread.

Para entender melhor esse conceito, vamos a um exemplo: temos uma aplicação web e nela cada processo é uma requisição feita pelo usuário.

Com o decorrer da aplicação, novos usuários irão acessá-la, gerando múltiplos processos no servidor. Um sistema de arquitetura bloqueante vai enfileirar cada requisição que são realizadas ao mesmo tempo e depois ele vai processando, uma a uma.

Este modelo não permite múltiplos processamentos ao mesmo tempo. Enquanto uma requisição é processada, as demais ficam em espera, ou seja, a aplicação bloqueia os demais processos de fazer I/O no servidor, mantendo-os em um pequeno período de tempo numa fila de requisições ociosas.

Esta é uma arquitetura clássica, existente em diversos sistemas web e que possui um design ineficiente. É gasta grande parte do tempo mantendo requisições em uma fila ociosa enquanto é executado I/O para apenas uma requisição. Para exemplificar melhor, tarefas de I/O são tarefas de enviar e-mail, consultar o banco de dados, leitura de arquivos em disco.

E essas tarefas bloqueiam o sistema inteiro enquanto não são finalizadas. Com o aumento de usuários acessando o sistema, a frequência de gargalos será maior, surgindo a necessidade de se fazer uma escalabilidade vertical (upgrade dos servidores) ou escalabilidade horizontal (inclusão de novos servidores trabalhando para um load balancer). Ambos os tipos se tornam custosos, quando se fala em gastos com infraestrutura.

O ideal seria buscar novas tecnologias que façam bom uso dos servidores existentes, que permitam o uso intenso e máximo do processamento atual.

Foi baseado neste problema que, no final de 2009, Ryan Dahl, e mais 14 colaboradores, criaram o Node.js. Esta tecnologia possui um modelo inovador: sua arquitetura é totalmente non-blocking thread (não-bloqueante) que apresenta uma boa performance com consumo de memória e utiliza ao máximo e de forma eficiente o poder de processamento dos servidores, principalmente em sistemas que produzem uma alta carga de processamento.

Sistemas criados com Node.js estão livres de aguardarem por muito tempo o resultado de seus processos, e mais importante, não sofrem dead-locks, pelo simples fato de trabalharem em single-thread. Além dessas vantagens, desenvolver sistemas nessa plataforma é super simples e prático.

O Node.js é uma plataforma altamente escalável e de baixo nível, logo, você terá a possibilidade de programar diretamente com diversos protocolos de rede e internet ou utilizar bibliotecas que acessam recursos do sistema operacional. Para programar em Node.js basta dominar a linguagem JavaScript. Ele utiliza a engine Javascript V8, a mesma utilizada no navegador Google Chrome.

Características do Node.js

Single-thread

Suas aplicações serão single-thread, ou seja, cada aplicação terá instância de uma única thread por processo iniciado. Se você está acostumado a trabalhar com programação multithread, por exemplo, Java ou .NET, infelizmente não será possível com Node.js, mas saiba que existem outras maneiras de se criar um sistema que com processamento paralelo.

Por exemplo, você pode utilizar uma biblioteca nativa chamada de clusters, que é um módulo que permite implementar uma rede de processos de sua aplicação, você cria N processos de sua aplicação e uma delas se encarrega de balancear a carga, permitindo processamentos paralelos um único servidor. Outra maneira é adotar a programação assíncrona nas tarefas se seu servidor.

Esse será o assunto mais abordado durante o decorrer deste artigo, pelo qual explicaremos diversos cenários e exemplos práticos de como são executadas em paralelo, funções em assíncronas não-bloqueante.

Event-Loop

Node.js é orientado a eventos. Ele segue a mesma filosofia de orientação de eventos do JavaScript de browser; a única diferença são os eventos, ou seja, não existem eventos de click do mouse, keyup do teclado ou qualquer evento de componentes do HTML.

Na verdade, trabalhamos com eventos de I/O como, por exemplo, o evento connect de um banco de dados, um open de um arquivo, um data de um streaming de dados e muitos outros.

O Event-Loop é o agente responsável por escutar e emitir eventos. Na prática, ele é basicamente um loop infinito que a cada iteração verifica em sua fila de listening de eventos se um determinado evento foi disparado. Quando ocorre, é emitido um evento. Ele o executa e envia para fila de executados.

Quando um evento está em execução, nós podemos programar qualquer lógica dentro dele e isso tudo acontece graças ao mecanismo de callback de função do JavaScript.

Esta técnica que permite trabalhar em cima do design event-driven com Node.js. Ele foi inspirado pelos frameworks Event Machine do Ruby e Twisted do Python. Porém, o Event-loop do Node.js é mais performático por que seu mecanismo é nativamente executado de forma não-bloqueante.

Isso faz dele um grande diferencial em relação aos seus concorrentes que realizam chamadas bloqueantes para iniciar seus respectivos loops de eventos.

Porque deve-se aprender Node.js?

JavaScript everywhere: Praticamente o Node.js usa JavaScript como linguagem de programação server-side. Essa característica permite que você reduza e muito sua curva de aprendizado, afinal a linguagem é a mesma do JavaScript client-side, seu desafio nesta plataforma será de aprender a fundo como funciona a programação assíncrona para se tirar maior proveito dessa técnica em sua aplicação.

Outra vantagem de se trabalhar com JavaScript é que você poderá manter um projeto de fácil manutenção. Você terá facilidade em procurar profissionais para seus projetos, e vai gastar menos tempo estudando uma nova linguagem server-side. Uma vantagem técnica do JavaScript comparador com outras linguagens de back-end é que você não irá utilizar mais aqueles frameworks de serialização de objetos JSON (JavaScript Object Notation), afinal o JSON client-side é o mesmo no server-side, há também casos de aplicações usando banco de dados orientado a documentos (por exemplo: MongoDB ou CouchDB) e neste caso toda manipulação dos dados são realizadas através de objetos JSON também.

Comunidade ativa: Esse é um dos pontos mais fortes do Node.js. Atualmente existem várias comunidades no mundo inteiro trabalhando muito para esta plataforma, seja divulgando posts e tutoriais, palestrando em eventos e principalmente publicando e mantendo +70000 módulos no site NPM (Node Package Manager). Aqui no Brasil temos dois grupos bem ativos no Google Groups temos o NodeBR e no Facebook tem o grupo Node.js Brasil (ver seção Links).

Ótimos salários: Desenvolvedores Node.js geralmente recebem bons salários. Isso ocorre pelo fato de que infelizmente no Brasil ainda existem poucas empresas adotando essa tecnologia. Isso faz com que empresas que necessitem dessa tecnologia paguem salários na média ou acima da média para manterem esses desenvolvedores em seus projetos.

Outro caso interessante são as empresas que contratam estagiários ou programadores juniores que tenham ao menos conhecimentos básicos de JavaScript, com o objetivo de treiná-los para trabalhar com Node.js. Neste caso, não espere um alto salário e sim um amplo conhecimento preenchendo o seu currículo.

Ready for real-time: O Node.js ficou popular graças aos seus frameworks de interação real-time entre cliente e servidor. O SockJS, Socket.IO, Engine.IO são alguns exemplos disso. Eles são compatíveis com o recente protocolo WebSockets e permitem trafegar dados através de uma única conexão bidirecional, tratando todas as mensagens através de eventos JavaScript.

Big players: LinkedIn, Wallmart, Groupon, Microsoft e Paypal são algumas das empresas usando Node.js atualmente, no Brasil conheço algumas empresas por exemplo: Agendor, Sappos, Neoassist e tem mais um monte de outras empresas e projetos (veja na seção Links).

Saiba mais Série Programe com o Node.js

Onde posso usar o Node.js?

Chats

Um chat é o mais típico exemplo de aplicação multi-usuário em tempo real. Desde IRC até muitos protocolos proprietários e abertos sobre portas não-padrão, até mesmo a habilidade de implementar tudo hoje no Noje.js com websockets rodando sobre a mesma porta padrão 80.

A aplicação de chat é realmente é um ótimo exemplo de ser usada com Noje.js: é leve, tem alto tráfico de dados, porém exige pouco processamento/computação e que executa dentre vários dispositivos.

Além de tudo, é bem simples, ótimo para os desenvolvedores que estão iniciando o aprendizado na tecnologia, cobrindo a maior parte dos paradigmas usados pela linguagem. Basicamente, uma aplicação desse tipo funciona dentro de um domínio de website já pronto onde as pessoas podem ir e efetuar a troca de mensagens entre si conectados por uma estrutura de programação e redes. No lado do servidor, temos uma aplicação simples que implementa duas coisas:

· Uma requisição GET que serve a página web contendo ambas mensagem e botão de enviar para inicializar uma nova entrada de mensagem;

· Websockets que escutam por novas mensagens emitidas pelos seus clientes.

No lado cliente nós temos uma página HTML com uma série de handlers configurados, um para o botão de Envio, que seleciona a mensagem e a envia para o websocket, e outro que escuta por mensagens que estão chegando no cliente. Obviamente, este é um modelo simples e básico, mas que baseia os demais em variância às suas complexidades.

API sobre um objeto DB

Embora o Node.js realmente brilhe com aplicações em tempo real, é natural ter que expor os dados a partir de um objeto de banco de dados (MongoDB, por exemplo). Dados JSON armazenados permitem que o Node.js funcione sem a diferença de impedância e conversão de dados.

Por exemplo, se você estiver usando Rails, você deve converter JSON para modelos binários e em seguida, expô-los de volta como JSON sobre o HTTP quando os dados são consumidos por frameworks como o Backbone.js, Angular.js, etc., ou mesmo por simples chamadas jQuery Ajax.

Com o Node.js, você pode simplesmente expor seus objetos JSON com uma API REST para o cliente a consumir. Além disso, você não precisa se preocupar com a conversão entre JSON e tudo aquilo que for ler ou escrever em seu banco de dados (se estiver usando MongoDB). Em suma, você pode evitar a necessidade de múltiplas conversões usando um formato de serialização de dados uniforme em todo o cliente e servidor de banco de dados.

Entradas de Filas

Se você está recebendo uma grande quantidade de dados simultâneos, o banco de dados pode se tornar um gargalo. O Node.js pode facilmente lidar com as próprias conexões simultâneas. Mas porque o acesso a bancos de dados é uma operação bloqueante, nos depararemos certamente com problemas. A solução é reconhecer o comportamento do cliente antes de os dados serem realmente gravados na base de dados.

Com essa abordagem, o sistema mantém a sua capacidade de resposta sob uma carga pesada, o que é particularmente útil quando o cliente não precisa de confirmação firme de uma gravação de dados bem-sucedida.

Exemplos típicos incluem: o registro ou gravação de dados de rastreamento de usuário, com processamento em lotes e não utilizados até mais tarde; bem como operações que não precisam ser refletidas instantaneamente (como a atualização de um count 'Likes' no Facebook), onde a consistência eventual (tantas vezes usada no mundo NoSQL) é aceitável.

Os dados são colocados em fila por algum tipo de cache ou enfileiramento de mensagens de infraestrutura (por exemplo, RabbitMQ, ZeroMQ) e digerido por um processo de banco de dados separado em lotes e escrita, ou computação intensiva de serviços de processamento de back-end, escrito em uma plataforma de melhor desempenho para tais tarefas. Comportamento semelhante pode ser implementado com outras linguagens/frameworks, mas não no mesmo hardware, com o mesmo alto rendimento mantido.

Fluxo de Dados

Em plataformas mais tradicionais da web, solicitações e respostas HTTP são tratadas como eventos isolados; na verdade, eles são realmente streams. Esta observação pode ser utilizada no Node.js para construir alguns recursos interessantes.

Por exemplo, é possível processar arquivos enquanto eles ainda estão sendo carregados, uma vez que os dados vêm no meio de um stream e podemos processá-los de forma online. Isso poderia ser feito para áudio em tempo real ou codificação de vídeo e proxy entre as diferentes fontes de dados.

Proxy

O Node.js é facilmente utilizado como um proxy do lado do servidor, onde ele pode lidar com uma grande quantidade de conexões simultâneas de uma maneira non-blocking.

É especialmente útil para proxys de diferentes serviços com diferentes tempos de resposta ou a coleta de dados a partir de vários pontos de origem.

Considere como exemplo um aplicativo que do lado do servidor se comunica com recursos de terceiros, puxando dados de diferentes fontes ou armazenando ativos como imagens e vídeos para serviços em nuvem de terceiros.

Embora existam servidores proxy dedicados, utilizando Node.js, ao invés, pode ser útil se a sua infraestrutura de proxy é inexistente ou se você precisa de uma solução para o desenvolvimento local. Por isso, você pode construir um aplicativo do lado do cliente com um servidor de desenvolvimento Node.js para ativos e solicitações de proxy/Stubbing API, enquanto em produção você lidaria com tais interações com um serviço dedicado de proxy (nginx, HAProxy, etc.).

Painel de Monitoramento de Aplicações

Outro caso de uso comum, em que o Node com websockets se encaixa perfeitamente é o rastreamento de visitantes do site e visualização de suas interações em tempo real.

Você poderia reunir estatísticas em tempo real a partir de seu usuário, ou mesmo movê-lo para o próximo nível através da introdução de interações específicas com seus visitantes, abrindo um canal de comunicação quando chegam a um ponto específico no seu filtro.

Imagine como é possível melhorar um dado negócio, se for possível saber o que os visitantes estavam fazendo em tempo real, se for possível visualizar suas interações. Com o tempo real, nos dois sentidos bases de Node.js, agora é possível.

Painel de Monitoramento de Sistemas

Agora, vamos visitar o lado da infraestrutura das coisas. Imagine, por exemplo, um provedor de SaaS que quer oferecer a seus usuários uma página de serviço de monitoramento (por exemplo, página de status do GitHub). Com o evento de circuito Node.js, podemos criar um poderoso painel baseado na web que verifica os estados dos serviços de forma assíncrona e envia dados para os clientes que usam websockets.

Tanto interna quanto os status dos serviços públicos podem ser relatados ao vivo e em tempo real, utilizando esta tecnologia.

Indo um pouco além com essa ideia e tentando imaginar um Centro de Operações de Rede (NOC) aplicações de monitoramento em um operador de telecomunicações, nuvem/rede/provedor de hospedagem, ou alguma instituição financeira, todos executados na pilha web aberta apoiada pelo Node.js e websockets em vez de Java e/ou Java Applets, como foi visto por muito tempo.

Hands on Node.js

Para configurar um ambiente Node.js, independente de qual sistema operacional, as dicas serão as mesmas. Somente alguns procedimentos serão diferentes para cada sistema, principalmente para o Windows, mas não será nada grave.

NOTA: Todos os códigos aplicados neste artigo funcionarão apenas em versões do Node.js 0.8.X ou superior, exceto o último exemplo do artigo que utiliza a implementação de Generators do ECMAScript 6 que somente vai funcionar em uma versão unstable do Node.js 0.11.X.

Instalação do Node.js

O primeiro passo é acessar seu site oficial (ver seção Links). Em seguida, clique em Install para baixar automaticamente a última versão compatível com seu sistema operacional (isto é se seu sistema é Windows ou MacOS).

Caso você use Linux recomendo que leia em detalhes a Wiki do repositório Node.js (ver na seção Links), pois lá é explicado as principais instruções sobre como instalá-lo através de um package manager de uma distribuição Linux.

Agora instale-o e caso não ocorra problemas, basta abrir seu terminal ou prompt de comandos e digite o seguinte comando para ver as respectivas versões do Node.js e NPM que foram instaladas:

node -v && npm -v

A última versão estável que está sendo utilizada neste artigo é Node v0.10.31 e NPM 1.4.23.

Configurando o ambiente de desenvolvimento

Para configurar o ambiente de desenvolvimento basta adicionar a variável de ambiente NODE_ENV no sistema operacional. Em sistemas Linux ou MacOS, basta acessar com um editor de texto qualquer e em modo super user (sudo) o arquivo .bash_profile ou .bashrc e no final do arquivo adicione a seguinte linha de comando:

export NODE_ENV=’development’

Clique com botão direito no ícone Meu Computador e selecione a opção Propriedades e no lado esquerdo da janela clique no link Configurações avançadas do sistema. Na janela seguinte, acesse a aba Avançado e clique no botão Variáveis de Ambiente….

Agora no campo Variáveis do sistema clique no botão Novo… e no campo nome da variável digite NODE_ENV e no campo valor da variável digite development, tal como demonstrado na Figura 1. Após finalizar essa tarefa, reinicie seu computador para carregar essa variável automaticamente no sistema operacional.

Configurando variável de ambiente no Windows 7
Figura 1. Configurando variável de ambiente no Windows 7.

Rodando o Node.js

Para testarmos o ambiente, executaremos o nosso primeiro programa de Hello World. Abra seu terminal ou prompt de comando e execute o comando node. Este comando vai acessar o modo REPL (Read-Eval-Print-Loop) que permite executar códigos JavaScriptdiretamente pela tela preta. Agora digite console.log(“Hello World”) e tecle ENTER para executá-lo na hora (Figura 2).

Hello World em
Node.js via terminal
Figura 2. Hello World em Node.js via terminal.

Gerenciando dependências e módulos usando o NPM

Assim como o RubyGems do Ruby ou o Maven do Java, o Node.js também possui seu próprio gerenciador de pacotes, ele se chama NPM. Ele se tornou tão popular pela comunidade, que foi a partir da versão 0.6.X que foi integrado no instalador do Node.js, tornando-se o gerenciador padrão desta plataforma.

Isto simplificou a vida dos desenvolvedores na época, pois fez com que diversos projetos se convergissem para esta plataforma até os dias de hoje. Utilizar o NPM é muito fácil, então vejamos os comandos principais para que você tenha noções de como usá-los:

  • npm install nome-do-modulo: instala um módulo no projeto;
  • npm install -g nome-do-modulo: instala um módulo global;
  • npm install nome-do-modulo --save: instala o módulo no projeto, atualizando o package.json na lista de dependências;
  • npm list: lista todos os módulos do projeto;
  • npm list -g: lista todos os módulos globais;
  • npm remove nome-do-modulo: desinstala um módulo do projeto;
  • npm remove -g nome-do-modulo: desinstala um módulo global;
  • npm update nome-do-modulo: atualiza a versão do módulo;
  • npm update -g nome-do-modulo: atualiza a versão do módulo global;
  • npm -v: exibe a versão atual do npm;
  • npm adduser nome-do-usuario: cria uma conta no npm através do site do NPM (ver seção Links);
  • npm whoami: exibe detalhes do seu perfil público npm (é necessário criar uma conta antes);
  • npm publish: publica um módulo no site do npm (é necessário ter uma conta NPM antes).

Entendendo o package.json

Todo projeto Node.js é chamado de módulo, mas o que é um módulo? No decorrer da leitura, perceba que vamos discutir muito sobre o termo módulo, biblioteca e framework, e, na prática, eles possuem o mesmo significado.

O termo módulo surgiu do conceito de que o JavaScript trabalha com uma arquitetura modular. E todo módulo é acompanhado de um arquivo descritor, conhecido pelo nome de package.json.

Este arquivo é essencial para um projeto Node.js. Um package.json mal escrito pode causar bugs ou impedir o funcionamento correto do seu módulo, pois ele possui alguns atributos chaves que são compreendidos pelo Node.js e NPM.

Na Listagem 1 temos um package.json que contém os principais atributos para descrever um módulo.

Listagem 1. Formato de um package.json.


    {
      "name": "meu-primero-node-app",
      "description": "Meu primeiro app Node.js",
      "author": "Caio <caio@email.com>",
      "version": "1.2.3",
      "private": true,
      "dependencies": {
        "modulo-1": "1.0.0",
        "modulo-2": "~1.0.0",
        "modulo-3": ">=1.0.0"
      },
      "devDependencies": {
       "modulo-4": "*"
      }
    }

Com esses atributos, você já descreve o mínimo possível o que será sua aplicação. O atributo name é o principal. Com ele, você descreve o nome do projeto, nome pelo qual seu módulo será chamado via função require('meu-primeiro-node-app'). Em description, descrevemos o que será este módulo. Ele deve ser escrito de forma curta e clara, fornecendo um resumo do módulo.

O author é um atributo para informar o nome e e-mail do autor. Utilize o formato Nome <email> para que sites como npm reconheça corretamente esses dados. Outro atributo principal é o version, com o qual definimos a versão atual do módulo. É extremamente recomendado que tenha este atributo, se não será impossível instalar o módulo via comando npm.

O atributo private é um booleano, e determina se o projeto terá código aberto ou privado para download no npm.

Os módulos no Node.js trabalham com três níveis de versionamento. Por exemplo, a versão 1.2.3 está dividida nos níveis: Major (1), Minor (2) e Patch (3).

Repare que no campo dependencies foram incluídos 4 módulos, sendo que cada um utilizou uma forma diferente de definir a versão do projeto. O primeiro, o modulo-1, somente será incluído sua versão fixa, a 1.0.0. Utilize este tipo versão para instalar dependências cuja atualizações possam quebrar o projeto pelo simples fato de que certas funcionalidades foram removidas e ainda as utilizamos na aplicação.

O segundo módulo já possui uma certa flexibilidade de update. Ele utiliza o caractere "~" que faz atualizações a nível de patch (1.0.x). Geralmente essas atualizações são seguras, trazendo apenas melhorias ou correções de bugs.

O modulo-3 atualiza versões que sejam maior ou igual a 1.0.0 em todos os níveis de versão. Em muitos casos, utilizar ">=" pode ser perigoso, porque a dependência pode ser atualizada a nível major ou minor, contendo grandes modificações que podem quebrar um sistema em produção, comprometendo seu funcionamento e exigindo que você atualize todo código até voltar ao normal.

O último, o modulo-4, utiliza o caractere "*"; este sempre pegará a última versão do módulo em qualquer nível. Ele também pode causar problemas nas atualizações e tem o mesmo comportamento do versionamento do modulo-3.

Geralmente ele é utilizado em devDependencies, que são dependências focadas para testes ou de uso exclusivo para ambiente de desenvolvimento, e as atualizações dos módulos não prejudicam o comportamento do sistema que já está no ar.

Escopos de variáveis locais e globais

Assim como no browser, utilizamos o mesmo JavaScript no Node.js. Ele também utiliza escopos locais e globais de variáveis. A única diferença é na forma como são implementados os escopos de variáveis globais. No browser, as variáveis globais são criadas da forma como pode ser vista na Listagem 2.

Listagem 2. Variáveis globais no client-side.


  window.hoje = new Date();
  alert(window.hoje);

Em qualquer browser, a palavra-chave window permite criar variáveis globais que são acessadas em qualquer lugar. Já no Node.js, utilizamos a palavra-chave global para aplicar essa mesma técnica (Listagem 3).

Listagem 3. Variáveis globais no server-side do Node.js.


  
  global.hoje = new Date();
  console.log(global.hoje);

Ao utilizar global mantemos uma variável global, acessível em qualquer parte do projeto sem a necessidade de chamá-la via require ou passá-la por parâmetro em uma função. Esse conceito de variável global é existente na maioria das linguagens de programação, assim como sua utilização, portanto é recomendado trabalhar com o mínimo possível de variáveis globais para evitar futuros gargalos de memória na aplicação.

CommonJS

O Node.js utiliza nativamente o padrão CommonJS para organização e carregamento de módulos. Na prática, diversas funções deste padrão serão utilizadas com frequência em um projeto Node.js. A função require('nome-do-modulo') é um exemplo disso, ela carrega um módulo. E para criar um código modular, carregável pela função require, utilizam-se as variáveis globais: exports ou module.exports.

Veja nas listagens a seguir dois exemplos de módulos para Node.js. Primeiro, crie o código hello.js (Listagem 4) e depois crie o código human.js (Listagem 5).

Listagem 4. Criando um módulo com module.exports.


  module.exports = function(msg) {
     console.log(msg);
  };

Listagem 5. Criando um módulo com exports.


  exports.hello = function(msg) {
     console.log(msg);
  };

A diferença entre o hello.js e o human.js está na maneira como eles serão carregados. Em hello.js carregamos uma única função modular, e em human.js é carregado um objeto com funções modulares. Essa é a grande diferença entre eles. Para entender melhor na prática, crie o código app.js para carregar esses módulos:


  var hello = require('./hello');
  var human = require('./human');
  hello('Olá pessoal!');
  human.hello('Olá galera!');

Tenha certeza de que esses módulos hello.js, human.js e app.js estão na mesma pasta, e em seguida, rode o comando:

node app.js

E então, o que aconteceu? O resultado foi praticamente o mesmo: o app.js carregou os módulos hello.js e human.js via função require(), em seguida foi executada a função hello("Olá pessoal!”) que imprimiu a mensagem "Olá pessoal!" e, por último, o objeto human executou sua função human.hello('Olá galera!').

Um detalhe final é que também é possível criar um objeto com função modular usando module.exports, apenas faça com que o módulo retorne um objeto com funções públicas, semelhante ao código da Listagem 6.

Listagem 6. Emulando o comportamento das funções exports usando module.exports.


  module.exports = function() {
     return {
        hello: function(msg) {
           console.log(msg);
        }
     };
  };

E assim você terá um módulo com mesmo comportamento do human.hello(“olá").

Programando de forma assíncrona

Agora vamos para a última parte do nosso artigo. Agora que já temos uma base sobre o que é e como usar o Node.js, vamos focar em aprender boas práticas sobre funções assíncronas. Afinal este é o paradigma principal desta plataforma, e é muito importante dominar esses conceitos para se tirar melhor proveito desta tecnologia, assim como entender como funciona funções assíncronas.

O código a seguir exemplifica as diferenças entre uma função síncrona e assíncrona em relação à linha do tempo na qual elas são executadas. Basicamente, criaremos um loop de cinco iterações, sendo que a cada iteração será criado um arquivo texto com o mesmo conteúdo "Hello Node.js!". Primeiro vamos começar com o código síncrono. Crie o arquivo text_sync.js com o código da Listagem 7.

Listagem 7. Escrevendo arquivo de texto de forma síncrona.


  var fs = require('fs');
  for(var i = 1; i <= 5; i++) {
      var file = "sync-txt" + i + ".txt";
      fs.writeFileSync(file, "Hello Node.js!");
      console.log("Criando arquivo: " + file);
  }

Agora vamos criar o arquivo text_async.js, com seu respectivo código, diferente apenas na forma de chamar a função fs.writeFileSync, que será a versão assíncrona fs.writeFile, pelo qual seu retorno de sucesso acontece através da execução de uma função de callback existente no terceiro argumento da função (Listagem 8).

Listagem 8. Escrevendo arquivo de texto de forma assíncrona.


  var fs = require('fs');
  for(var i = 1; i <= 5; i++) {
      var file = "async-txt" + i + ".txt";
      fs.writeFile(file, "Hello Node.js!", function(err, out) {
          console.log("Criando arquivo: " + file);
      });
  }

Execute os comandos node text_sync.js e depois node text_async.js. Se forem gerados 10 arquivos no mesmo diretório do código-fonte, então deu tudo certo. Mas a execução de ambos foi tão rápida que não foi possível visualizar as diferenças entre o text_async.js e o text_sync.js.

Para entender melhor as diferenças, veja os gráficos hipotéticos a seguir que ocorrem quando executamos esses módulos. O text_sync.js, por ser um código síncrono, invocou chamadas de I/O bloqueantes e gerou o gráfico da Figura 3.

meline de execução síncrona
Figura 3. Timeline de execução síncrona.

Repare no tempo de execução, o text_sync.js hipoteticamente demorou 1000 milissegundos, isto é, 200 milissegundos para cada escrita de arquivo. Já em text_async.js foram criados os arquivos de forma totalmente assíncrona, ou seja, as chamadas de I/O eramnão-bloqueantes, e isso permitiu escreverarquivos em paralelo, como naFigura 4.

Timeline de execução
assíncrona
Figura 4. Timeline de execução assíncrona.

Isto fez com que o tempo de execução levasse hipoteticamente 200 milissegundos, afinal foi invocada cinco vezes, em paralelo, a função fs.writeFile, e isso maximizou o uso de processamento e I/O e minimizou o tempo de execução.

Só para finalizar esse gráfico é apenas hipotético, e foi usado para exemplificar como funciona o assincronismo do Node.js, nem sempre um conjunto de funções assíncronas vão executar em paralelo de forma tão perfeita como foi apresentado nesses gráficos.

Threads vs Assincronismo

Por mais que as funções assíncronas possam executar em paralelo várias tarefas, elas jamais serão consideradas uma Thread (por exemplo as Threads do Java). A diferença é que Threads são manipuláveis pelo desenvolvedor, ou seja, você pode pausar a execução de uma Thread ou fazê-la esperar o término de uma outra Thread para ser executada.

Chamadas assíncronas apenas invocam suas funções em e você não controla elas, apenas trabalha com seus retornos através de uma função callback.

Pode parecer vantajoso ter o controle sobre a execução de Threads a favor de um sistema que executa tarefas em paralelo, mas pouco conhecimento sobre eles pode transformar seu sistema em um caos de travamentos de dead-locks, afinal elas são executadas de forma bloqueante.

Este é o grande diferencial das chamadas assíncronas, elas executam em paralelo suas funções sem travar processamento das outras e, principalmente, sem bloquear a aplicação.

É fundamental que o seu código Node.js invoque o mínimo possível de funções bloqueantes. Toda função síncrona impedirá, naquele instante, que o Node.js continue executando os demais códigos até que aquela função seja finalizada.

Por exemplo, se essa função fizer um I/O em disco, ele vai bloquear o sistema inteiro, deixando o processador ocioso enquanto ele utiliza outros recursos do servidor, como por exemplo leitura em disco, utilização da rede etc. Sempre que puder, utilize funções assíncronas para aproveitar essa característica principal do Node.js.

Assincronismo versus Sincronismo

Caso você ainda não esteja convencido sobre as vantagens do processamento assíncrono vou lhe mostrar como e quando utilizar bibliotecas assíncronas não-bloqueantes através de um teste prático.

Para exemplificar melhor, os códigos adiante representam um benchmark comparando o tempo de bloqueio de execução assíncrona vs síncrona. Para isso, crie três arquivos: processamento.js, leitura_async.js e leitura_sync.js. Criaremos isoladamente o módulo leitura_async.js que será responsável por fazer leitura assíncrona de arquivo grande, tal como na Listagem 9.

Listagem 9. Código de benchmark de tempo de bloqueio em leitura assíncrona.


  var fs = require('fs');
  var leituraAsync = function(arquivo){
      console.log("Fazendo leitura assíncrona");
      var inicio = new Date().getTime();
      fs.readFile(arquivo);
      var fim = new Date().getTime();
      console.log("Bloqueio assíncrono: "+(fim - inicio)+ "ms");
  };
  module.exports = leituraAsync;

Em seguida, crie o módulo leitura_sync.js, que realizará leitura síncrona no mesmo arquivo (Listagem 10).

Listagem 10. Código de benchmark de tempo de bloqueio em leitura síncrona.


  var fs = require('fs');
  var leituraSync = function(arquivo){
      console.log("Fazendo leitura síncrona");
      var inicio = new Date().getTime();
      fs.readFileSync(arquivo);
      var fim = new Date().getTime();
      console.log("Bloqueio síncrono: "+(fim - inicio)+ "ms");
  };
  module.exports = leituraSync;

Para finalizar, vamos carregar esses módulos dentro do código processamento.js. Basicamente este módulo principal vai fazer download da última versão do instalador Node.js que tem em média 7 MB de tamanho. Quando o download terminar ele vai enviar o arquivo para os módulos realizarem uma leitura de conteúdo, no término de cada leitura será apresentado o tempo de bloqueio que cada módulo obteve, como observado através da Listagem 11.

Listagem 11. Código de execução do benchmark síncrono vs assíncrono.


  var http = require('http');
  var fs = require('fs');
  var leituraAsync = require('./leitura_async');
  var leituraSync = require('./leitura_sync');
  var arquivo = "./node.exe";
  var stream = fs.createWriteStream(arquivo);
  var download = "http://nodejs.org/dist/latest/x64/node.exe";
  http.get(download, function(res) {
      console.log("Fazendo download do Node.js");
      res.on('end', function(){
          console.log("Download finalizado!");
          console.log("Executando benchmark sync vs async...");
          leituraAsync(arquivo);
          leituraSync(arquivo);
      });
  });

Rode o comando node processamento.js para executar o benchmark. E agora, ficou clara a diferença entre o modelo bloqueante e o não-bloqueante? Parece que o módulo leituraAsync executou muito rápido, mas não quer dizer que o arquivo foi lido. Ele recebe um último parâmetro, que é um callback indicando quando o arquivo foi lido, que não passamos na invocação que fizemos.

Ao usar o fs.readFileSync(), bastaria fazer var conteudo = fs.readFileSync(). Mas qual é o problema dessa abordagem? Ela bloqueia todo o processamento da aplicação! Somente quando o bloqueio terminar as demais linhas de código serão executadas, em contra partida o fs.readFile() continua executando qualquer código que não estiver dentro de seu callback, então com isso você pode, por exemplo, programar sua aplicação para fazer outras tarefas em paralelo.

A seguir, temos o resultado do benchmark realizado em na máquina que possui as seguintes configurações:

  • MacBook Air 2011;
  • Processador: Core i5 1.6GHz;
  • Memória: 4GB DDR 3;
  • Disco: 128GB SSD.
Resultado do benchmark I/O Async vs I/O Sync
Figura 5. Resultado do benchmark I/O Async vs I/O Sync.

Veja a pequena, porém significante, diferença de tempo entre as duas funções de leitura realizada na máquina (Figura 5).

Evitando Callbacks Hell

De fato, vimos o quanto é vantajoso e performático trabalhar de forma assíncrona, porém, em certos momentos, inevitavelmente implementaremos diversas funções assíncronas, que serão encadeadas uma na outra através de suas funções callback. No código da Listagem 12 vemos um exemplo desse caso.

Listagem 12. Exemplo de Callback Hell.


  var fs = require('fs');
  fs.readdir(__dirname, function(erro, contents) {
      if (erro) { throw erro; }
      contents.forEach(function(content) {
          var path = './' + content;
          fs.stat(path, function(erro, stat) {
              if (erro) { throw erro; }
              if (stat.isFile()) {
                  console.log('%s %d bytes', content, stat.size);
              }
          });
      });
  });

Reparem na quantidade de callbacks encadeados que existem em nosso código. Detalhe: ele apenas faz uma simples leitura dos arquivos de seu diretório e imprime na tela seu nome e tamanho em bytes. Uma pequena tarefa como essa deveria ter menos encadeamentos, concorda? Agora, imagine como seria a organização disso para realizar tarefas mais complexas? Praticamente o seu código seria um caos e totalmente difícil de fazer manutenções.

Por ser assíncrono, você perde o controle do que está executando em troca de ganhos com performance, porém, um detalhe importante sobre assincronismo é que, na maioria dos casos, os callbacks bem elaborados possuem como parâmetro uma variável de erro.

Verifique nas documentações sobre sua existência e sempre faça o tratamento deles na execução do seu callback: if (erro) { throw erro; }. Isso vai impedir a continuação da execução aleatória quando for identificado um erro.

Uma boa prática de código JavaScript é criar funções que expressem seu objetivo e de forma isoladas, salvando em variável e passando-as como callback. Ao invés de criar funções anônimas, por exemplo, crie um arquivo chamado callback_heaven.js com o código da Listagem 13.

Listagem 13. Minimizando callback hell declarando funções em variáveis.


  var fs = require('fs');
  var lerDiretorio = function() {
      fs.readdir(__dirname, function(erro, diretorio) {
          if (erro) return erro;
          diretorio.forEach(function(arquivo) {
              ler(arquivo);
          });
      });
  };
  var ler = function(arquivo) {
      var path = './' + arquivo;
      fs.stat(path, function(erro, stat) {
          if (erro) return erro;
          if (stat.isFile()) {
              console.log('%s %d bytes', arquivo, stat.size);
          }
      });
  };
  lerDiretorio();

Veja o quanto melhorou a legibilidade do seu código. Dessa forma deixamos mais semântico e legível o nome das funções e diminuímos o número de encadeamentos das funções de callback. A boa prática é ter o bom senso de manter no máximo até dois encadeamentos de callbacks. Ao passar disso, significa que está na hora de criar uma função externa para ser passada como parâmetro nos callbacks, em vez de continuar criando um callback hell em seu código.

Evitando Callbacks Hell usando Generators

Muitos recursos interessantes estão surgindo para a nova especificação JavaScript conhecida por ECMAScript6 ou ES6, o Generators é um deles e seu objetivo principal é minimizar callback hell em seu código. Em resumo Generators é um recurso que permite escrever funções assíncronas sem callbacks, utilizando uma sintaxe de código síncrono, retornando valores da função em um array que representa os possíveis parâmetros de uma função callback.

Como esse recurso ainda não é oficial, somente alguns browsers (últimas versões do Chrome e Firefox) o utilizam no client-side. Já no server-side temos que habilitar no Node.js, porém somente existe para as versões instáveis: 0.11.X e possivelmente será oficializada na próxima versão estável: 0.12.x.

Observação: Para utilizar uma versão 0.11.X recomendo que faça download e instalação das versões Nightlies do Node.js (ver na seção Links). Lembrando que não recomendado utilizar uma versão instável em uma aplicação em produção.

Com a versão 0.11.X instalada em sua máquina, basta executar suas aplicações utilizando a flag —harmony, por exemplo:

node --harmony app.js

Com harmony habilitado, podemos usar alguns recursos do ES6, incluindo o Generators. Porém será necessário uma instalar uma biblioteca adicional que permite trabalhar com Generators e também faz algumas magias extras. Para isso instale o módulo suspend. Antes de criarmos os códigos de Callback e Generators, instale o módulo suspend utilizando o seguinte código:

npm install suspend --save

Agora vamos a implementação. A seguir temos dois códigos que fazem a mesma tarefa: ambos criam um arquivo de texto, escreve nele um timestamp e, no fim, excluir o próprio arquivo gerado. O código da Listagem 14 utiliza vários call-backs.

Listagem 14. Outro exemplo de callback hell.


  var fs = require("fs");
  var time = new Date().getTime();
  fs.writeFile("log.txt", time, function(err) {
    console.log("Iniciando log");
    fs.readFile("log.txt", function(err, text) {
      console.log("Timestamp: " + text);
      fs.unlink("log.txt", function() {
        console.log("Log finalizado");
      });
    });
  });

Na Listagem 15 temos a segunda parte do código, que é uma versão otimizada que implementa Generators para lidar com os call-backs.

Listagem 15. Implementando Generators para minimizar callback hell.


  var fs = require('fs');
  var suspend = require('suspend');
  var resume = suspend.resume;
  var time = new Date().getTime();
  suspend(function* (){
    yield fs.writeFile("log.txt", time, resume());
    console.log("Iniciando log");
    var text = yield fs.readFile("log.txt", resume());
    console.log("Timestamp" + text);
    yield fs.unlink("log.txt", resume());
    console.log("Log finalizado");
  })();

Simplesmente o encadeamento de callbacks diminuiu com Generators, e isso deixou seu código mais limpo e menos complexo.

De fato o Node.js é uma excelente opção para desenvolvedores front-end conhecerem um pouco sobre back-end sem precisar aprender uma nova linguagem, afinal esta plataforma o mesmo JavaScript client-side, apenas com alguns detalhes diferentes.

Outro detalhe importante é sobre as vantagens das funções assíncronas e seu I/O não-bloqueante. Afinal como vimos em um benchmark, testamos uma ação de I/O simples que fazia uma leitura de um único arquivo de mais ou menos 7 MB e o tempo de bloqueio foi muito menor do que uma leitura bloqueante.

Agora, imagine esta mesma ação em larga escala, lendo múltiplos arquivos de 1 GB ao mesmo tempo ou realizando múltiplos I/Os em seu servidor para milhares de usuários, tudo isso sem bloquear a aplicação. Esse é um dos pontos fortes do Node.js!