"Um estudante perguntou ‘Os programadores de antigamente usavam somente máquinas simples e nenhuma linguagem de programação, mas mesmo assim eles construíram lindos programas. Por que nós usamos máquinas complicadas e linguagens de programação?’. Fu-Tzu respondeu ‘Os construtores de antigamente usaram somente varas e barro, mas mesmo assim eles construíram lindas cabanas."
Mestre Yuan-Ma, The Book of Programming
Até agora você vem aprendendo e usando a linguagem JavaScript num único ambiente: o navegador. Esse capítulo e o próximo vão introduzir brevemente você ao Node.js, um programa que permite que você aplique suas habilidades de JavaScript fora do navegador. Com isso, você pode construir desde uma ferramenta de linha de comando até servidores HTTP dinâmicos.
Esses capítulos visam te ensinar conceitos importantes nos quais o Node.js foi construído, e também te dar informação suficiente para escrever alguns programas úteis. Esses capítulos não detalham completamente o funcionamento do Node.
Você vem executando o código dos capítulos anteriores diretamente nessas páginas, pois eram pura e simplesmente JavaScript ou foram escritos para o navegador, porém os exemplos de códigos nesse capítulo são escritos para o Node e não vão rodar no navegador.
Se você quer seguir em frente e rodar os códigos desse capítulo, comece indo em http://nodejs.org e seguindo as instruções de instalação para o seu sistema operacional. Guarde também esse site como referência para uma documentação mais profunda sobre Node e seus módulos integrados.
Um dos problemas mais difíceis em escrever sistemas que se comunicam através de uma rede é administrar a entrada e saída — ou seja, ler escrever dados na rede, num disco rígido, e outros dispositivos. Mover os dados desta forma consome tempo, e planejar isso de forma inteligente pode fazer uma enorme diferença na velocidade em que um sistema responde ao usuário ou às requisições da rede.
A maneira tradicional de tratar a entrada e saída é ter uma função, como
readfile
, que começa a ler um arquivo e só retorna quando o arquivo foi
totalmente lido. Isso é chamado I/O síncrono (I/O quer dizer input/output ou
entrada/saída).
Node foi inicialmente concebido para o propósito de tornar a assincronicidade I/O
mais fácil e conveniente. Nós já vimos interfaces síncronas antes, como o objeto
XMLHttpRequest
do navegador, discutido no Capítulo 17. Uma interface
assíncrona permite que o script continue executando enquanto ela faz seu trabalho
e chama uma função de callback quando está finalizada. Isso é como Node faz
todo seu I/O.
JavaScript é ideal para um sistema como Node. É uma das poucas linguagens de programação que não tem uma maneira embutida de fazer I/O. Dessa forma, JavaScript poderia encaixar-se bastante na abordagem excêntrica do Node para o I/O sem acabar ficando com duas interfaces inconsistentes. Em 2009, quando Node foi desenhado, as pessoas já estavam fazendo I/O baseado em funções de callback no navegador, então a comunidade em volta da linguagem estava acostumada com um estilo de programação assíncrono.
Eu vou tentar ilustrar I/O síncrono contra I/O assíncrono com um pequeno exemplo, onde um programa precisa buscar recursos da Internet e então fazer algum processamento simples com o resultado dessa busca.
Em um ambiente síncrono, a maneira óbvia de realizar essa tarefa é fazer uma requisição após outra. Esse método tem a desvantagem de que a segunda requisição só será realizada após a primeira ter finalizado. O tempo total de execução será no mínimo a soma da duração das duas requisições. Isso não é um uso eficaz da máquina, que vai estar inativa por boa parte do tempo enquanto os dados são transmitidos através da rede.
A solução para esse problema, num sistema síncrono, é iniciar threads de controle. (Dê uma olhada no Capítulo 14 para uma discussão sobre threads.) Uma segunda thread poderia iniciar a segunda requisição, e então ambas as threads vão esperar os resultados voltarem, e após a ressincronização elas vão combinar seus resultados.
No seguinte diagrama, as linhas grossa representam o tempo que o programa gastou em seu processo normal, e as linhas finas representam o tempo gasto esperando pelo I/O. Em um modelo síncrono, o tempo gasto pelo I/O faz parte da linha do tempo de uma determinada thread de controle. Em um modelo assíncrono, iniciar uma ação de I/O causa uma divisão na linha do tempo, conceitualmente falando. A thread que iniciou o I/O continua rodando, e o I/O é finalizado juntamente à ela, chamando uma função de callback quando é finalizada.
Uma outra maneira de mostrar essa diferença é que essa espera para que o I/O finalize é implícita no modelo síncrono, enquanto que é explícita no assíncrono. Mas assincronia é uma faca de dois gumes. Ela faz com que expressivos programas que seguem uma linha reta se tornem mais estranhos.
No capítulo 17, eu já mencionei o fato de que todos esses callbacks adicionam um pouco de ruído e rodeios para um programa. Se esse estilo de assincronia é uma boa ideia ou não, em geral isso pode ser discutido. De qualquer modo, levará algum tempo para se acostumar.
Mas para um sistema baseado em JavaScript, eu poderia afirmar que esse estilo de assincronia com callback é uma escolha sensata. Uma das forças do JavaScript é sua simplicidade, e tentar adicionar múltiplas threads de controle poderia causar uma grande complexidade. Embora os callbacks não tendem a ser códigos simples, como conceito, eles são agradavelmente simples e ainda assim poderosos o suficiente para escrever servidores web de alta performance.
Quando Node.js está instalado em um sistema, ele disponibiliza um programa
chamado node
, que é usado para executar arquivos JavaScript. Digamos que
você tenha um arquivo chamado ola.js
, contendo o seguinte código:
var mensagem = "Olá mundo";
console.log(mensagem);
Você pode então rodar node
a partir da linha de comando para executar o
programa:
$ node ola.js
Olá mundo
O método console.log
no Node tem um funcionamento bem parecido ao do
navegador. Ele imprime um pedaço de texto. Mas no Node, o texto será impresso
pelo processo padrão de saída, e não no console JavaScript do navegador.
Se você rodar node
sem especificar nenhum arquivo, ele te fornecerá um
prompt no qual você poderá escrever códigos JavaScript e ver o resultado
imediatamente.
$ node
> 1 + 1
2
> [-1, -2, -3].map(Math.abs)
[1, 2, 3]
> process.exit(0)
$
A variável process
, assim como a variável console
, está disponível
globalmente no Node. Ela fornece várias maneiras de inspecionar e manipular o
programa atual. O método exit
finaliza o processo e pode receber um código
de saída, que diz ao programa que iniciou node
(nesse caso, a linha de
comando) se o programa foi completado com sucesso (código zero) ou se encontrou
algum erro (qualquer outro código).
Para encontrar os argumentos de linha de comando recebidos pelo seu script, você
pode ler process.argv
, que é um array de strings. Note que também
estarão inclusos o nome dos comandos node
e o nome do seu script, fazendo
com que os argumentos comecem na posição 2. Se showargv.js
contém somente
o statement console.log(process.argv)
, você pode rodá-lo dessa forma:
$ node showargv.js one --and two
["node", "/home/braziljs/showargv.js", "one", "--and", "two"]
Todas as variáveis JavaScript globais, como Array
, Math
and
JSON
, estão presentes também no ambiente do Node. Funcionalidades
relacionadas ao navegador, como document
e alert
estão ausentes.
O objeto global do escopo, que é chamado window
no navegador, passa a ser
global
no Node, que faz muito mais sentido.
Além de algumas variáveis que mencionei, como console
e process
, Node
também colocou pequenas funcionalidades no escopo global. Se você quiser acessar
outras funcionalidades embutidas, você precisa pedir esse módulo ao sistema.
O sistema de módulo CommonJS, baseado na função require
, estão descritos
no Capítulo 10. Esse sistema é construído em Node e é usado para carregar desde
módulos integrados até bibliotecas transferidas, ou até mesmo, arquivos que
fazem parte do seu próprio programa.
Quando require
é chamado, Node tem que transformar a string recebida em
um arquivo real a ser carregado. Nomes de caminhos que começam com "/", "./", ou
"../" são resolvidos relativamente ao atual caminho do módulo, aonde "./"
significa o diretório corrente, "../" para um diretório acima, e "/" para a raiz
do sistema de arquivos. Então se você solicitar por "./world/world"
do
arquivo /home/braziljs/elife/run.js
, Node vai tentar carregar o arquivo
/home/braziljs/elife/world/world.js
. A extensão .js
pode ser
omitida.
Quando uma string recebida pelo require
não parece ter um caminho
relativo ou absoluto, fica implícito que ela se refere a um módulo integrado ou
que está instalado no diretório node_modules
. Por exemplo,
require(fs)
disponibilizará o módulo de sistema de arquivos integrado ao
Node, require("elife")
vai tentar carregar a biblioteca encontrada em
node_modules/elife
. A maneira mais comum de instalar bibliotecas como
essas é usando NPM, que em breve nós vamos discutir.
Para ilustrar o uso do require
, vamos configurar um projeto simples que
consiste de dois arquivos. O primeiro é chamado main.js
, que define um
script que pode ser chamado da linha de comando para alterar uma string.
var garble = require("./garble");
// O índice 2 possui o valor do primeiro parâmetro da linha de comando
var parametro = process.argv[2];
console.log(garble(parametro));
O arquivo garble.js
define uma biblioteca para alterar string, que pode
ser usada tanto da linha de comando quanto por outros scripts que precisam ter
acesso direto a função de alterar.
module.exports = function(string) {
return string.split("").map(function(ch) {
return String.fromCharCode(ch.charCodeAt(0) + 5);
}).join("");
}
Lembre-se que substituir module.exports
, ao invés de adicionar propriedades
à ele, nos permite exportar um valor específico do módulo. Nesse caso, nós
fizemos com que o resultado ao requerer nosso arquivo garble
seja a
própria função de alterar.
A função separa a string recebida em dois caracteres únicos separando a string vazia e então substituindo cada caractere cujo código é cinco pontos maior. Finalmente, o resultado é reagrupado novamente numa string.
Agora nós podemos chamar nossa ferramenta dessa forma:
$ node main.js JavaScript
Of{fXhwnuy
NPM, que foi brevemente discutido no Capítulo 10, é um repositório online de
módulos JavaScript, muitos deles escritos para Node. Quando você instala o Node
no seu computador, você também instala um programa chamado npm
, que fornece
uma interface conveniente para esse repositório.
Por exemplo, um módulo que você vai encontrar na NPM é figlet
, que pode
converter texto em ASCII art—desenhos feitos de caracteres de texto. O trecho
a seguir mostra como instalar e usar esse módulo:
$ npm install figlet
npm GET https://registry.npmjs.org/figlet
npm 200 https://registry.npmjs.org/figlet
npm GET https://registry.npmjs.org/figlet/-/figlet-1.0.9.tgz
npm 200 https://registry.npmjs.org/figlet/-/figlet-1.0.9.tgz
figlet@1.0.9 node_modules/figlet
$ node
> var figlet = require("figlet");
> figlet.text("Hello world!", function(error, data) {
if (error)
console.error(error);
else
console.log(data);
});
_ _ _ _ _ _ _
| | | | ___| | | ___ __ _____ _ __| | __| | |
| |_| |/ _ \ | |/ _ \ \ \ /\ / / _ \| '__| |/ _` | |
| _ | __/ | | (_) | \ V V / (_) | | | | (_| |_|
|_| |_|\___|_|_|\___/ \_/\_/ \___/|_| |_|\__,_(_)
Depois de rodar npm install
, NPM já vai ter criado um diretório chamado
node_modules
. Dentro desse diretório haverá um outro diretório chamado
figlet
, que vai conter a biblioteca. Quando rodamos node
e
chamamos require("figlet")
, essa biblioteca é carregada, e nós podemos
chamar seu método text
para desenhar algumas letras grandes.
Talvez de forma inesperada, ao invés de retornar a string que faz crescer as
letras, figlet.text
têm uma função de callback que passa o resultado
para ela. Ele também passa outro parâmetro no callback, error
, que vai
possuir um objeto de erro quando alguma coisa sair errada ou nulo se tudo
ocorrer bem.
Isso é um padrão comum em Node. Renderizar alguma coisa com figlet
requer
a biblioteca para ler o arquivo que contém as formas das letras. Lendo esse
arquivo do disco é uma operação assíncrona no Node, então figlet.text
não
pode retornar o resultado imediatamente. Assincronia é, de certa forma,
infecciosa—qualquer função que chamar uma função assincronamente precisa se
tornar assíncrona também.
Existem muito mais coisas no NPM além de npm install
. Ele pode ler
arquivos package,json
, que contém informações codificadas em JSON sobre
o programa ou biblioteca, como por exemplo outras bibliotecas que depende.
Rodar npm install
em um diretório que contém um arquivo como esse vai
instalar automaticamente todas as dependências, assim como as dependências das
dependências. A ferramenta npm
também é usada para publicar bibliotecas
para o repositório NPM online de pacotes para que as pessoas possam encontrar,
transferir e usá-los.
Esse livro não vai abordar detalhes da utilização do NPM. Dê uma olhada em npmjs.org para uma documentação mais detalhada e para uma maneira simples de procurar por bibliotecas.
Um dos módulos integrados mais comuns que vêm com o Node é o módulo "fs"
,
que significa file system. Esse módulo fornece funções para o trabalho com
arquivos de diretórios.
Por exemplo, existe uma função chamada readFile
, que lê um arquivo e então
chama um callback com o conteúdo desse arquivo.
var fs = require("fs");
fs.readFile("file.txt", "utf8", function(error, text) {
if (error)
throw error;
console.log("The file contained:", text);
});
O segundo argumento passado para readFile
indica a codificação de caracteres
usada para decodificar o arquivo numa string. Existem muitas maneiras de
codificar texto em informação binária, mas a maioria dos sistemas modernos usam
UTF-8 para codificar texto, então a menos que você tenha razões para acreditar
que outra forma de codificação deve ser usada, passar "utf8" ao ler um arquivo de
texto é uma aposta segura. Se você não passar uma codificação, o Node vai
assumir que você está interessado na informação binária e vai te dar um objeto
Buffer
ao invés de uma string. O que por sua vez, é um objeto
array-like que contém números representando os bytes nos arquivos.
var fs = require("fs");
fs.readFile("file.txt", function(error, buffer) {
if (error)
throw error;
console.log("The file contained", buffer.length, "bytes.",
"The first byte is:", buffer[0]);
});
Uma função similar, writeFile
, é usada para escrever um arquivo no disco.
var fs = require("fs");
fs.writeFile("graffiti.txt", "Node was here", function(err) {
if (err)
console.log("Failed to write file:", err);
else
console.log("File written.");
});
Aqui, não foi necessário especificar a codificação de caracteres, pois a função
writeFile
assume que recebeu uma string e não um objeto Buffer
, e
então deve escrever essa string como texto usando a codificação de caracteres
padrão, que é UTF-8.
O módulo "fs"
contém muitas outras funções úteis: readdir
que vai
retornar os arquivos em um diretório como um array de strings, stat
vai buscar informação sobre um arquivo, rename
vai renomear um arquivo,
unlink
vai remover um arquivo, e assim por diante. Veja a documentação em
nodejs.org para especificidades.
Muitas das funções em "fs"
vêm com variantes síncronas e assíncronas. Por
exemplo, existe uma versão síncrona de readFile
chamada
readFileSync
.
var fs = require("fs");
console.log(fs.readFileSync("file.txt", "utf8"));
Funções síncronas requerem menos formalismo na sua utilização e podem ser úteis em alguns scripts, onde a extra velocidade oferecida pela assincronia I/O é irrelevante. Mas note que enquanto tal operação síncrona é executada, seu programa fica totalmente parado. Se nesse período ele deveria responder ao usuário ou a outras máquinas na rede, ficar preso com um I/O síncrono pode acabar produzindo atrasos inconvenientes.
Outro principal é o "http"
. Ele fornece funcionalidade para rodar
servidores HTTP e realizar requisições HTTP.
Isso é tudo que você precisa para rodar um simples servidor HTTP:
var http = require("http");
var server = http.createServer(function(request, response) {
response.writeHead(200, {"Content-Type": "text/html"});
response.write("<h1>Hello!</h1><p>You asked for <code>" +
request.url + "</code></p>");
response.end();
});
server.listen(8000);
Se você rodar esse script na sua máquina, você pode apontar seu navegador para o endereço http://localhost:8000/hello para fazer uma requisição no seu servidor. Ele irá responder com uma pequena página HTML.
A função passada como um argumento para createServer
é chamada toda vez
que um cliente tenta se conectar ao servidor. As variáveis request
e
response
são os objetos que representam a informação que chega e sai. A
primeira contém informações sobre a requisição, como por exemplo a propriedade
url
, que nos diz em qual URL essa requisição foi feita.
Para enviar alguma coisa de volta, você chama métodos do objeto response
.
O primeiro, writeHead
, vai escrever os cabeçalhos de resposta (veja o
Capítulo 17). Você define o código de status (200 para "OK" nesse caso) e um
objeto que contém valores de cabeçalho. Aqui nós dizemos ao cliente que
estaremos enviando um documento HTML de volta.
Em seguida, o corpo da resposta (o próprio documento) é enviado com
response.write
. Você pode chamar esse método quantas vezes você quiser
para enviar a resposta peça por peça, possibilitando que a informação seja
transmitida para o cliente assim que ela esteja disponível. Finalmente,
response.end
assina o fim da resposta.
A chamada de server.listen
faz com que o servidor comece a esperar por
conexões na porta 8000. Por isso você precisa se conectar a localhost:8000, ao
invés de somente localhost (que deveria usar a porta 80, por padrão), para se
comunicar com o servidor.
Para parar de rodar um script Node como esse, que não finaliza automaticamente pois está aguardando por eventos futuros (nesse caso, conexões de rede), aperte Ctrl+C.
Um servidor real normalmente faz mais do que o que nós vimos no exemplo
anterior—ele olha o método da requisição (a propriedade method
) para ver
que ação o cliente está tentando realizar e olha também a URL da requisição para
descobrir que recurso essa ação está executando. Você verá um servidor mais
avançado daqui a pouco neste capítulo.
Para agir como um cliente HTTP, nós podemos usar a função request
no
módulo "http"
.
var http = require("http");
var request = http.request({
hostname: "eloquentjavascript.net",
path: "/20_node.html",
method: "GET",
headers: {Accept: "text/html"}
}, function(response) {
console.log("Server responded with status code",
response.statusCode);
});
request.end();
O primeiro parâmetro passado para request
configura a requisição, dizendo
pro Node qual o servidor que ele deve se comunicar, que caminho solicitar
daquele servidor, que método usar, e assim por diante. O segundo parâmetro é a
função que deverá ser chamada quando uma resposta chegar. É informado um objeto
que nos permite inspecionar a resposta, para descobrir o seu código de status,
por exemplo.
Assim como o objeto response
que vimos no servidor, o objeto request
nos permite transmitir informação na requisição com o método write
e
finalizar a requisição com o método end
. O exemplo não usa write
porque requisições GET
não devem conter informação no corpo da requisição.
Para fazer requisições para URLs HTTP seguras (HTTPS), o Node fornece um pacote
chamado https
, que contém sua própria função request
, parecida a
http.request
.
Nós já vimos dois exemplos de streams em HTTP—são, consecutivamente, o
objeto de resposta no qual o servidor pode escrever e o objeto de requisição que
foi retornado do http.request
.
Strams de gravação são um conceito amplamente usado nas interfaces Node.
Todos os streams de gravação possuem um método write
, que pode receber
uma string ou um objeto Buffer
. Seus métodos end
fecham a transmissão e, se passado um parâmetro, também vai escrever alguma
informação antes de fechar. Ambos métodos podem receber um callback como um
parâmetro adicional, que eles vão chamar ao fim do escrever ou fechar a
transmissão.
É possível criar streams de gravação que apontam para um arquivo com a função
fs.createWritebleStram
. Então você pode usar o método write
no
objeto resultante para escrever o arquivo peça por peça, ao invés de escrever
tudo de uma só vez com o fs.writeFile
.
Streams de leitura são um pouco mais fechados. Em ambos a variável
request
que foi passada para a função de callback do servidor HTTP e a
variável response
para o cliente HTTP são streams de leitura. (Um
servidor lê os pedidos e então escreve as respostas, enquanto que um cliente
primeiro escreve um pedido e então lê a resposta.) Para ler de um stream
usamos manipuladores de eventos, e não métodos.
Objetos que emitem eventos no Node têm um método chamado on
que é similar
ao método addEventListener
no navegador. Você dá um nome de evento e então
uma função, e isso irá registrar uma função para ser chamada toda vez que um
dado evento ocorrer.
Streams de leitura possuem os eventos "data"
e "end"
. O primeiro é
acionado sempre que existe alguma informação chegando, e o segundo é chamado
sempre que a stream chega ao fim. Esse modelo é mais adequado para um
streamming de dados, que pode ser imediatamente processado, mesmo quando todo
documento ainda não está disponível. Um arquivo pode ser lido como uma stream
de leitura usando a função fs.createReadStream
.
O seguinte código cria um servidor que lê o corpo da requisição e o devolve em caixa alta para o cliente via stream:
var http = require("http");
http.createServer(function(request, response) {
response.writeHead(200, {"Content-Type": "text/plain"});
request.on("data", function(chunk) {
response.write(chunk.toString().toUpperCase());
});
request.on("end", function() {
response.end();
});
}).listen(8000);
A variável chunk
enviada para o manipulador de dados será um Buffer
binário,
que nós podemos converter para uma string chamando toString
nele, que vai
decodificá-lo usando a codificação padrão (UTF-8).
O seguinte trecho de código, se rodado enquanto o servidor que transforma letras em caixa alta estiver rodando, vai enviar uma requisição para esse servidor e retornar a resposta que obtiver:
var http = require("http");
var request = http.request({
hostname: "localhost",
port: 8000,
method: "POST"
}, function(response) {
response.on("data", function(chunk) {
process.stdout.write(chunk.toString());
});
});
request.end("Hello server");
O exemplo escreve no process.stdout
(a saída padrão de processos, como uma
stream de escrita) ao invés de usar console.log
. Nós não podemos usar
console.log
porque isso adicionaria uma linha extra depois de cada pedaço de
texto escrito, o que é adequado no nosso exemplo.
Vamos combinar nossas novas descobertas sobre servidores HTTP e conversas sobre sistema de arquivos e criar uma ponte entre eles: um servidor HTTP que permite acesso remoto ao sistema de arquivos. Um servidor desse tipo possui diversos usuários. Ele permite que aplicações web guardem e compartilhem dados ou dá direito para um determinado grupo de pessoas compartilhar muitos arquivos.
Quando lidamos com arquivos de recursos HTTP, os métodos HTTP GET
, PUT
e
DELETE
podem ser usados, respectivamente, para ler, escrever e apagar esses
arquivos. Nós vamos interpretar o caminho na requisição como o caminho do
arquivo referido por aquela requisição.
Provavelmente nós não queremos compartilhar todo nosso sistema de arquivos, então
nós vamos interpretar esses caminhos como se começassem no diretório de trabalho
do servidor, que é o diretório no qual ele começou. Se eu rodar o servidor de
/home/braziljs/public/
(ou C:\Users\braziljs\public\
no Windows), então a
requisição por /file.txt
deve ser referir a /home/braziljs/public/file.txt
(
ou C:\Users\braziljs\public\file.txt
).
Nós vamos construir um programa peça por peça, usando um objeto chamado
methods
para guardar as funções que tratam os vários métodos HTTP.
var http = require("http"), fs = require("fs");
var methods = Object.create(null);
http.createServer(function(request, response) {
function respond(code, body, type) {
if (!type) type = "text/plain";
response.writeHead(code, {"Content-Type": type});
if (body && body.pipe)
body.pipe(response);
else
response.end(body);
}
if (request.method in methods)
methods[request.method](urlToPath(request.url),
respond, request);
else
respond(405, "Method " + request.method +
" not allowed.");
}).listen(8000);
Isso vai começar um servidor que apenas retorna erro 405 nas respostas, que é o código usado para indicar que dado método não está sendo tratado pelo servidor.
A função respond
é passada para as funções que tratam os vários métodos e agem
como callback para finalizar a requisição. Isso carrega um código de status
do HTTP, um corpo e opcionalmente um tipo conteúdo como argumentos. Se o valor
passado para o corpo é um stream de leitura, ele terá um método pipe
, que
será usado para encaminhar uma stream de leitura para uma stream de escrita.
Caso contrário, assumimos que o corpo será null
(não há corpo) ou uma string
é passada diretamente para o método end
da resposta.
Para obter um caminho de uma URL em uma requisição, a função urlToPath
usa o
módulo "url
" embutido no Node para parsear a URL. Ela pega o nome do caminho,
que será algo parecido a /file.txt
, o decodifica para tirar os códigos de
escape (como %20
e etc), e coloca um único ponto para produzir um caminho
relativo ao diretório atual.
function urlToPath(url) {
var path = require("url").parse(url).pathname;
return "." + decodeURIComponent(path);
}
É provável que você esteja preocupado com a segurança da função urlToPath
,
e você está certo, deve se preocupar mesmo. Nós vamos retornar a ela nos
exercícios.
Nós vamos fazer com que o método GET
retorne uma lista de arquivos quando
lermos um diretório e retornar o conteúdo do arquivo quando lermos um arquivo
regular.
Uma questão delicada é que tipo de cabeçalho Content-Type
nós devemos
adicionar quando retornar um conteúdo de um arquivo. Tendo em vista que esses
arquivos podem ser qualquer coisa, nosso servidor não pode simplesmente retornar
o mesmo tipo para todos eles. Mas o NPM pode ajudar com isso. O pacote mime
(indicadores de tipo de conteúdo como text/plain
também são chamados
MIME types) sabe o tipo adequado de um grande número de extensões de arquivos.
Se você rodar o seguinte comando npm
no diretório aonde o script do servidor
está, você estará apto a usar require("mime")
para acessar essa biblioteca:
$ npm install mime
npm http GET https://registry.npmjs.org/mime
npm http 304 https://registry.npmjs.org/mime
mime@1.2.11 node_modules/mime
Quando um arquivo requisitado não existe, o código de erro HTTP adequado a ser
retornado é 404. Nós vamos usar fs.stat
, que obtém informações sobre um
arquivo, para saber se o arquivo existe e/ou se é um diretório.
methods.GET = function(path, respond) {
fs.stat(path, function(error, stats) {
if (error && error.code == "ENOENT")
respond(404, "File not found");
else if (error)
respond(500, error.toString());
else if (stats.isDirectory())
fs.readdir(path, function(error, files) {
if (error)
respond(500, error.toString());
else
respond(200, files.join("\n"));
});
else
respond(200, fs.createReadStream(path),
require("mime").lookup(path));
});
};
Como ele pode levar um bom tempo para encontrar o arquivo no disco, fs.stat
é
assíncrono. Quando o arquivo não existe, fs.stat
vai passar um objeto de erro
com "ENOENT"
em uma propriedade chamada code
para o seu callback. Isso
seria muito bom se o Node definisse diferentes subtipos de Error
para
diferentes tipos de erros, mas ele não o faz. Ao invés disso, Node coloca um
código obscuro, inspirado no sistema Unix lá.
Nós vamos reportar qualquer erro que não esperamos com o código de status 500, que indica que o problema está no servidor, ao contrário dos códigos que começam com 4 (como o 404), que se referem a requisições ruins. Existem algumas situações nas quais isso não totalmente preciso, mas para um programa pequeno de exemplo como esse, deverá ser bom o suficiente.
O objeto status
retornado pelo fs.stat
nos diz uma porção de coisas sobre um
arquivo, tais como tamanho (propriedade size
) e sua data de modificação
(propriedade mtime
). Nosso interesse aqui é saber se isso é um diretório ou um
arquivo regular, e quem nos diz isso é o método isDirectory
.
Nós usamos fs.readdir
para ler a lista de arquivos em um diretório e, ainda em
outro callback, retornar o resultado para o usuário. Para arquivos comuns, nós
criamos uma stream de leitura com o fs.createReadStream
e passamos ela ao
respond
, junto com o tipo de conteúdo que o módulo "mime"
nos deu para esse
nome de arquivo.
O código que trata as requisições de DELETE
é um pouco mais simples.
methods.DELETE = function(path, respond) {
fs.stat(path, function(error, stats) {
if (error && error.code == "ENOENT")
respond(204);
else if (error)
respond(500, error.toString());
else if (stats.isDirectory())
fs.rmdir(path, respondErrorOrNothing(respond));
else
fs.unlink(path, respondErrorOrNothing(respond));
});
};
Você deve estar se perguntando porque tentar deletar um arquivo inexistente retornar um status 204, e não um erro. Quando o arquivo que será deletado não existe, você pode dizer que o objetivo da requisição já foi cumprido. O padrão HTTP recomenda que as pessoas façam requisições idempotentes, o que significa que independente da quantidade de requisições, elas não devem produzir um resultado diferente.
function respondErrorOrNothing(respond) {
return function(error) {
if (error)
respond(500, error.toString());
else
respond(204);
};
}
Quando uma resposta HTTP não contém nenhum dado, o status 204 ("no content")
pode ser usado para indicar isso. Tendo em vista que a gente precisa construir
callbacks que reportam um erro ou retornam uma resposta 204 em diferentes
situações, eu escrevi uma função chamada respondErrorOrNothing
que cria esse
callback.
Aqui está a função que trata as requisições PUT
:
methods.PUT = function(path, respond, request) {
var outStream = fs.createWriteStream(path);
outStream.on("error", function(error) {
respond(500, error.toString());
});
outStream.on("finish", function() {
respond(204);
});
request.pipe(outStream);
};
Aqui, nós não precisamos checar se o arquivo existe - se ele existe, nós
simplesmente sobrescrevemos ele. Novamente nós usamos pipe
para mover a
informação de um stream de leitura para um de escrita, nesse caso de uma
requisição para um arquivo. Se a criação do stream falhar, um evento "error"
é disparado e reportado na nossa resposta. Quando a informação for transferida
com sucesso, pipe
vai fechar ambos streams, o que vai disparar o evento
"finish"
no stream de escrita. Quando isso acontecer, nós podemos reportar
sucesso na nossa resposta para o cliente com um status 204.
O script completo para o servidor está disponível em eloquentjavascript.net/code/file_server.js. Você pode fazer o download e rodá-lo com Node pra começar seu próprio servidor de arquivos. E é claro, você pode modificá-lo e extendê-lo para resolver os exercícios desse capítulo ou para experimentar.
A ferramente de linha de comando curl
, amplamente disponível em sistemas Unix,
pode ser usada para fazer requisições HTTP. A sessão a seguir é um rápido teste
do nosso servidor. Note que -X
é usado para para escolher o método da
requisição e -d
é usado para incluir o corpo da requisição.
$ curl http://localhost:8000/file.txt
File not found
$ curl -X PUT -d hello http://localhost:8000/file.txt
$ curl http://localhost:8000/file.txt
hello
$ curl -X DELETE http://localhost:8000/file.txt
$ curl http://localhost:8000/file.txt
File not found
A primeira requisição feita para o arquivo file.txt
falha pois o arquivo ainda
não existe. A requisição PUT
cria o arquivo, para que então a próxima
requisição consiga encontrá-lo com sucesso. Depois de deletar o arquivo com uma
requisição DELETE
, o arquivo passa a não ser encontrado novamente.
No código para o servidor de arquivos, existem seis lugares aonde nós estamos explicitando exceções de rota que nós não sabemos como tratá-los como respostas de erro. Como exceções são passadas como argumentos e, portanto, não são automaticamente propagadas para os callbacks, elas precisam ser tratadas a todo momento de forma explícita. Isso acaba completamente com a vantagem de tratamento de exceções, isto é, a habilidade de centralizar o tratamento das condições de falha.
O que acontece quando alguma coisa joga uma exceção em seu sistema? Como não
estamos usando nenhum bloco try
, a exceção vai propagar para o topo da pilha
de chamada. No Node, isso aborta o programa e escreve informações sobre a
exceção (incluindo um rastro da pilha) no programa padrão de stream de erros.
Isso significa que nosso servidor vai colidir sempre que um problema for
encontrado no código do próprio servidor, ao contrário dos problemas
assíncronos, que são passados como argumentos para os callbacks. Se nós
quisermos tratar todas as exceções levantadas durante o tratamento de uma
requisição, para ter certeza que enviamos uma resposta, precisamos adicionar
blocos de try/catch
para todos os callbacks.
Isso é impraticável. Muitos programas em Node são escritos para fazer o menor uso possível de exceções, assumindo que se uma exceção for levantada, aconteceu algo que o programa não conseguiu resolver, e colidir é a resposta certa.
Outra abordagem é usar promessas, que foram introduzidas no Capítulo 17.
Promessas capturam as exceções levantadas por funções de callback e propagam
elas como falhas. É possível carregar uma biblioteca de promessa no Node e
usá-la para administrar seu controle assíncrono. Algumas bibliotecas Node
fazem integração com as promessas, mas as vezes é trivial envolvê-las. O
excelente módulo "promise"
do NPM contém uma função chamada denodeify
, que
converte uma função assíncrona como a fs.readFile
para uma função de retorno
de promessa.
var Promise = require("promise");
var fs = require("fs");
var readFile = Promise.denodeify(fs.readFile);
readFile("file.txt", "utf8").then(function(content) {
console.log("The file contained: " + content);
}, function(error) {
console.log("Failed to read file: " + error);
});
A título de comparação, eu escrevi uma outra versão do servidor de arquivos baseado em promessas, que você pode encontrar em eloquentjavascript.net/code/file_server_promises.js. Essa versão é um pouco mais clara pois as funções podem retornar seus resultados, ao invés de ter que chamar callbacks, e a rota de exceções está implícito, ao invés de explícito.
Eu vou mostrar algumas linhas do servidor de arquivos baseado em promessas para ilustrar a diferença no estilo de programação.
O objeto fsp
que é usado por esse código contém estilos de promessas variáveis
para determinado número de funções fs
, envolvidas por Promise.denodeify
. O
objeto retornado, com propriedades code
e body
, vai se tornar o resultado
final de uma cadeia de promessas, e vai ser usado para determinar que tipo de
resposta vamos mandar pro cliente.
methods.GET = function(path) {
return inspectPath(path).then(function(stats) {
if (!stats) // Does not exist
return {code: 404, body: "File not found"};
else if (stats.isDirectory())
return fsp.readdir(path).then(function(files) {
return {code: 200, body: files.join("\n")};
});
else
return {code: 200,
type: require("mime").lookup(path),
body: fs.createReadStream(path)};
});
};
function inspectPath(path) {
return fsp.stat(path).then(null, function(error) {
if (error.code == "ENOENT") return null;
else throw error;
});
}
A função inspectPath
simplesmente envolve o fs.stat
, que trata o caso de
arquivo não encontrado. Nesse caso, nós vamos substituir a falha por um sucesso
que representa null
. Todos os outros erros são permitidos a propagar. Quando a
promessa retornada desses manipuladores falha, o servidor HTTP responde com um
status 500.
Node é um sistema bem íntegro e legal que permite rodar JavaScript em um contexto fora do navegador. Ele foi originalmente concebido para tarefas de rede para desempenhar o papel de um nó na rede. Mas ele se permite a realizar todas as tarefas de script, e se escrever JavaScript é algo que você gosta, automatizar tarefas de rede com Node funciona de forma maravilhosa.
O NPM disponibiliza bibliotecas para tudo que você possa imaginar (e algumas
outras coisas que você provavelmente nunca pensou), e permite que você atualize
e instale essas bibliotecas rodando um simples comando. Node também vêm com um
bom número de módulos embutidos, incluindo o módulo "fs"
, para trabalhar com
sistema de arquivos e o "http"
, para rodar servidores HTTP e fazer requisições
HTTP.
Toda entrada e saída no Node é feita de forma assíncrona, a menos que você
explicitamente use uma variante síncrona da função, como a fs.readFileSync
.
Você fornece as funções de callback e o Node vai chamá-las no tempo certo,
quando o I/O que você solicitou tenha terminado.
No Capítulo 17, o primeiro exercício era fazer várias requisições para
eloquentjavascript.net/author, pedindo
por tipos diferentes de conteúdo passando cabeçalhos Accept
diferentes.
Faça isso novamente usando a função http.request
do Node. Solicite pelo menos
os tipos de mídia text/plain
, text/html
e application/json
. Lembre-se que
os cabeçalhos para uma requisição podem ser passados como objetos, na
propriedade headers
do primeiro argumento da http.request
.
Escreva o conteúdo das respostas para cada requisição.
Dica:
Não se esqueça de chamar o método end
no objeto retornado pela http.request
para de fato disparar a requisição.
O objeto de resposta passado ao callback da http.request
é um stream de
leitura. Isso significa que ele não é muito trivial pegar todo o corpo da
resposta dele. A função a seguir lê todo o stream e chama uma função de
callback com o resultado, usando o padrão comum de passar qualquer erro
encontrado como o primeiro argumento do callback:
function readStreamAsString(stream, callback) {
var data = "";
stream.on("data", function(chunk) {
data += chunk.toString();
});
stream.on("end", function() {
callback(null, data);
});
stream.on("error", function(error) {
callback(error);
});
}
Para um fácil acesso remoto aos arquivos, eu poderia adquirir o hábito de ter o
servidor de arquivos definido nesse capítulo na minha máquina, no diretório
/home/braziljs/public/
. E então, um dia, eu encontro alguém que tenha
conseguido acesso a todos as senhas que eu gravei no navegador.
O que aconteceu?
Se ainda não está claro para você, pense novamente na função urlToPath
definida dessa forma:
function urlToPath(url) {
var path = require("url").parse(url).pathname;
return "." + decodeURIComponent(path);
}
Agora considere o fato de que os caminhos para as funções "fs"
podem ser
relativos-eles podem conter "../" para voltar a um diretório acima. O que
acontece quando um cliente envia uma requisição para uma dessas URLs abaixo?
http://myhostname:8000/../.config/config/google-chrome/Default/Web%20Data
http://myhostname:8000/../.ssh/id_dsa
http://myhostname:8000/../../../etc/passwd
Mudar o urlToPath
corrige esse problema. Levando em conta o fato de que o Node
no Windows permite tanto barras quanto contrabarras para separar diretórios.
Além disso, pense no fato de que assim que você expor algum sistema meia boca na internet, os bugs nesse sistema podem ser usado para fazer coisas ruins para sua máquina.
Dicas
Basta remover todas as recorrências de dois pontos que tenham uma barra, uma
contrabarra ou as extremidades da string. Usando o método replace
com uma
expressão regular é a maneira mais fácil de fazer isso. Não se esqueça da flag
g
na expressão, ou o replace
vai substituir somente uma única instância e
as pessoas ainda poderiam incluir pontos duplos no caminho da URL a partir dessa
medida de segurança! Também tenha certeza de substituir depois de decodificar
a string, ou seria possível despistar o seu controle que codifica pontos e
barras.
Outro caso de preocupação potencial é quando os caminhos começam com barra, que
são interpretados como caminhos absolutos. Mas por conta do urlToPath
colocar
um ponto na frente do caminho, é impossível criar requisições que resultam em
tal caminho. Múltiplas barras numa linha, dentro do caminho, são estranhas mas
serão tratadas como uma única barra pelo sistema de arquivos.
Embora o método DELETE
esteja envolvido em apagar diretórios (usando
fs.rmdir
), o servidor de arquivos não disponibiliza atualmente nenhuma maneira
de criar diretórios.
Adicione suporte para o método MKCOL
, que deve criar um diretório chamando
fs.mkdir
. MKCOL
não é um método básico do HTTP, mas ele existe nas normas
da WebDAV, que especifica um conjunto de extensões para o HTTP, tornando-o
adequado para escrever recursos, além de os ler.
Dicas
Você pode usar a função que implementa o método DELETE
como uma planta baixa
para o método MKCOL
. Quando nenhum arquivo é encontrado, tente criar um
diretório com fs.mkdir
. Quando um diretório existe naquele caminho, você pode
retornar uma resposta 204, então as requisições de criação de diretório serão
idempotentes. Se nenhum diretório de arquivo existe, retorne um código de
erro. O código 400 ("bad request") seria o mais adequado nessa situação.
Uma vez que o servidor de arquivos serve qualquer tipo de arquivo e ainda inclui
o cabeçalho Content-Type
, você pode usá-lo para servir um website. Mas uma vez
que seu servidor de arquivos permita que qualquer um delete e sobrescreva
arquivos, seria um tipo interessante de website: que pode ser modificado,
vandalizado e destruído por qualquer um que gaste um tempo para criar a
requisição HTTP correta. Mas ainda assim, seria um website.
Escreva uma página HTML básica que inclui um simples arquivo JavaScript. Coloque os arquivos num diretório servido pelo servidor de arquivos e abra isso no seu navegador.
Em seguida, como um exercício avançado ou como um projeto de fim de semana, combine todo o conhecimento que você adquiriu desse livro para construir uma interface mais amigável pra modificar o website de dentro do website.
Use um formulário HTML (Capítulo 18) para editar os conteúdos dos arquivos que fazem parte do website, permitindo que o usuário atualize eles no servidor fazendo requisições HTTP como vimos no Capítulo 17.
Comece fazendo somente um único arquivo editável. Então faça de uma maneira que o usuário escolha o arquivo que quer editar. Use o fato de que nosso servidor de arquivos retorna uma lista de arquivos durante a leitura de um diretório.
Não trabalhe diretamente no código do servidor de arquivos, tendo em vista que se você cometer um engano você vai afetar diretamente os arquivos que estão lá. Ao invés disso, mantenha seu trabalho em um diretório sem acessibilidade pública e copie ele pra lá enquanto testa.
Se seu computador está diretamente ligado a internet, sem um firewall,
roteador, ou outro dispositivo interferindo, você pode ser capaz de convidar um
amigo para usar seu website. Para checar, vá até
whatismyip.com, copie e cole o endereço de IP que
ele te deu na barra de endereço do seu navegador, e adicione :8000
depois dele
para selecionar a porta correta. Se isso te levar ao seu website, está online
para qualquer um que quiser ver.
Dicas
Você pode criar um elemento <textarea>
para conter o conteúdo do arquivo que
está sendo editado. Uma requisição GET
, usando XMLHttpRequest
, pode ser
usada para pegar o atual conteúdo do arquivo. Você pode usar URLs relativas como
index.html, ao invés de http://localhost:8000/index.html, para referir-se
aos arquivos do mesmo servidor que está rodando o script.
Então, quando o usuário clicar num botão (você pode usar um elemento <form>
e
um evento "submit"
ou um simples manipulador "click"
), faça uma requisição
PUT
para a mesma URL, com o conteúdo do <textarea>
no corpo da requisição
para salvar o arquivo.
Você pode então adicionar um elemento <select>
que contenha todos os arquivos
na raiz do servidor adicionando elementos <option>
contendo as linhas
retornadas pela requisição GET
para a URL /. Quando um usuário seleciona outro
arquivo (um evento "change"
nesse campo), o script deve buscar e mostrar o
arquivo. Também tenha certeza que quando salvar um arquivo, você esteja usando
o nome do arquivo selecionado.
Infelizmente, o servidor é muito simplista para ser capaz de ler arquivos de
subdiretórios de forma confiável, uma vez que ele não nos diz se a coisa que
está sendo buscado com uma requisição GET
é um arquivo ou um diretório. Você
consegue pensar em uma maneira de extender o servidor para solucionar isso?