Site menu Node.js: ferramenta e filosofia
e-mail icon
Site menu

Node.js: ferramenta e filosofia

e-mail icon

Confira também o artigo "Node.js: um humilde exemplo" onde descrevo a implementação Node.js de comentários e chat no meu site.

O Node.js é uma daquelas coisas, boas ou ruins, que mudam o panorama da nossa profissão. Neste caso, foi uma coisa majoritariamente boa.

A história

Provavelmente você já conhece o Node.js e sua origem, mas por completeza vou recontar a história.

O GMail motivou a aparição de inúmeros sites e aplicações Web "AJAX". Na esteira desta tendência, surgiram as máquinas virtuais Javascript de alta performance. Entre elas, o V8 do Google, que pelo menos na época do lançamento deixou os concorrentes comendo poeira. O V8 calou a boca de muita gente a respeito das pretensas limitações do Javascript — e entusiasmou outro tanto.

Em 2009, o Node.js foi desenvolvido por Ryan Dahl em torno do V8; essencialmente como uma forma de rodar programas Javascript fora do contexto de um browser.

O consciente coletivo da comunidade de desenvolvedores sempre sonhou em usar Javascript como uma linguagem de uso geral, particularmente no lado servidor de um sistema Web. O Node preencheu esta expectativa, de forma tardia mas efetiva.

Na realidade, o Node nem foi o primeiro software do gênero: o Rhino já era bastante utilizado neste papel, e a própria Netscape tinha lançado o Enterprise Server no meio da década de 90! Mas a performance do Node, sua filosofia inovadora, código-fonte aberto, portabilidade e um bom marketing no lançamento tornaram-no sinônimo de "Javascript para uso geral".

Para o Javascript poder funcionar como linguagem de uso geral, fora do browser, é preciso oferecer APIs para interagir com o sistema — manipular arquivos e conexões de rede, no mínimo. Em resposta a esta necessidade, o Node.js seguiu a filosofia assíncrona que já permeava o Javascript dentro do browser. Foi sua primeira grande inovação, que definiu seu caráter e provavelmente seu destino.

Em 2011 foi desenvolvido o npm, gerenciador de pacotes do Node, modelado a partir de iniciativas semelhantes bem-sucedidas como o CPAN do Perl. Em minha opinião o npm é o grande motor do sucesso do Node, pois coloca uma vastíssima coleção de bibliotecas e ferramentas ao alcance da mão. É a materialização mais próxima de outro velho sonho da comunidade: reutilização de software em larga escala, fácil e sem dor-de-cabeça.

Soluções verticais

Na esteira do npm apareceram diversas soluções "verticais" como Angular, Socket.io, Express e outros. Cada uma delas desfruta de enorme sucesso e realimenta a febre do Node.js.

Por verticais eu entendo as soluções que atendem ao mesmo tempo os lados cliente e servidor. Você instala no servidor usando npm, e ela está automaticamente disponível no browser. A conveniência de um Socket.io pode ser motivo suficiente para a adoção do Node.js num projeto. O rabo abana o cachorro.

Claro, é perfeitamente possível usar Socket.io apenas no lado cliente e reimplementar o lado servidor fora do Node. Ou usar uma implementação não-oficial. Ou então usar WebSockets diretamente. Mas fazer isto custa trabalho adicional, difícil de justificar.

Comparação com outras linguagens de uso geral

Usar Javascript como linguagem de uso geral tem três grandes atrativos. Ou, talvez, dois atrativos e meio :)

Primeiro: é uma linguagem extremamente difundida. Em aplicativos Web, é atraente a idéia de usar a mesma linguagem "dos dois lados", no browser e no servidor.

Segundo: é fácil rodar Javascript em qualquer lugar. Seja no browser, fora do browser, num computador de mesa ou num dispositivo móvel. Em toda plataforma se encontra um interpretador Javascript; e se não houver, basta escolher uma máquina virtual dentre as disponíveis e portá-la.

Terceiro: é uma excelente linguagem. Nisto muita gente vai discordar, por isso eu considero este um meio-atrativo. Pessoalmente considero Python uma linguagem mais poderosa que Javascript, e muitos citariam Ruby, de que não gosto muito.

Falta ao Javascript alguns recursos necessários para desenvolvimento de sistemas em larga escala, por exemplo um sistema de classes poderoso o suficiente para criar tipos novos, indistinguíveis dos tipos primitivos, como é possível em Python e C++.

Isto não impede, mas desencoraja o uso de Node.js em algumas aplicações "sérias", como sistemas ERP. O Node pode ser empregado neste tipo de sistema na "linha de frente", para serviços via Internet, mas as regras de negócio ficam melhor se a cargo de outra linguagem de programação.

Comparação com outras ferramentas

Conforme dito antes, Javascript já era empregado fora do browser antes de surgir o Node.js. O Rhino, uma máquina virtual escrita em Java, era e ainda é muito utilizado. Eu uso Rhino nos testes unitários dos meus projetos em Javascript, e não tenho motivos para migrá-los para Node.js.

O Rhino tem uma característica marcante: por ser baseado em Java, ele "herda" automaticamente todas as APIs Java, o que evita a necessidade de redefinir estas APIs no nível Javascript. Até mesmo os recursos de threading estão à mão. Bibliotecas Java de terceiros são facilmente distribuíveis no formado JAR.

Infelizmente, o Rhino também herda a lentidão inerente ao fato de ser uma máquina virtual implementada em cima de outra. Java também tem o sério problema do ambiente: qualquer atualização pode potencialmente quebrar seu programa, assim como uma atualização do próprio Rhino pode ter o mesmo efeito. Algumas APIs do Java são altamente embrulhadas, passando longe da simplicidade que se espera numa linguagem interpretada.

Usar o Rhino como servidor Web implicaria em adotar algum framework, como o Ringo, aí sua solução passa a depender de um framework de nicho.

Já que Node.js é muito empregado como servidor na Internet, na prática o produto correspondente do ecossistema Python é o Django. Conheço muito pouco de Django para emitir um juízo; só sei que Python é de longe uma linguagem mais poderosa. Python tem o pip, análogo ao npm, embora eu o perceba menos conveniente.

Node.js é basicamente a única forma de rodar Javascript num servidor Web. Já o Django é apenas um entre vários frameworks Python para Web. Isto fragmenta os esforços da comunidade Python; ou fragmentava, porque Django é o padrão de fato hoje em dia.

O PHP é um concorrente importante também. Como linguagem de uso geral é uma nulidade, mas como ferramenta Web ele é respeitável e tem posição proemiente. "Assim que tirado da caixa" o PHP é muito mais fácil de usar que o Node.js, e viabiliza aquele estilo de desenvolvimento "por acreção" em torno de um site Web — acreção que caracteriza os grandes projetos baseados em PHP como Facebook e Wikipedia (e que, depois de grandes, ninguém entende porque PHP foi utilizado!). Eu uso PHP no meu site, a maioria das páginas é semi-estática e não vejo motivo para mudar.

No frigir dos ovos, o que realmente pode fazer você decidir a favor do Node em detrimento de outras alternativas, é

O gerenciador de pacotes npm

Eu deveria falar primeiro da programação assíncrona, mas eu realmente acho que a maior força do Node.js está no gerenciador de pacotes. O que muitas linguagens interpretadas tentaram, o Node conseguiu: criar um sistema fácil de usar, fácil de colaborar, e que não cria problemas.

Um aspecto fundamental do npm é que, por padrão, os pacotes são instalados na pasta corrente. Se você tiver 30 aplicativos diferentes, pode instalar as dependências de cada um em sua própria pasta. Mesmo que cada aplicativo exija uma versão diferente de um mesmo módulo de terceiros, não haverá conflito.

É possível instalar pacotes do Node "globalmente", mas isto não é vantagem, salvo talvez no caso de pacotes de desenvolvimento com executáveis. O manifesto de dependências de cada aplicativo pode especificar versões ou faixas de versões, novamente atacando o problema de versionamento, e naturalmente é possível instalar todas as dependências "por atacado" com um único comando com base no manifesto.

Em outras linguagens, este problema cresce até tomar proporções monstruosas. Em Python eu costumo incluir as dependências no código-fonte do aplicativo, para facilitar a instalação em produção e evitar problemas com versões. Mas tenho de fazer isto manualmente, porque não é assim que operam o pip e os scripts de instalação dos módulos. Talvez eu devesse usar uma solução tipo Full Stack Python. Mas no npm não preciso fazer nada disso; o comportamento "assim que tirado da caixa" já é o ideal.

Programação assíncrona

A programação assíncrona é o aspecto mais disruptivo para o desenvolvedor que está conhecendo o Node.js. Sem dúvida é a causa das reações mais extremadas ao Node — louvação ou desprezo.

Um computador trabalha assincronamente o tempo todo, mas tanto os sistemas operacionais quanto as linguagens de programação ocultam este fato. Observe o seguinte programa:

f = open("arquivo.txt")
dados = f.read()
f.close()

Parece um programa trivial, que executa de cima para baixo, mas nos bastidores é tudo bem caótico. Ele vai ficar em suspenso pelo menos três vezes, onde o controle é repassado ao sistema operacional e dali para o hardware do armazenamento (disco ou SSD).

Por trás da aparente simplicidade existe um compêndio de normas envolvendo as chamadas open(), read() e close(). Num primeiro momento o desenvolvedor não precisa conhecê-las para ser produtivo e a simplicidade de uso faz crer que tais normas nem existam; mas um belo dia elas emergem.

Por exemplo, a chamada read() é "rápida" se o arquivo aberto estiver num disco local — "rápida" significa que o sistema vai executá-la imediatamente. Só vai haver um atraso significativo se inúmeros outros processos estiverem competindo pela CPU.

Por outro lado, se o disco for remoto, o read() pode demorar segundos ou minutos para ser cumprida. Se o programa também precisa executar uma tarefa a cada 60 segundos, como é que fica?

Para sair desta enrascada, teríamos de usar uma chamada como select():

f = open("arquivo.txt")
while True:
	ler, gravar, erro = select.select([f], [], [], 60)
	if ler:
		dados = f.read()
		break
	else:
		# Timeout estourou sem leitura disponível,
		# realizar a tarefa periódica
f.close()

Isto funciona, usar select() ou poll() é a praxe em aplicativos que lidam com conexões de rede em vez de arquivos, mas o fluxo do código deixou de ser trivial.

Alguns sistemas como o Symbian implementavam manipulação assíncrona de arquivos, e os desenvolvedores odiavam, mas tudo no Symbian era odioso. O Node.js tambem oferece uma API assíncrona:

fs.readFile('arquivo.txt', 'utf8', function (err, dados) {
	if (err) {
		return console.log(err);
	}
	// usa os dados...
});

setTimeout(function () {
	# Executa a tarefa periódica
}, 60000);

Sem dúvida a leitura de um arquivo fica mais complicada usando a API assíncrona. Por outro lado, executar uma tarefa periódica em paralelo à tarefa de leitura ficou mais fácil. Em algum lugar nas entranhas do Node.js, um select() é utilizado de modo a garantir que todas as tarefas "agendadas" tenham chance de execução.

A sintaxe Javascript pelo menos permite especificar o callback em linha, o que permite ler o código "de cima para baixo". Mas isto não muda a dura realidade: o callback só é executado quando ou se o arquivo for aberto e lido. Se o arquivo não puder ser lido, o programa fica em suspenso para sempre, e será difícil saber porquê. Programar com APIs assíncronas exige uma mentalidade defensiva e muitas mensagens de log para depurar eventuais problemas.

O "problema" da assincronia é quando uma tarefa depende de várias anteriores. Por exemplo, ler cinco arquivos (de preferência ao mesmo tempo, não em cadeia, senão fica muito fácil!) e então executar uma tarefa que dependa de todos eles.

Em desenvolvimento de interfaces gráficas, serviços de rede ou sistemas Web, problemas como este aparecem o tempo todo, e simplesmente não se pode lidar com eles de forma síncrona. Um pacote de rede pode chegar, ou não. Um usuário pode clicar o botão da tela, ou não. Os desenvolvedores MS-DOS sofreram bastante na transição para Windows e estes sofreram na transição para Web, mas todos acabaram se acostumando.

Da mesma forma, eu acho que a API assíncrona do Node.js é uma questão de costume, e de organizar o código para que não vire um espaguete. Como as APIs de base do Node são assíncronas, todas as demais (e.g. comunicação com banco de dados) também são assíncronas, tanto por necessidade (já que dependem da base) quanto para seguir o padrão.

Por último, se você insiste, é possível ler o conteúdo de um arquivo de forma síncrona — bastante conveniente para arquivos pequenos, como configurações ou certificados:

dados = fs.readFileSync(nome);

Threads, só que não

O Node.js não suporta threads. É uma decisão de design acoplada com o uso de assincronia. Como o código "nunca bloqueia", o arquiteto do Node permitiu-se abolir as threads, no nível do Javascript.

Internamente, é possível e provável que módulos nativos (escritos em C++) façam uso de threads para executar alguma tarefa demorada, mas o callback com o resultado sempre é entregue na mesma thread para o código Javascript.

Isto é bom ou ruim?

Primeiro as más notícias. Um código Javascript pode bloquear sim, basta aparecer uma tarefa computacionalmente intensiva, do tipo processamento de imagem. Enquando uma parte do código estiver executando esta tarefa, nenhum outro evento assíncrono pode ser tratado.

O Node.js permite delegar uma tarefa do jeito tradicional do Unix: criando um subprocesso. Isto custa bastante ao sistema, mas se a tarefa for realmente pesada, o custo é diluído.

O Node.js definitivamente não é a melhor escolha para aplicativos do tipo codificador de vídeo, que além de exigir muita CPU costuma dividir o trabalho entre várias threads, uma por CPU, e precisa coordenar o trabalho das threads o tempo todo via compartilhamento de memória.

Mas quantos de nós escrevem codificadores de vídeo de alta performance? A quase totalidade dos aplicativos que eu escrevi na vida está sempre esperando por alguma coisa: um pacote de rede, a ação de um usuário ou a resposta de um banco de dados. Este tipo de aplicativo dificilmente precisa de threads, e na verdade a ausência de threads melhora muito a estabilidade.

Usar threads sempre cria riscos, mesmo em linguagens com suporte nativo, como Java. O fato é que a maioria dos desenvolvedores não sabe usar threads de forma segura. E o que é pior: tende a usar threads para qualquer besteira, como panacéia, mesmo quando há APIs melhores que evitariam o uso de threads. (Já perdi a conta a quantos desenvolvedores apresentei a classe Handler do Android.)

Em muitas linguagens interpretadas, threads são utilizadas como Band-Aid, justamente para compensar a falta de uma API assíncrona. Isto é muito comum em Java: ler um pacote de uma conexão de rede bloqueia a execução até que o pacote chegue, simplesmente não existe uma outra forma de fazer isto, então a "solução" é criar uma thread por conexão.

Em Python, o uso de threads não é eficiente devido ao famoso e temido "GIL" — Global Interpreter Lock. (Toda linguagem interpretada precisa de dispositivo semelhante se suporta threads.) O caso de uso mais importante para threads é a interação com alguma API bloqueante, por exemplo um módulo que bloqueie indefinidamente a execução, então usa-se o mesmo "Band-Aid" do Java: deixar uma thread conversando com aquele módulo.

Sob este ponto de vista, o Node.js foi corajoso e correto em abolir threads. Eu pessoalmente preferiria que elas estivessem lá mas seu uso fosse desencorajado ao extremo, como é o caso em Python. Há trabalhos, como o JXNode, que hoje é um fork do Node, que adicionam threads encapsuladas na forma de "tasks". Quem sabe um dia eles sejam integrados ao Node.

Módulos de alta performance computacional para Python, como o NumPy, executam tarefas em suas próprias threads, criadas por código nativo, bem a salvo do interpretador Python e do GIL. Da mesma forma, nada impede você de implementar um módulo Node que faça uso de threads no nível do código nativo.

Resta uma base não coberta: a execução de tarefas independentes em paralelo, um problema comum em servidores Web. Um humilde Apache com PHP roda "n" instâncias ao mesmo tempo e consegue aproveitar todas as CPUs do sistema, sem que o desenvolvedor precise mover uma palha. Isto o Node.js não faz automaticamente, mas existem opções que imitam a estratégia do mpm_prefork do Apache.

Socket.io

O Socket.io implementa a troca de mensagens em tempo real entre o servidor Web e o cliente rodando no browser. Sua API simples e elegante torna extremamente fácil o desenvolvimento de aplicações Web interativas, como chats e aplicativos colaborativos.

Apesar do nome, ele não é uma API de sockets TCP/IP genérica. A comunicação ocorre exclusivamente com o servidor de origem, usando WebSockets ou outras tecnologias (o Socket.io faz fallback automático de acordo com o que o browser suporta).

Este módulo toma conta de vários detalhes chatos: reconectar em caso de queda, reenviar mensagens perdidas, empacotar e desempacotar mensagens (de modo que o outro lado receba apenas mensagens inteiras e íntegras), "broadcast" de mensagens do servidor para uma lista de clientes, etc. Imagine quanto tempo você levaria para implementar e testar tudo isso.

Se o seu aplicativo Web precisa comunicar-se com o servidor o tempo todo, o Socket.io é a resposta, ponto. É motivo suficiente para você usar Node.js no seu projeto, mesmo que em paralelo com outra tecnologia.

Existem implementações do lado servidor do Socket.io para Django, PHP e outros servidores Web, mas são apócrifas e podem não funcionar com as versões mais recentes do cliente Socket.io. Supondo você use um servidor Django, é mais fácil desenvolver o lado servidor do Socket.io com Node e usar outro método para o Node "conversar" localmente com o Django.

A tentação é abandonar requisições AJAX "tradicionais" em favor do Socket.io, porque é muito mais fácil de usar. Mas lembre-se que cada cliente Socket.io abre uma conexão contínua com o servidor. 10 mil usuários que estejam com sua página aberta no browser, ativos ou não, demandarão 10 mil conexões contínuas — uma carga respeitável para qualquer servidor.

Na minha página, o Socket.io só é ativado se o usuário efetivamente abre o chat, para economizar em conexões perenes. Os comentários faziam uso de Socket.io originalmente, mas migrei para AJAX para evitar o excessivo número de conexões abertas. Como Socket.io e AJAX são igualmente assíncronos, desenvolvi uma camada de abstração que torna a migração fácil.

O chat tem de fazer uso de Socket.io para receber as falas de outros usuários em tempo real. Para obter um efeito semelhante em AJAX eu teria de fazer uma requisição a cada "n" segundos.

Express

O Node.js vem com um módulo de serviço HTTP incluso, e ele funciona a contento para coisas simples. Porém o Express tem se firmado como o "padrão ouro" de servidor HTTP. Inúmeros outros módulos populares que têm alguma relação com HTTP (e.g. o próprio Socket.io) integram-se automaticamente com o Express.

O Express, com a ajuda dos seus módulos auxiliares instalados opcionalmente, implementa toda a gama de soluções necessárias para uma aplicação Web completa: controle de autenticação, sessões, rotas, templates, servir páginas estáticas, servir AJAX, JSON, etc.

MEAN

Assim como a sigla LAMP (Linux, Apache, MySQL e PHP, às vezes "P" significa "Python") foi a coqueluche dos anos 90, a modinha agora é MEAN: MongoDB, Express, Angular.js e Node.js.

Do Angular.js não posso falar muito, porque nunca fiz uso dele. Gente que entende muito mais que eu tem recomendado tentar antes o React. Para meus projetos relativamente pequenos tenho me contentado com o jQuery.

Utilizei o MongoDB para coisas pequenas, para não ficar completamente de fora da modinha. Assim como tudo no Node, a API MongoDB também é assíncrona, mais um fator de estranheza. Fora isso é realmente fácil de usar; a ausência de "schema" e a tradução direta dos documentos armazenados de/para JSON faz um bom casamento de impedância entre Node, Javascript e banco de dados.

Muita gente também tem me dito que há outros bancos de dados NoSQL melhores. Eu ainda preferiria SQL para aplicações "sérias"; e mesmo os bancos SQL estão incorporando recursos típicos do NoSQL, como query dentro de estruturas JSON.

e-mail icon