035.1 Lição 1
Certificação: |
Web Development Essentials |
---|---|
Versão: |
1.0 |
Tópico: |
035 Programação do servidor Node.js |
Objetivo: |
035.i Noções básicas de Node.js |
Lição: |
1 de 1 |
Introdução
O Node.js é um ambiente de tempo de execução que executa código JavaScript em servidores web — o chamado back-end (lado do servidor) — em vez de usar uma segunda linguagem, como Python ou Ruby, para os programas do lado do servidor. A linguagem JavaScript já é utilizada no front-end moderno dos aplicativos web, interagindo com o HTML e CSS da interface de usuário do navegador web. O uso do Node.js em conjunto com o JavaScript no navegador oferece a possibilidade de se empregar apenas uma linguagem de programação para todo o aplicativo.
A principal razão para a existência do Node.js é a maneira como ele lida com diversas conexões simultâneas no back-end. Uma das maneiras mais comuns de um servidor de aplicativos web manipular conexões é por meio da execução de vários processos. Quando abrimos um aplicativo instalado no computador, inicia-se um processo que consome muitos recursos. Imagine o que acontece quando milhares de usuários estão fazendo a mesma coisa em um grande aplicativo web.
O Node.js evita esse problema usando um design chamado laço de eventos (event loop), que é um loop interno que verifica continuamente as tarefas de entrada a serem computadas. Graças ao uso generalizado do JavaScript e à onipresença das tecnologias web, o Node.js foi largamente adotado em aplicativos pequenos e grandes. Existem outras características que também ajudaram no crescimento do Node.js, como o processamento de entrada/saída (I/O) assíncrono e sem bloqueio, explicado posteriormente nesta lição.
O ambiente do Node.js usa um mecanismo em JavaScript para interpretar e executar o código JavaScript no lado do servidor ou na área de trabalho. Nessas condições, o código JavaScript que o programador escreve é analisado e compilado dinamicamente (just-in-time) para executar as instruções de máquina geradas pelo código JavaScript original.
Note
|
Conforme você progride nestas lições sobre o Node.js, você vai notar que o JavaScript do Node.js não é exatamente o mesmo que roda no navegador (que segue a especificação ECMAScript), mas é bastante semelhante. |
Para começar
Esta seção e os exemplos a seguir presumem que o Node.js já está instalado em seu sistema operacional Linux e que você já domina habilidades básicas, como executar comandos no terminal.
Para executar os exemplos a seguir, crie um diretório de trabalho chamado node_examples
. Abra um prompt de terminal e digite node
. Se o Node.js estiver instalado corretamente, ele apresentará um prompt >
onde será possível testar os comandos JavaScript interativamente. Este tipo de ambiente é chamado REPL, que em inglês é a sigla para “ler, avaliar, imprimir e fazer loop”. Digite a seguinte entrada (ou alguma outra instrução JavaScript) nos prompts >
. Pressione a tecla Enter após cada linha e o ambiente REPL retornará os resultados de suas ações:
> let array = ['a', 'b', 'c', 'd']; undefined > array.map( (element, index) => (`Element: ${element} at index: ${index}`)); [ 'Element: a at index: 0', 'Element: b at index: 1', 'Element: c at index: 2', 'Element: d at index: 3' ] >
O trecho de código foi escrito usando a sintaxe ES6, que oferece uma função de mapa para iterar sobre a matriz e imprimir os resultados usando modelos de string. É possível escrever praticamente qualquer comando que seja válido. Para sair do terminal Node.js, digite .exit
, sem esquecer o ponto inicial.
Para scripts e módulos mais longos, é mais prático usar um editor de texto como o VS Code, Emacs ou Vim. Você pode salvar as duas linhas de código que acabamos de mostrar (com uma pequena modificação) em um arquivo chamado start.js
:
let array = ['a', 'b', 'c', 'd'];
array.map( (element, index) => ( console.log(`Element: ${element} at index: ${index}`)));
Em seguida, execute o script do shell de maneira a produzir os mesmos resultados de antes:
$ node ./start.js Element: a at index: 0 Element: b at index: 1 Element: c at index: 2 Element: d at index: 3
Antes de mergulhar em mais código, vamos dar uma visão geral do funcionamento do Node.js, usando seu ambiente de execução de thread único e o laço de eventos.
Laço de eventos e thread único
É difícil dizer quanto tempo seu programa Node.js levará para lidar com uma solicitação. Algumas solicitações podem ser curtas — por exemplo, percorrer variáveis na memória e retorná-las — ao passo que outras podem exigir atividades mais demoradas, como abrir um arquivo no sistema ou emitir uma consulta a um banco de dados e aguardar os resultados. Como o Node.js lida com essa incerteza? O laço de eventos é a resposta.
Imagine um chef de cozinha realizando várias tarefas. Assar um bolo é algo requer um certo tempo do forno. O chef não fica sentado esperando o bolo ficar pronto e só depois começa a coar um café. Em vez disso, enquanto o forno assa o bolo, o chef prepara o café e faz outras coisas em paralelo. Mas ele está sempre verificando se está na hora de mudar o foco para uma tarefa específica (como fazer café) ou de tirar o bolo do forno.
O laço de eventos é como o chef que está constantemente ciente do que se passa ao redor. No Node.js, um “verificador de eventos” está sempre verificando as operações que foram concluídas ou que estão esperando para serem processadas pelo mecanismo JavaScript.
Graças a esse método, uma operação assíncrona e longa não bloqueia as outras operações mais rápidas que vêm depois. Isso ocorre porque o mecanismo de laço de evento está sempre verificando se aquela tarefa longa, como uma operação de I/O, já foi realizada. Caso contrário, o Node.js pode continuar a processar outras tarefas. Quando a tarefa em segundo plano é concluída, os resultados são retornados e o aplicativo que está rodando sobre o Node.js pode usar uma função de gatilho (retorno de chamada) para processar a saída.
Como o Node.js evita o uso de múltiplos encadeamentos (threads), como fazem outros ambientes, ele é chamado de ambiente de thread único e, portanto, é importantíssimo que a execução ocorra sem bloqueios. É por isso que o Node.js usa um laço de eventos. Para as tarefas de computação intensiva, o Node.js não está entre as melhores ferramentas: existem outras linguagens de programação e ambientes que fazem isso de forma mais eficiente.
Nas seções a seguir, examinaremos mais de perto as funções de retorno de chamada. Por enquanto, entenda que as funções de retorno de chamada são gatilhos executados após a conclusão de uma operação predefinida.
Módulos
É sempre recomendável dividir as funcionalidades complexas e os trechos extensos de código em partes menores. Essa modularização ajuda a organizar melhor a base de código, abstrair as implementações e evitar problemas de engenharia complicados. Para atender a essas necessidades, os programadores empacotam blocos de código-fonte para serem consumidos por outras partes internas ou externas do código.
Considere o exemplo de um programa que calcula o volume de uma esfera. Abra seu editor de texto e crie um arquivo chamado volumeCalculator.js
contendo o seguinte código JavaScript:
const sphereVol = (radius) => {
return 4 / 3 * Math.PI * radius
}
console.log(`A sphere with radius 3 has a ${sphereVol(3)} volume.`);
console.log(`A sphere with radius 6 has a ${sphereVol(6)} volume.`);
A seguir, execute o arquivo usando o Node:
$ node volumeCalculator.js A sphere with radius 3 has a 113.09733552923254 volume. A sphere with radius 6 has a 904.7786842338603 volume.
Aqui, uma função simples foi usada para calcular o volume de uma esfera com base em seu raio. Imagine que também precisamos calcular o volume de um cilindro, um cone e assim por diante: percebemos rapidamente que essas funções específicas devem ser adicionadas ao arquivo volumeCalculator.js
, que pode acabar se tornando uma enorme coleção de funções. Para organizar melhor a estrutura, podemos usar a ideia por trás dos módulos como pacotes de código separado.
Para isso, crie um arquivo separado chamado polyhedrons.js
:
const coneVol = (radius, height) => {
return 1 / 3 * Math.PI * Math.pow(radius, 2) * height;
}
const cylinderVol = (radius, height) => {
return Math.PI * Math.pow(radius, 2) * height;
}
const sphereVol = (radius) => {
return 4 / 3 * Math.PI * Math.pow(radius, 3);
}
module.exports = {
coneVol,
cylinderVol,
sphereVol
}
A seguir, no arquivo volumeCalculator.js
, exclua o código antigo e substitua-o por este trecho:
const polyhedrons = require('./polyhedrons.js');
console.log(`A sphere with radius 3 has a ${polyhedrons.sphereVol(3)} volume.`);
console.log(`A cylinder with radius 3 and height 5 has a ${polyhedrons.cylinderVol(3, 5)} volume.`);
console.log(`A cone with radius 3 and height 5 has a ${polyhedrons.coneVol(3, 5)} volume.`);
Depois execute o nome do arquivo no ambiente Node.js:
$ node volumeCalculator.js A sphere with radius 3 has a 113.09733552923254 volume. A cylinder with radius 3 and height 5 has a 141.3716694115407 volume. A cone with radius 3 and height 5 has a 47.12388980384689 volume.
No ambiente Node.js, todo arquivo de código-fonte é considerado um módulo, mas a palavra “module” em Node.js indica um pacote de código embrulhado como no exemplo anterior. Graças aos módulos, abstraímos as funções de volume do arquivo principal, volumeCalculator.js
, reduzindo assim seu tamanho e facilitando a aplicação de testes de unidade, o que é uma boa prática ao desenvolver aplicativos no mundo real.
Agora que sabemos como os módulos são usados no Node.js, podemos usar uma das ferramentas mais importantes: o Node Package Manager (NPM).
Uma das principais tarefas do NPM é gerenciar, baixar e instalar módulos externos no projeto ou no sistema operacional. O comando npm init
permite inicializar um repositório de nó.
O NPM fará as perguntas padrão sobre o nome do seu repositório, versão, descrição e assim por diante. Você pode ignorar essas etapas usando npm init --yes
, e o comando irá gerar automaticamente um arquivo package.json
descrevendo as propriedades do seu projeto/módulo.
Abra o arquivo package.json
em seu editor de texto favorito e você verá um arquivo JSON contendo propriedades como palavras-chave, comandos de script para usar com NPM, um nome etc.
Uma dessas propriedades são as dependências instaladas em seu repositório local. O NPM adicionará o nome e a versão dessas dependências em package.json
, junto com package-lock.json
, outro arquivo usado como reserva pelo NPM no caso de package.json
falhar.
Digite o seguinte em seu terminal:
$ npm i dayjs
O sinalizador i
é um atalho para o argumento install
. Se você estiver conectado à internet, o NPM vai procurar um módulo chamado dayjs
no repositório remoto do Node.js, baixar o módulo e instalá-lo localmente. O NPM também adicionará essa dependência aos arquivos package.json
e package-lock.json
. Você perceberá a presença de uma pasta chamada node_modules
, que contém o módulo instalado junto com outros módulos, se eles forem necessários. O diretório node_modules
contém o código real que será usado quando a biblioteca for importada e chamada. Porém, esta pasta não é salva nos sistemas de versionamento que usam Git, já que o arquivo package.json
fornece todas as dependências usadas. Outro usuário pode pegar o arquivo package.json
e simplesmente executar npm install
em sua própria máquina, onde o NPM criará uma pasta node_modules
com todas as dependências de package.json
, evitando assim o controle de versão para os milhares de arquivos disponíveis no repositório NPM.
Agora que o módulo dayjs
está instalado no diretório local, abra o console Node.js e digite as seguintes linhas:
const dayjs = require('dayjs');
dayjs().format('YYYY MM-DDTHH:mm:ss')
O módulo dayjs
é carregado com a palavra-chave require
. Quando um método do módulo é chamado, a biblioteca pega a data e hora atuais do sistema e as exibe no formato especificado:
2020 11-22T11:04:36
Este é o mesmo mecanismo usado no exemplo anterior, em que o tempo de execução do Node.js carrega a função de terceiros no código.
Funcionalidade do servidor
Como o Node.js controla o back-end de aplicativos web, uma de suas principais tarefas é lidar com solicitações HTTP.
Eis um resumo de como os servidores web lidam com as solicitações HTTP de entrada. A funcionalidade do servidor é escutar solicitações, determinar o mais rápido possível qual a resposta de que cada um precisa e retornar essa resposta ao remetente da solicitação. Esse aplicativo deve receber uma solicitação HTTP de entrada acionada pelo usuário, analisar a solicitação, realizar o cálculo, gerar a resposta e enviá-la de volta. Um módulo HTTP, como o Node.js, é usado porque simplifica essas etapas, permitindo que o programador se concentre no próprio aplicativo.
Considere o seguinte exemplo, que implementa essa funcionalidade básica:
const http = require('http');
const url = require('url');
const hostname = '127.0.0.1';
const port = 3000;
const server = http.createServer((req, res) => {
const queryObject = url.parse(req.url,true).query;
let result = parseInt(queryObject.a) + parseInt(queryObject.b);
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.end(`Result: ${result}\n`);
});
server.listen(port, hostname, () => {
console.log(`Server running at http://${hostname}:${port}/`);
});
Salve esse conteúdo em um arquivo chamado basic_server.js
e execute-o através de um comando node
. O terminal que executa o Node.js exibirá a seguinte mensagem:
Server running at http://127.0.0.1:3000/
Em seguida, visite a URL a seguir em seu navegador web: http://127.0.0.1:3000/numbers?a=2&b=17
O Node.js está rodando um servidor web em seu computador e usando dois módulos: http
e url
. O módulo http
configura um servidor HTTP básico, processa as solicitações da web de entrada e as entrega ao nosso aplicativo simples. O módulo URL analisa os argumentos passados na URL, converte-os em um formato inteiro e executa a operação de adição. O módulo http
então envia a resposta em forma de texto para o navegador web.
Em um aplicativo web real, o Node.js é comumente usado para processar e obter dados, geralmente de um banco de dados, e retornar as informações processadas ao front-end para exibição. Mas o aplicativo básico nesta lição mostra de forma concisa como o Node.js usa módulos para lidar com solicitações da web como um servidor web.
Exercícios Guiados
-
Quais são as razões para se usar módulos em vez de escrever funções simples?
-
Por que o ambiente Node.js se tornou tão popular? Cite uma característica.
-
Qual é a finalidade do arquivo
package.json
? -
Por que não é recomendado salvar e compartilhar a pasta
node_modules
?
Exercícios Exploratórios
-
Como você pode executar aplicativos Node.js em seu computador?
-
Como delimitar parâmetros na URL ao se fazer análises dentro do servidor?
-
Descreva um cenário em que uma tarefa específica pode ser um gargalo para um aplicativo Node.js.
-
Como seria possível implementar um parâmetro para multiplicar ou somar os dois números no exemplo do servidor?
Resumo
Esta lição trouxe uma visão geral do ambiente do Node.js, suas características e como ele pode ser usado para implementar programas simples. A lição inclui os seguintes conceitos:
-
O que é o Node.js e por que é usado.
-
Como executar programas do Node.js usando a linha de comando.
-
Os laços de eventos e o thread único.
-
Módulos.
-
Node Package Manager (NPM).
-
Funcionalidade de servidor.
Respostas aos Exercícios Guiados
-
Quais são as razões para se usar módulos em vez de escrever funções simples?
Ao optar por módulos em vez de funções convencionais, o programador cria uma base de código mais simples de ler e manter, bem como para escrever testes automatizados.
-
Por que o ambiente Node.js se tornou tão popular? Cite uma característica.
Um dos motivos é a flexibilidade da linguagem JavaScript, que já era amplamente utilizada no front-end dos aplicativos web. O Node.js permite o uso de apenas uma linguagem de programação em todo o sistema.
-
Qual é a finalidade do arquivo
package.json
?Este arquivo contém metadados para o projeto, como nome, versão, dependências (bibliotecas) e assim por diante. Com um arquivo
package.json
, outras pessoas podem baixar e instalar as mesmas bibliotecas e executar testes da mesma forma que o criador original. -
Por que não é recomendado salvar e compartilhar a pasta
node_modules
?A pasta
node_modules
contém as implementações de bibliotecas disponíveis em repositórios remotos. Portanto, a melhor maneira de compartilhar essas bibliotecas é indicá-las no arquivopackage.json
e então usar o NPM para baixá-las. Esse método é mais simples e menos sujeito a erros, já que não é necessário rastrear e manter as bibliotecas localmente.
Respostas aos Exercícios Exploratórios
-
Como você pode executar aplicativos Node.js em seu computador?
É possível fazer isso digitando
node PATH/FILE_NAME.js
na linha de comando do terminal, alterandoPATH
para o caminho do arquivo Node.js e trocandoFILE_NAME.js
pelo nome de arquivo escolhido. -
Como delimitar parâmetros na URL ao se fazer análises dentro do servidor?
O caractere “e comercial” (
&
) é usado para delimitar esses parâmetros, de forma que eles possam ser extraídos e analisados no código JavaScript. -
Descreva um cenário em que uma tarefa específica pode ser um gargalo para um aplicativo Node.js.
O Node.js não é um bom ambiente para executar processos intensivos de CPU, já que usa um único thread. Um cenário de computação numérica poderia desacelerar e bloquear todo o aplicativo. Se uma simulação numérica for necessária, é melhor usar outras ferramentas.
-
Como seria possível implementar um parâmetro para multiplicar ou somar os dois números no exemplo do servidor?
Use um operador ternário ou uma condição if-else para verificar um parâmetro adicional. Se o parâmetro é a string
mult
ele retorna o produto dos números, do contrário retorna a soma. Substitua o código antigo pelo trecho abaixo. Reinicie o servidor na linha de comando pressionando kbd:[Ctrl+C] e execute novamente o comando para reiniciar o servidor. A seguir, teste o novo aplicativo visitando a URLhttp://127.0.0.1:3000/numbers?a=2&b=17&operation=mult
em seu navegador. Se você omitir ou alterar o último parâmetro, os resultados devem ser a soma dos números.let result = queryObject.operation == 'mult' ? parseInt(queryObject.a) * parseInt(queryObject.b) : parseInt(queryObject.a) + parseInt(queryObject.b);