Pular para o conteúdo principal

Node.js Noções básicas

logo do node.js

Então você ouviu falar de Node.js e ainda não faz idéia de como se usa JavaScript no lado do servidor? Neste artigo vou apresentar as noções básicas sobre a plataforma e um tutorial básico para já começar a por a mão na massa e produzir scripts com Node.

Node.js é uma linguagem?

Pra começar a conversa, Node.js (ou apenas "node" para os íntimos) não é uma linguagem de programação, mas sim uma plataforma de código aberto (https://github.com/nodejs/node) capaz de rodar códigos escritos em linguagem JavaScript em uma máquina virtual V8, que é a engine de JavaScript do navegador Google Chrome.

Porém, isso não significa que você terá acesso a API de DOM, como teria ao rodar JavaScript pelo navegador. Por outro lado, com Node você terá acesso a bibliotecas e APIs específicas para se trabalhar a requisição de um cliente no lado do servidor, ou seja, bibliotecas para criação de servidores web, para conexão com banco de dados, para gerenciamento de filas, etc. assim como se tem em linguagens como PHP, feitas para se rodar exclusivamente no servidor.

Principais características de Node.js

Node.js é uma plataforma para rodar códigos em linguagem JavaScript de forma interpretada, portanto, não é uma linguagem compilada.

JavaScript é uma linguagem dinâmica, fracamente tipada, baseada em prototipagem (prototype) e multi paradigma. Ela é extremamente flexível, o que lhe permite realizar operações bastante incomuns, como construir métodos em um objeto em tempo de execução, ou modificar o comportamento de um método em tempo de execução. E a linguagem permite a criação de aplicações procedurais (baseadas em funções), orientadas a objetos (baseadas em classes, ainda que o suporte à orientação a objetos não seja tão completo), orientada a eventos ou até orientada a aspectos.

Node é capaz de prover seu próprio servidor HTTP e, portanto, consegue prover um único processo que gerencia as requisições HTTP e também realiza as operações esperadas pela aplicação, diferentemente de PHP, que depende de um servidor HTTP externo (como o Apache ou o Nginx). Por este motivo, o gerenciamento de memória deve ser feito de forma mais cuidadosa, já que o processo principal fica vivo em memória, diferente de um script PHP que é executado e depois morto a cada requisição. Por outro lado, esta característica (que também é comum em outras linguagens como Java, Python, Golang, Perl) permite que a aplicação seja bastante performática, uma vez que o código é interpretado apenas no momento do primeiro carregamento e, além disso, é possível manter dados em cache na memória RAM para reutilizá-lo entre diferentes requisições.

Uma característica fundamental da plataforma é que ela é single-thread. Isso significa que, diferente de linguagens/frameworks que abrem uma nova thread para tratar uma nova requisição HTTP, Node mantém um único processo single-thread e é capaz de realizar operações concorrentes através de sua engine, que aproveita os momentos de espera da execução (em funções assíncronas não bloqueantes) para realizar outras operações. Por exemplo, ao realizar uma consulta a um banco de dados, que pode gastar algum tempo, a engine aproveita o tempo "ocioso" para utilizar o processador para, por exemplo, responder por outras requisições. Embora seja single-thread, também é possível montar uma estrutura multi-thread com um processo pai e processos filhos, possibilitando a realização de operações paralelas (além das operações concorrentes que dependem dos pontos de espera não bloqueantes).

Por fim, a linguagem JavaScript também tem suporte a eventos. Ou seja, é possível implementar funções que, em determinados momentos, disparam notificações de eventos que podem ser ouvidos por outros pontos da aplicação de forma concorrente. Este suporte pode ser especialmente útil para elaborar algumas soluções reativas (reactive programming).

O "ecossistema" do Node.js

Embora o Node em si seja auto-suficiente para prover a criação de aplicações web, é comum que, na vida real, sejam utilizadas várias outras ferramentas que chamarei de "ecossistema" do Node.js, e que ajudam no processo de desenvolvimento.

A primeira ferramenta é o NVM, que permite o gerenciamento e uso de múltiplas versões da engine do Node.js no mesmo computador. Ou seja, com ele você consegue ter na mesma máquina o Node na versão 8.0,0, 10.0.0, 11.0.0, 11.6.0 e usá-los quando quiser.

Aqui estão alguns exemplos básicos de uso do NVM:

# Instalando a versão 11.0.0 do node
nvm install v11.0.0

# Mostrar as verões de node que estão instaladas
nvm list

# Mostrar qual a versão de node utilizada no momento
nvm current

# Executar um script em node com a versão corrente do node
node script.js

# Forçar a execução do script em node com uma versão específica do node
nvm run 6.10.3 script.js

Para mais detalhes sobre instalação e utilização do NVM, acesse a documentação oficial do NVM.


A segunda ferramenta é o NPM, que é um gerenciador de pacotes oficial do node, ou seja, ele gerencia as dependências (bibliotecas externas) que seu projeto precisa ter para funcionar sem precisar reinventar a roda.

Para começar a usar o NPM em um projeto, utiliza-se o comando init, que irá pedir as informações iniciais para criação do arquivo package.json, que contém dados sobre seu projeto (nome, versão, dependências, scripts, licença, etc). Depois, utiliza-se os comandos abaixo para realizar operações básicas:

# Baixar/instalar uma dependência no seu projeto e atualizar no package.json
npm install [DEPENDÊNCIA]

# Baixar/instalar uma dependência usada apenas em modo de desenvolvimento e atualizar no package.json
npm install --save-dev [DEPENDÊNCIA]

# Desinstalar uma dependência do projeto
npm remove [DEPENDẼNCIA]

# Para instalar todas dependências descritas no package.json
npm install

# Para executar o comando "start", descrito praviamente no package.json
npm run start

As dependências são instaladas na pasta node_modules, que normalmente não é versionada em projetos de código aberto. E ao instalar as dependências, é gerado um arquivo package-lock.json que guarda o estado atual do seu projeto em relação às dependências e serve de base para realizar apenas as operações necessárias.

Para buscar por dependências para usar no seu projeto, basta acessar o site oficial do npm. Além disso, você é livre para criar bibliotecas públicas e publicá-las no NPM, seguindo as normas de versionamento da plataforma.

Ao instalar uma versão do Node com o nvm, automaticamente é disponibilizado o comando npm. Para mais informações de uso, veja a ajuda com o comando npm help ou npm help [COMANDO].

Observação: também existe o Yarn, que é outro gerenciador de pacotes diferente do npm e que resolve alguns problemas do npm. Para conhecer melhor, sugiro a leitura do artigo npm vs Yarn: e agora, quem poderá nos defender?


A última ferramenta a se citar é o NPX, que é um utilitário para executar dependências do NPM instaladas localmente ou dependências remotas. Com ela é possível, por exemplo, executar o seguinte comando:

npx github:piuccio/cowsay "hello, Rubens"

Este comando irá baixar a última versão do projeto "cowsay" e suas dependências, executar o script e depois desalocar tudo que baixou.

Para mais informações, consulte o site oficial do NPX.

Criando um script Node

Para começar a brincar com Node.js, nada como criar um script "Hello, World". Em Node.js, ele pode ser feito assim:

process.stdout.write('Hello, World!\n');

Para executá-lo, basta salvá-lo como script.js e depois executar na linha de comando:

node script

A variável process é um objeto global do Node que contém métodos e atributos relacionados ao processo. Um dos atributos é o stdout, que é um objeto do tipo Stream que, por sua vez, possui o método write.

Agora vamos ver um exemplo que envolve o processamento concorrente de operações no Node:

const fs = require('fs');

fs.readdir('/', {}, function (err, files) {
  if (err) {
    process.stderr.write('Error to read root directory\n');
    return;
  }
  process.stdout.write('Root:\n' + files.join('\n') + '\n');
});

fs.readdir('/home', {}, function (err, files) {
  if (err) {
    process.stderr.write('Error to read home directory\n');
    return;
  }
  process.stdout.write('Home:\n' + files.join('\n') + '\n');
});

No início do script, importamos a biblioteca fs, que é nativa do Node (não há necessidade de baixá-la via NPM) e jogamos ela numa constante chamada fs (poderiamos usar um outro nome, se desejável, ou se houvessem duas dependências com mesmo nome sendo usadas no mesmo arquivo).

Em seguida, o método readdir do objeto fs foi chamado duas vezes: uma para listar o conteúdo do diretório / e outra para listar o conteúdo do diretório /home. Note que passamos no terceiro parâmetro uma função de callback que será executada assim que a operação de ler o conteúdo do diretório for concluída. Ou seja, chamamos duas funções assíncronas (não bloqueantes). Ao chamarmos este script, pode ser que seja listado primeiro o conteúdo do diretório /home, mesmo que ele esteja abaixo da listagem do diretório /. Isso é essencial de se entender no Node: quando uma função assíncrona é chamada com algum callback, o script continua executando concorrentemente as próximas operações enquanto a função assíncrona não termina seu serviço. Caso desejássemos ler primeiro o diretório / e só depois o diretório /home, poderíamos fazer assim:

const fs = require('fs');

fs.readdir('/', {}, function (err, files) {
  if (err) {
    process.stderr.write('Error to read root directory\n');
    return;
  }
  process.stdout.write('Root:\n' + files.join('\n') + '\n');

  fs.readdir('/home', {}, function (errHome, filesHome) {
    if (errHome) {
      process.stderr.write('Error to read home directory\n');
      return;
    }
    process.stdout.write('Home:\n' + filesHome.join('\n') + '\n');
  });
});

Observe que esse código pode começar a virar uma bagunça caso existam muitas operações que precisem ser executadas em uma sequência específica ou que o valor gerado por uma função assíncrona seja parâmetro para outra função. Por isso, existe uma outra notação para lidar com essas funções assíncronas, que é com a declaração de funções async com instruções de await. Veja a implementação do mesmo exemplo anterior usando a notação de async/await:

const fs = require('fs');
const util = require('util');
const readDir = util.promisify(fs.readdir);

async function readDirs() { 
  try { 
    const filesRoot = await readDir('/');
    const filesHome = await readDir('/home');

    process.stdout.write('Root:\n' + filesRoot.join('\n') + '\n');
    process.stdout.write('Home:\n' + filesHome.join('\n') + '\n');
  } catch (e) { 
    process.stderr.write(`Error to read directory: ${e.message}`);
  }
}

readDirs();

Neste exemplo, criamos uma versão modificada da função fs.readdir e demos o nome de readDir, que trabalha retornando Promises ao invés de trabalhar com callback. Quando temos uma Promise na mão, podemos usar await para aguardar pelo resultado prometido e capturar possíveis erros com try/catch.

Com mais um pequeno ajuste, conseguimos melhorar a performance do script fazendo com que as consultas aos conteúdos dos dois diretórios sejam iniciadas de forma concorrente, mas esperaremos pelos dois resultados antes de mostrá-los:

    const [filesRoot, filesHome] = await Promise.all(
      [readDir('/'), readDir('/home')]
    );

Para finalizar, vamos ver um exemplo de como criar um pacote para ser utilizado em diferentes pontos da sua aplicação. Primeiro, vamos criar um arquivo readdirs.js:

const fs = require('fs');
const util = require('util');
const readDir = util.promisify(fs.readdir);

async function readDirs(dirs) {
  try { 
    const results = await Promise.all(
      dirs.map(dir => readDir(dir))
    );

    results.forEach(result => {
      process.stdout.write(result.join('\n') + '\n\n');
    })
  } catch (e) { 
    process.stderr.write(`Error to read directory: ${e.message}`);
  }
}

module.exports = readDirs;

Neste exemplo, vale destacar que foi feita uma função assíncrona e genérica (recebe um array de diretórios por parâmetro) e, no final, faz um module.expors = readDirs, que significa que este script é um pacote node que irá prover uma função. Para usar esta função em outro script, fazemos um require passando o caminho relativo do readdirs.js:

const rd = require('./readdirs');
rd(['/', '/home']);

Neste script, importamos a função provida pelo pacote readdirs, mas colocamos a função numa constante chamada rd e chamamos ela passando dois diretórios. Note que referenciamos o nome da função readDir com outro nome.

Os pacotes no Node podem exportar: uma única função, um objeto concreto contendo atributos/métodos ou uma classe. Veja exemplos:

// Importando uma função e usando ela
const readdir = require('./readdir');
readdir('/');

// Importando um objeto e usando métodos dele
const logger = require('./services/logger');
logger.debug('mensagem de debug');
logger.error('mensagem de erro');

// Importando uma classe e instanciando um objeto dela
const UserModel = require('./models/userModel');
const user = new UserModel();
user.setName('Rubens');

Vale ressaltar que o comando require do Node possui um cache e, portanto, ao requerer um pacote que já foi requerido anteriormente por outro script ou pacote, é devolvida a mesma instância. Portanto, se algum pacote exporta um objeto, ele será, a princípio, um singleton em toda aplicação. Isso significa que se um script requisita essa instância e modifica seu estado (atributos), outros scripts/pacotes que importaram o pacote terão o objeto modificado também. Por outro lado, se um pacote exporta uma classe e o seu script importa essa classe do pacote e instancia um objeto da classe, este objeto fica restrito ao escopo do script, não afetando instâncias criadas por outros scripts que também usaram a classe.

Por isso, é preciso planejar bem o que se espera exportar em um pacote: se será uma única função, um conjunto de funções, um objeto com atributos/métodos ou uma classe para ser instanciada manualmente.

Conclusão

Node é uma linguagem bastante versátil e que tem muitos detalhes para se explorar. Nos próximos artigos, pretendo explicar melhor sobre a parte de Promises, estruturação de aplicações, principais bibliotecas, criação de aplicações web, etc.

Comentários

Postagens mais visitadas deste blog

Mais um blog sobre tecnologia

Olá, novamente. Seja bem vindo(a) ao meu novo blog. Meu nome é Rubens e resolvi abrir um novo blog sobre tecnologia. Para quem não conhece, meu antigo blog é o PHP, Web e coisas assim . Como o nome sugere, o antigo blog falava principalmente sobre PHP e tecnologias web. Resolvi criar este novo blog com a intenção de abrangir outras linguagens como JavaScript (node.js), Python e Go (golang), além de temas relacionados à arquitetura, infraestrutura, containers, cloud, etc. Mas neste primeiro post, vou apenas me apresentar e contar um pouco sobre a minha trajetória profissional.