O guia completo do package.json do Node.js

NPM
NPM

O package.json é um elemento-chave em muitas aplicações do ecossistema Node.js. Se você trabalha com JavaScript, ou se você já interagiu com um projeto JavaScript antes, Node.js ou front-end, você certamente já se deparou com um arquivo package.json.

Para quê ele serve?

O que você deveria saber sobre ele e que coisas legais pode fazer com ele?

O package.json é uma espécie de manifesto do seu projeto. Ele pode fazer várias coisas, completamente não relacionadas. Ele é um repositório central de configurações de ferramentas, por exemplo. Ele também é onde npm armazena os nomes e versões dos pacotes instalados.

Neste artigo você vai ver:

Vamos lá!

A estrutura do arquivo

Aqui temos um exemplo de arquivo package.json válido:

Está vazio Não existem exigências fixas do que deve estar em um arquivo package.json para uma aplicação. O único requisito é que ele respeite o formato JSON, de outra maneira ele não poderá ser lido pelos programas que tentarem acessar suas propriedades programaticamente. Se você está construindo um pacote Node.js que você quer distribuir através do npm, as coisas mudam radicalmente e você deve ter um conjunto de propriedades que ajudem outras pessoas a usá-lo. Veremos mais sobre isso mais tarde. Este é outro package.json válido:

Ele define uma propriedade name, que diz o nome da aplicação ou pacote, que está contido na mesma pasta onde o arquivo vive. Aqui temos um exemplo muito mais complexo, o qual eu extraí de uma aplicação Vue.js de exemplo:

Existem muitas coisas acontecendo aqui:

  • name define o nome da aplicação ou pacote;
  • version indica a versão atual;
  • description é um resumo da sua aplicação/pacote;
  • main define o ponto de entrada da aplicação;
  • private (true) previne a sua aplicação de ser publicada acidentalmente no npm;
  • scripts define um conjunto de scripts Node para você executar;
  • dependencies define uma lista de pacotes npm instalados como dependências;
  • devDependencies define uma lista de pacotes npm instalados como dependências de desenvolvimento;
  • engines define quais versões de Node este pacote/aplicação funciona;
  • browserslist é usado para dizer quais browsers (e versões) você quer suportar;

Todas estas propriedades são usadas tanto pelo npm quanto por outras ferramentas que podemos usar.

Detalhando as propriedades

Esta seção descreve as propriedades que você pode usar em detalhes. Eu refiro várias vezes a pacote, mas a mesma coisa se aplica a aplicações locais que você não use como pacote. A maioria destas propriedades são usadas somente no site do npm. Outras são usadas por scripts que interagem com seu código, como npm ou outros. name Define o nome do pacote. Ex:

O nome deve ser menor que 214 caracteres, não pode ter espaços e somente pode conter letras minúsculas, hífens (-) ou underscore (_).

Isto tudo porque quando um pacote é publicado no npm, ele ganha sua URL baseado nesta propriedade. Se você publicou este pacote publicamente no GitHub, um bom valor para esta propriedade é o nome do repositório. author Exibe o nome do autor do pacote. Ex:

Também pode ser usado neste formato:

contributors

Assim como o autor, o projeto pode ter um ou mais contribuidores. Esta propriedade é uma array que os lista:

Pode também ser ussado neste formato:

bugs

Links para o issue tracker do pacote, geralmente uma página de issues no GitHub. Ex:

homepage

Define a página inicial do pacote. Ex:

version

Indica a versão atual do pacote. Ex:

Esta propriedade segue a notação semântica de versionamento (semver), o que significa que a versão é sempre expressada com três números: x.x.x.

O primeiro número é a versão principal, o segundo é a versão secundária e o terceiro é a versão do patch.

Existe um significado nestes números: uma release que somente corrija bugs é uma release patch, uma release que introduza mudanças na compatibilidade retroativa é uma release secundária e uma release principal é aquela que pode quebrar compatibilidade.

license Indica a licença do pacote. Ex:

keywords Esta propriedade contém um array ou palavras-chave associados com o que seu pacote faz. Ex:

Isto ajuda as pessoas a encontrar o seu pacote quando estiverem procurando pacotes similares ou quando navegam pelo site do npm.

description

Esta propriedade contém um resumo do pacote. Ex:

Isto é especialmente útil se você decidir publicar seu pacote para o npm, visando que outras pessoas possam descobrir o que este pacote faz.

repository Esta propriedade especifica onde o repositório do pacote está localizado. Ex:

Note o prefixo github. Existem outros serviços populares também:

Você pode explicitamente definir o controle de versão do sistema:

Você também pode usar diferentes sistemas de controle de versão:

main Define o ponto de entrada do pacote. Quando você importa este pacote em uma aplicação, é essa aplicação que a aplicação irá buscar pelo module.exports. Ex:

private Se definido como true, previne que a aplicação/pacote seja acidentalmente publicada no npm. Ex:

scripts Define um conjunto de scripts node que você pode executar. Ex:

Estes scripts são aplicações de linha de comando. Você pode rodá-los usando npm run XXXX, onde XXXX é o nome do comando. Ex:

Você pode usar qualquer nome que você quiser para um comando, e os scripts podem ser literalmente qualquer coisa que você quiser.

dependencies

Define uma lista de pacotes npm instalados como dependências. Quando você instalar um pacote usando npm:

o pacote será automaticamente inserido nesta lista. Exemplo:

devDependencies

Define uma lista de pacotes npm instalados como dependências de desenvolvimento. Eles diferem de dependencies porque eles serão instalados somente em máquinas de desenvolvimento, não necessárias para executar o código em produção.

Quando você instalar um pacote usando npm:

o pacote será automaticamente inserido nessa lista. Exemplo:

engines

Define quais versões de Node.js e outros comandos que este pacote ou aplicação suporta. Exemplo:

browserslist

É usado para dizer quais browsers (e suas versões) você quer suportar. Ele é referenciado pelo Babel, Autoprefixer, e outras ferramentas, para somente adicionar os polyfills e fallbacks necessários aos navegadores-alvo. Exemplo:

Esta configuração significa que você quer suportar ao menos as duas versões principais de todos os browsers com ao menos 1% de uso no mercado (as estatísticas são do CanIUse.com ), exceto IE8 e inferiores (saiba mais no browserslist do NPM).

Comandos específicos de propriedades

O arquivo package.json também pode hospedar configurações de comandos específicos, por exemplo Babel, ESLint, e muito mais.

Cada um possui uma propriedade específica, como eslintConfigbabel e outros. Estes são comandos específicos e você pode encontrar como usá-los na documentação do respectivo comando ou projeto.

Versões de Pacote

Eu já falei sobre isso extensivamente neste artigo aqui.

O arquivo package-lock.json

Na versão 5 o NPM introduziu o arquivo package-lock.json, que é automaticamente gerado quando você instala pacotes Node.

O que ele faz? Você provavelmente sabe tudo sobre o arquivo package.json a essa altura, o que é bem mais comum e está aí há bem mais tempo.

O objetivo deste arquivo é manter registro das versões exatas de cada pacote que é instalado para que um produto possa ser 100% reproduzível da mesma maneira mesmo se os pacotes forem atualizados por seus mantenedores.

Isto soluciona um problema bem específico que o package.json não resolvia. No package.json você pode definir quais versões você quer atualizar, usando a notação semver, por exemplo:

  • se você escrever ~0.13.0, você quer somente atualizar patch releases: 0.13.1 está ok, mas 0.14.0 não.
  • se você escrever ^0.13.0, você quer atualizar patches e minor releases: 0.13.10.14.0 e assim por diante.
  • se você escrevere 0.13.0, você somente usará esta versão exata do pacote.

Você não commita no Git a sua pasta node_modules, uma vez que ela é geralmente muito grande, e quando você tenta replicar o projeto em outra máquina usando o comando npm install, se você especificar a sintaxe ~ e uma patch release de um pacote tiver sido lançada, ela será instalada. O mesmo para  ^ e minor releases.

Se você especificar as versões exatas, como 0.13.0 neste exemplo, você não sofre desse problema.

Além poder ser um problema para você, pode ser um problema para outra pessoa qualquer querendo rodar o seu projeto com npm install, podendo acontecer do seu projeto original ficar diferente da nova instalação. Isto implica obviamente em possíveis bugs, mesmo que seja um patch ou minor release.

O arquivo package-lock.json define as versões instaladas de cada pacote de maneira irreversível e o npm usará exatamente estas versões quando você rodar npm install.

Este conceito não é novo e outros gerenciadores de pacotes de linguagens de programação (como o Composer do PHP) usam um sistema semelhante por anos.

O arquivo package-lock.json precisa ser commitado no Git para que possa ser baixado por outras pessoas. E se precisar atualizar as versões das dependências no package-lock.json, basta rodar npm update.

Espero que este artigo tenha sido útil para você que está aprendendo Node.js Para conteúdo mais aprofundado, recomendo meus livros. Para videoaulas, recomendo o meu curso online.

Curso Node.js e MongoDB
Curso Node.js e MongoDB

Tutorial CRUD em Node.js com driver nativo do MongoDB – Parte 3

O tutorial de hoje é uma continuação de uma série bastante acessada aqui no blog, onde ensino como fazer um sistema de cadastro bem simples em Node.js, usando o web framework ExpressJS e o banco de dados não-relacional MongoDB. Ou seja, o famoso CRUD.

Nesta terceira e última parte, introduzo um conceito importante de construção de interfaces que é a modularização da página através do recurso de partial views do EJS (view-engine que estamos usando com ExpressJS) e ensino como dar um “tapa” no visual usando o framework front-end Bootstrap, muito utilizado mundialmente para estruturar aplicações web responsivas e elegantes.

Para conseguir acompanhar todos os códigos, é importante que você tenha realizado a parte anterior ou pelo menos baixe os códigos-fonte que se encontram no formulário ao final do tutorial anterior (parte 2). Caso prefira assistir um vídeo, a primeira parte dessa séria pode ser resumida com este vídeo abaixo que é uma aula gratuita do meu curso de Node.js e MongoDB.

Aproveite!

O conteúdo do post é:

Vamos lá!

Atenção, uma versão ainda mais completa deste tutorial está disponível em videoaula em meu curso de Node.js e MongoDB.

O que são Partial Views?

É muito comum quando estamos construindo aplicações web que certas porções de nossas views (telas) acabem se repetindo. Por causa disso, todas as linguagens de programação para web possuem recursos de partial views, ou seja, de modularização da interface a partir de “partes” da tela que são construídas separadas e depois são montadas em conjunto para que o usuário veja apenas o resultado final, completo.

Fazendo uma analogia com o que fazemos no código backend do Node.js, uma partial view é como se fosse um módulo NPM, que depois usamos o require para carregá-lo em nosso código. Em outras plataformas isso é chamado de import, include, using, etc.

Nossa aplicação é bem simples, possui apenas três views: index, new e error, conforme você vê na imagem abaixo.

Estrutura do projeto
Estrutura de Views

Essas views por sua vez também são mega simples, logo, não há uma reeeaaal necessidade de usar partial views aqui, usaremos mais a título de conhecimento. No entanto, esse conceito é muito poderoso e além de tornar seu código de front-end mais elegante e fácil de ler, ele torna a programação de novas páginas muito mais produtiva, pois sempre aproveitaremos elementos genéricos da interface que serão criados apenas uma vez.

Se olharmos agora para as views index e new, você deve notar que existem muitas tags em comum no início e no fim delas. É por aí que vamos começar a melhorar.

Estrutura que se repete
Estrutura que se repete

Partial views com EJS

Não é de hoje que eu uso EJS (Embedded JavaScript) em meus tutoriais e neste não será diferente. Apesar de não ser a view-engine (motor de visualização) padrão do ExpressJS (esse título é do PUG/Jade), ele é meu favorito pois a curva de aprendizado é ridiculamente baixa, considerando que usa HTML + JS, coisa que todo dev web está careca de saber.

Como não poderia deixar de ser, o EJS possui a funcionalidade de criar partial views (lembro dos meus tempos de ASP.NET em que chamávamos isso de MasterPages…). Isso é bem fácil de fazer, comece criando um arquivo top.ejs (top=topo) na sua pasta views e nele vamos colocar toda a parte comum que teremos no topo de todas páginas do nosso sistema, que é apenas o HTML abaixo por enquanto:

Note que nesta top.ejs estamos usando algumas variáveis JS que devem vir do backend, através do model passado na função render do Node.js, lembra?

Agora vamos criar mais um arquivo novo, o bottom.ejs (bottom=rodapé), também na pasta views e nele vamos colocar toda a parte comum que teremos no rodapé de todas páginas do nosso sistema, que é apenas o HTML abaixo por enquanto:

E agora, como fazemos para as demais views do projeto usarem essas partial views para compor a sua estrutura?

Bem simples, vamos começar pela index.ejs, remova a parte do topo do arquivo que é repetida ao arquivo top.ejs e no lugar desse pedaço, deixe como abaixo, usando o comando ‘include’ para “incluir” a partial view na primeira linha do arquivo:

Note que já incluí também o include para o bottom.ejs no final do arquivo. Na hora que o arquivo HTML vai ser construído para ser enviado ao browser para renderização, o EJS vai ler essa linha e entender que parte desse HTML está em outro arquivo. Ele vai ir nesse outro arquivo, copiar o conteúdo de lá e colar bem nesse local.

Assim, com esses dois includes, nós mantemos nossa aplicação modularizada em três pedações: topo, centro e rodapé, facilitando manutenção e aumentando o reuso de código.

Sim, reuso de código, pois agora nas páginas new.ejs e error.ejs nós podemos fazer os mesmos includes para não ter de repetir aqueles blocos iniciais e finais de HTML. Os ganhos agora são pequenos, mas no futuro, são enormes pois facilitam muito a evolução e manutenção do sistema web.

Eu não vou colocar aqui o código dos outros dois arquivos ejs com os includes, embora você possa baixar o projeto completo no final desse artigo deixando seu e-mail.

Depois de fazer estas alterações, coloque sua aplicação Node para rodar (não esqueça de subir o banco MongoDB também) e você vai ver que nada mudou para o usuário, que a tela continua renderizando do mesmo jeito e que as alterações só servem para ajudar o programador a trabalhar melhor mesmo.

Um dos grandes ganhos desta abordagem vem agora: manutenção. Vamos adicionar o framework front-end Bootstrap nessa aplicação pra dar um “tapa” no visual dela e vamos fazer isso de maneira muito fácil porque já deixamos a aplicação preparada para este tipo de alteração.

Adicionando o Bootstrap

Eu não vou me prolongar muito na teoria do Bootstrap por aqui pois falo bastante disso no meu livro Programação Web com Node.js. Inclusive aquele “B” na capa é o símbolo do Bootstrap (os outros símbolos são o JQuery, o HTML5 e o MongoDB).

Livro Node
Livro Node

Basicamente o Bootstrap é um framework front-end muito popular no mundo inteiro, fornecendo uma estrutura e comportamentos padronizados que muitos devs web sabem como mexer, acelerando o desenvolvimento e manutenção de qualquer sistema que use Bootstrap como padrão para front-end.

É como se os sites tivessem a mesma estrutura básica, entende?

Apesar da estrutura básica ser a mesma, a aparência pode variar enormemente através da customização de arquivos de estilo (CSS).

A aplicação do Bootstrap em um projeto web se dá através de seus arquivos de CSS e de seus arquivos de JavaScript.

Vamos começar pelo arquivo CSS, que é obtido no site oficial e deve ser adicionado no topo do nosso arquivo top.ejs, logo antes da chamada original de CSS que foi colocada pelo express-generator:

Como adicionamos esta linha no top.ejs, TODAS as páginas que incluem essa partial-view vão estar agora com o CSS do Bootstrap. Isso é produtividade na programação!

Note que também adicionei duas meta-tags, conforme sugerido na página do Bootstrap, para garantir máxima compatibilidade com os recursos do framework, como a responsividade em dispositivos móveis.

Agora, vamos adicionar os scripts JS necessários pro Bootstrap funcionar corretamente no arquivo bottom.ejs. Eles devem ser adicionados logo antes da tag de fecha-body:

Só o fato de termos feito essas adições já vão gerar alguns efeitos visuais na nossa aplicação, como pode ver na imagem abaixo:

Mas para despertar todo o “poder embelezador” do Bootstrap em nossa aplicação nós temos de fazer alguns ajustes na nossa estrutura, bem como usar algumas classes CSS definidas pelo Bootstrap.

Assim, vamos começar definindo uma “div class container” que vai englobar todo o conteúdo do body das páginas. Para fazer isso, basta adicionar o conjunto de tags div abaixo, com a abre-div no top.ejs e a fecha-div no bottom.ejs, de modo que ambas fiquem dentro do body.

Note que só mudou uma linha, logo acima do H1, bem no final do top.ejs. Já no bottom.ejs, também só vai mudar uma linha, que fica bem no início do arquivo:

Após essa adição, teremos mais um ajuste perceptível nas páginas já existentes, que ficarão com uma margem e mais centralizadas. A div container faz a sua página funcionar em um grid-system de 12 colunas virtuais, ou seja, o Bootstrap permite que você organize o conteúdo das suas páginas em 12 blocos de tamanhos iguais, como em um Lego. Desde que você siga esta regra cuidadosamente, a sua aplicação web será responsiva, alterando a sua largura conforme a tela do aparelho que estiver acessando a mesma.

E isso é só o início!

Customizando a aparência

Eu poderia ficar aqui durante muitas e muitas horas escrevendo sobre Bootstrap, então vou ser breve. Vamos apenas dar uma embelezada na página para te mostrar o poder do framework e depois vamos encerrar o tutorial. Fica a seu critério depois se aprofundar, seja em meu livro ou na documentação oficial.

Uma vez adicionado a div container, que é o principal bloco da sua página, vamos personalizar os botões, ou os elementos que queremos que se pareçam com botões. Para isso, basta usarmos duas classes em cada elemento “btn btn-primary” para os botões principais e apenas “btn” para os secundários.

Cada âncora ou submit/button deve ficar parecida com o HTML abaixo:

O botão primário é azul no Bootstrap, mas isso obviamente você pode alterar sobrescrevendo ou editando o CSS original do Bootstrap, ou ainda através de templates que você pega na Internet. As classes CSS de botão também já adicionam um efeito de sombra ao passar o mouse por cima deles, o chamado “hover”.

Para “fechar” a tela inicial, vamos customizar a tabela além do botão, para ela ficar bem bacanuda. Para isso, além de usar o padrão HTML5 para tabelas (com thead, tbody, etc), vamos adicionar uma série de classes a saber:

  • table: classe padrão Bootstrap para tabelas;
  • table-bordered: com linhas nas bordas;
  • table-striped: com linhas “zebradas”;
  • table-hover: com sombra quando passa o mouse;
  • thead-dark (no cabeçalho): pro cabeçalho ficar escuro;

O código final que lista os clientes cadastrados pode ser visto abaixo, e substitui a lista antiga:

Fechando de vez esta tela, atualize o código de paginação para ele ficar estilizado conforme recomendado pelo Bootstrap:

Com estas alterações, nossa tela inicial ficará muito melhor do que anteriormente, como mostra a imagem abaixo (coloquei btn-danger ao invés de btn-primary nos botões de exclusão):

Lista de Clientes com Bootstrap

Agora a segunda tela, de cadastro de cliente (new.ejs). Além de ajustarmos os botões com as classes que mencionei antes, vamos ajustar o form HTML conforme instruções do próprio Bootstrap:

O resultado, você confere na imagem abaixo (os tamanhos são ajustáveis, mas por padrão ele é responsivo):

Cadastro com Bootstrap

E com isso encerramos o tutorial de hoje e acredito que esta série sobre o CRUD com driver nativo do MongoDB. Qualquer dúvida que você tiver, deixe aí nos comentários que terei o maior prazer em responder.

Interessado em mais conteúdos sobre Node.js e MongoDB, clique no banner abaixo e conheça o meu curso em videoaulas sobre o assunto!

Curso Node.js e MongoDB
Curso Node.js e MongoDB

Entendendo o Node.js Event Loop

Continuando os estudos de Node.js me deparei com um elemento chave que não temos como ignorar quando o assunto é essa tecnologia. Estou falando do Event Loop.

Grande parte das características e principalmente das vantagens do Node.js se devem ao funcionamento do seu loop single-thread principal e como ele se relaciona com as demais partes do Node, como a biblioteca C++ libuv.

Assim, a ideia deste artigo é ajudar você a entender como o Event Loop do Node.js funciona, o que deve lhe ajudar a entender como tirar o máximo de proveito dele.

Vamos ver neste artigo:

Você pode acompanhar este conteúdo através do vídeo abaixo, que é parte do meu curso de Node.js e MongoDB:

O problema

Antes de entrar no Event Loop em si, vamos primeiro entender porque o Node.js possui um e qual o problema que ele propõe resolver.

A maioria dos backends por trás dos websites mais famosos não fazem computações complicadas. Nossos programas passam a maior parte do tempo lendo ou escrevendo no disco, ou melhor, esperando a sua vez de ler e escrever, uma vez que é um recurso lento e concorrido. Quando não estamos nesse processo de ir ao disco, estamos enviando ou recebendo bytes da rede, que é outro processo igualmente demorado. Ambos processos podemos resumir como operações de I/O (Input & Output) ou E/S (Entrada & Saída).

Processar dados, ou seja executar algoritmos, é estupidamente mais rápido do que qualquer operação de IO que você possa querer fazer. Mesmo se tivermos um SSD em nossa máquina com velocidades de leitura de 200-730 MB/s fará com que a leitura de 1KB de dados leve 1.4 micro-segundos. Parece rápido? Saiba que nesse tempo uma CPU de 2GHz consegue executar 28.000 instruções.

Isso mesmo. Ler um arquivo de 1KB demora tanto tempo quanto executar 28.000 instruções no processador. É muito lento.

Quando falamos de IO de rede é ainda pior. Faça um teste, abra o CMD e execute um ping no site do google.com, um dos mais rápidos do planeta:

A latência média nesse teste é de 44 milisegundos. Ou seja, enviar um ping para o Google demora o mesmo tempo que uma CPU necessita para executar 88 milhões de operações.

Ou seja, quando estamos fazendo uma chamada a um recurso na Internet, poderíamos estar fazendo cerca de 88 milhões de coisas diferentes na CPU.

É muita diferença!

A solução

A maioria dos sistemas operacionais lhe fornece mecanismos de programação assíncrona, o que permite que você mande executar tarefas concorrentes que não ficam esperando uma pela outra, desde que uma não precise do resultado da outra, é claro.

Esse tipo de comportamento pode ser alcançado de diversas maneiras. Atualmente a forma mais comum de fazer isso é através do uso de threads o que geralmente torna nosso código muito mais complexo. Por exemplo, ler um arquivo em Java é uma operação bloqueante, ou seja, seu programa não pode fazer mais exceto esperar a comunicação com a rede ou disco terminar. O que você pode fazer é iniciar uma thread diferente para fazer essa leitura e mandar ela avisar sua thread principal quando a leitura terminar.

Novas formas e programação assíncrona tem surgido com o uso de interfaces async como em Java e C#, mas isso ainda está evoluindo. Por ora isso é entediante, complicado, mas funciona. Mas e o Node? A característica de single-thread dele obviamente deveria representar um problema uma vez que ele só consegue executar uma tarefa de um usuário por vez, certo? Quase.

O Node usa um princípio semelhante ao da função setTimeout(func, x) do Javascript, onde a função passada como primeiro parâmetro é delegada para outra thread executar após x milisegundos, liberando a thread principal para continuar seu fluxo de execução. Mesmo que você defina x como 0, o que pode parecer algo inútil, isso é extremamente útil pois força a função a ser realizada em outra thread imediatamente.

Vamos falar melhor dessa solução na sequência.

Node.js Event Loop

Sempre que você chama uma função síncrona (i.e. “normal”) ela vai para uma “call stack” ou pilha de chamadas de funções com o seu endereço em memória, parâmetros e variáveis locais. Se a partir dessa função você chamar outra, esta nova função é empilhada em cima da anterior (não literalmente, mas a ideia é essa). Quando essa nova função termina, ela é removida da call stack e voltamos o fluxo da função anterior. Caso a nova função tenha retornado um valor, o mesmo é adicionado à função anterior na call stack.

Mas o que acontece quando chamamos algo como setTimeout, http.get, process.nextTick, ou fs.readFile? Estes não são recursos nativos do V8, mas estão disponíveis no Chrome WebApi e na C++ API no caso do Node.js.

Vamos dar uma olhada em uma aplicação Node.js comum – um servidor escutando em localhost:3000. Após receber a requisição, o servidor vai chamar wttr.in/ para obter informações do tempo e imprimir algumas mensagens no console e depois retorna a resposta HTTP.

O que será impresso quando uma requisição é enviada para localhost:3000?

Se você já mexeu um pouco com Node antes, não ficará surpreso com o resultado, pois mesmo que console.log(‘Obtendo a previsão do tempo, aguarde.’) tenha sido chamado depois de console.log(‘Previsão confirmada!’) no código, o resultado da requisição será como abaixo:

O que aconteceu? Mesmo o V8 sendo single-thread, a API C++ do Node não é. Isso significa que toda vez que o Node for solicitado para fazer uma operação bloqueante, Node irá chamar a libuv que executará concorrentemente com o Javascript em background. Uma vez que esta thread concorrente terminar ou jogar um erro, o callback fornecido será chamado com os parâmetros necessários.

A libuv é um biblioteca C++ open-source usada pelo Node em conjunto com o V8 para gerenciar o pool de threads que executa as operações concorrentes ao Event Loop single-thread do Node. Ela cuida da criação e destruição de threads, semáforos e outras “magias” que são necessárias para que as tarefas assíncronas funcionem corretamente. Essa biblioteca foi originalmente escrita para o Node, mas atualmente outros projetos a usam também.

Curso Node.js e MongoDB
Curso Node.js e MongoDB

Task/Event/Message Queue

Javascript é uma linguagem single-thread orientada a eventos. Isto significa que você pode anexar gatilhos ou listeners aos eventos e quando o respectivo evento acontece, o listener executa o callback que foi fornecido.

Toda vez que você chama setTimeout, http.get ou fs.readFile, Node.js envia estas operações para a libuv executá-las em uma thread separada do pool, permitindo que o V8 continue executando o código na thread principal. Quando a tarefa termina e a libuv avisa o Node disso, o Node dispara o callback da referida operação.

No entanto, considerando que só temos uma thread principal e uma call stack principal, onde que os callbacks ficam guardados para serem executados? Na Event/Task/Message Queue, ou o nome que você preferir. O nome ‘event loop’ se dá à esse ciclo de eventos que acontece infinitamente enquanto há callbacks e eventos a serem processados na aplicação.

Em nosso exemplo anterior, de previsão do tempo, nosso event loop ficou assim:

  1. Express registrou um handler para o evento ‘request’ que será chamado quando uma requisição chegar em ‘/’
  2. ele começar a escutar na porta 3000
  3. a stack está vazia, esperando pelo evento ‘request’ disparar
  4. quando a requisição chega, o evento dispara e o Express chama o handler configurado: sendWeatherOfRandomCity
  5. sendWeatherOfRandomCity é empilhado na call stack
  6. getWeatherOfRandomCity é chamado dentro da função anterior e é também empilhado na call stack
  7. Math.floor e Math.random são chamadas, empilhadas e logo desempilhadas, retornando uma cidade à variável city
  8. superagent.get é chamado com o parâmetro ‘wttr.in/${city}’ e definimos o handler/callback para o evento de término da requisição.
  9. a requisição HTTP para http://wttr.in/${city} é enviada para uma thread em background e a execução continua
  10. ‘Obtendo a previsão do tempo, aguarde.’é impresso no console e getWeatherOfRandomCity retorna
  11. sayHi é chamada, ‘Hi’ é impresso no console
  12. sendWeatherOfRandomCity retorna, é retirado da call stack, deixando-a vazia
  13. ficamos esperando pela chamada de http://wttr.in/${city} nos responder
  14. uma vez que a resposta chegue, o evento de ‘end’ é disparado
  15. o handler anônimo que passamos para .end() é chamado, é colocado na call stack com todos as variáveis locais, o que significa que ele pode ver e modificar os valores de express, superagent, app, CITIES, request, response, city e todas funções que definimos
  16. response.send() é chamado com um status code de 200 ou 500, mas isso também é executado em uma thread do pool para a stream de respostas não fique bloqueada e o handler anônimo é retirado da pilha.

E é assim que tudo funciona!

Vale salientar que por padrão o pool de threads da libuv inicia com 4 threads concorrentes e que isso pode ser configurado conforme a sua necessidade.

Microtasks e Macrotasks

Além disso, como se não fosse o bastante, nós temos duas task queues, não apenas uma. Uma task queue para microtasks e outra para macrotasks.

Exemplos de microtasks

  • process.nextTick
  • promises
  • Object.observe

Exemplos de macrotasks

  • setTimeout
  • setInterval
  • setImmediate
  • I/O

Para mostrar isso na prática, vamos dar uma olhada no seguinte código:

a saída no console é:

De acordo com a especificação da WHATWG, uma macrotask deve ser processada da macrotask queue em um ciclo do event loop. Depois que essa macrotask terminar, todas as microtasks existentes são processadas dentro do mesmo ciclo. Se durante este processamento de microtasks novas microtasks surgirem, elas são processadas também, até a microtask queue ficar vazia.

Este diagrama do site Rising Stack ilustra bem o event loop completo:

Em nosso caso:

Ciclo 1:

  1. setInterval é agendado como (macro)task
  2. setTimeout 1 é agendado como task
  3. em Promise.resolve 1 ambos thens são agendados como microtasks
  4. a call stack está vazia e as microtasks executam

Task queue: setInterval, setTimeout 1

Ciclo 2:

  1. a microtask queue está vazia, logo o handler setInterval pode executar;
  2. outro setInterval é agendado como task, logo atrás de setTimeout 1

Task queue: setTimeout 1, setInterval

Ciclo 3:

  1. a microtask queue está vazia, logo o handler setTimeout 1 pode executar;
  2. promise 3promise 4 são agendadas como microtasks;
  3. handlers de promise 3promise 4 são executados
  4. setTimeout 2 é agendado como task

Task queue: setInterval, setTimeout 2

Ciclo 4:

  1. a microtask queue está vazia; logo o handler de setInteval pode executar;
  2. outro setInterval é agendado como task, logo atrás de setTimeout

Task queue: setTimeout 2, setInterval

Ciclo 5:

  1. o handler setTimeout 2 executa;
  2. promise 5promise 6 são agendadas como microtasks;
  3. os handlers de promise 5 e promise 6 executam encerrando o programa.

Nota: isso funciona perfeitamente bem no Google Chrome, mas por questões que fogem da minha compreensão, não é regra em todos ambientes de execução. Existem modificações que podemos fazer no código para que o comportamento seja o mesmo em todos ambientes, mas deixam o código terrivelmente feio (i.e. callback hell).

Conclusões

Como você pôde ver, se quisermos ter total controle de nossas aplicações Node.js devemos prestar atenção nestes detalhes de como as tarefas são executadas dentro do event loop, principalmente para não bloquearmos sua execução sem querer.

O conceito do event loop pode ser um tanto complicado no início mas uma vez que você entender sue funcionamento na prática você não conseguirá mais imaginar sua vida sem ele. Obviamente o uso inicial intenso de callbacks é muito chato de gerenciar mas já é possível usar Promises em nossos códigos Javascript que permitem deixar as tarefas assíncronas mais inteligíveis e em breve devemos ter acesso ao recurso async-await com o ES7.

Uma última dica é que você também pode enviar seus processamentos longos (mas que não são operações de IO), que normalmente seriam executados na thread principal para as threads em background usando bibliotecas como async.js.

Recomendo agora colocar a mão na massa com esse tutorial de Node.js com MongoDB que preparei pra você!

Curtiu o post? Então clica no banner abaixo e dá uma conferida no meu livro sobre programação web com Node.js!