035.2 Lição 2
Certificação: |
Web Development Essentials |
---|---|
Versão: |
1.0 |
Tópico: |
035 Programação do servidor NodeJS |
Objetivo: |
035.3 Noções básicas de NodeJS Express |
Lição: |
2 de 2 |
Introdução
Os servidores web têm mecanismos muito versáteis para produzir respostas às solicitações do cliente. Para algumas solicitações, basta que o servidor web forneça uma resposta estática e não processada, porque o recurso solicitado é o mesmo para qualquer cliente. Por exemplo, quando um cliente solicita uma imagem acessível a todos, o servidor precisa apenas enviar o arquivo que contém a imagem.
Mas quando as respostas são geradas dinamicamente, elas podem precisar ser mais bem estruturadas do que simples linhas escritas no script do servidor. Nesses casos, é conveniente que o servidor web seja capaz de gerar um documento completo, que pode ser interpretado e processado pelo cliente. No contexto do desenvolvimento de aplicativos web, os documentos HTML são comumente criados como modelos e mantidos separados do script do servidor, que insere dados dinâmicos em locais predeterminados no modelo apropriado e, em seguida, envia a resposta formatada ao cliente.
Os aplicativos web geralmente consomem recursos estáticos e dinâmicos. Um documento HTML, mesmo que tenha sido gerado dinamicamente, pode ter referências a recursos estáticos, como arquivos CSS e imagens. Para demonstrar como o Express ajuda a lidar com esse tipo de demanda, vamos primeiro configurar um servidor de exemplo que entrega arquivos estáticos e, em seguida, implementar rotas que geram respostas estruturadas baseadas em modelos.
Arquivos estáticos
A primeira etapa é criar o arquivo JavaScript que será executado como servidor. Vamos seguir o mesmo padrão mostrado nas lições anteriores para criar um aplicativo Express simples: primeiro, crie um diretório chamado server
; em seguida, instale os componentes básicos com o comando npm
:
$ mkdir server $ cd server/ $ npm init $ npm install express
Para o ponto de entrada, qualquer nome de arquivo pode ser empregado, mas aqui usaremos o nome de arquivo padrão: index.js
. A lista a seguir mostra um arquivo index.js
básico que será usado como ponto de partida para o nosso servidor:
const express = require('express')
const app = express()
const host = "myserver"
const port = 8080
app.listen(port, host, () => {
console.log(`Server ready at http://${host}:${port}`)
})
Não é necessário escrever código explícito para enviar um arquivo estático. O Express possui um middleware para esse fim, chamado express.static
. Se o seu servidor precisa enviar arquivos estáticos para o cliente, basta carregar o middleware express.static
no início do script:
app.use(express.static('public'))
O parâmetro public
indica o diretório onde estão armazenados os arquivos que o cliente pode solicitar. Os caminhos solicitados pelos clientes não devem incluir o diretório public
, mas apenas o nome do arquivo, ou o caminho para o arquivo relativo ao diretório public
. Para solicitar o arquivo public/layout.css
, por exemplo, o cliente faz uma solicitação para /layout.css
.
Saída formatada
Embora o envio de conteúdo estático seja descomplicado, o conteúdo gerado dinamicamente pode variar muito. A criação de respostas dinâmicas com mensagens curtas facilita o teste de aplicativos em seus estágios iniciais de desenvolvimento. Veja a seguir, por exemplo, uma rota de teste que simplesmente repassa ao cliente uma mensagem enviada pelo método HTTP POST
. A resposta pode apenas replicar o conteúdo da mensagem em texto simples, sem nenhuma formatação:
app.post('/echo', (req, res) => {
res.send(req.body.message)
})
Uma rota como essa é um bom exemplo para aprender a usar o Express e para fins de diagnóstico, onde uma resposta bruta enviada com res.send()
é suficiente. Mas um servidor útil deve ser capaz de produzir respostas mais complexas. Assim, vamos aprender a desenvolver esse tipo de rota mais sofisticado.
Nosso novo aplicativo, em vez de apenas mandar de volta o conteúdo da solicitação atual, mantém uma lista completa das mensagens enviadas nas solicitações anteriores de cada cliente e envia de volta a lista de cada cliente quando solicitado. Existe a opção de mandar uma resposta mesclando todas as mensagens, mas outros modos de saída formatada são mais apropriados, especialmente no caso de respostas mais elaboradas.
Para receber e armazenar mensagens de clientes enviadas durante a sessão atual, precisamos primeiro incluir módulos extras para lidar com cookies e dados enviados pelo método HTTP POST
. O único propósito do servidor de exemplo a seguir é registrar as mensagens enviadas via POST
e exibir as mensagens enviadas anteriormente quando o cliente emitir uma solicitação GET
. Portanto, existem duas rotas para o caminho /
. A primeira atende às solicitações feitas com o método HTTP POST
e a segunda atende às solicitações feitas com o método HTTP GET
:
const express = require('express')
const app = express()
const host = "myserver"
const port = 8080
app.use(express.static('public'))
const cookieParser = require('cookie-parser')
app.use(cookieParser())
const { v4: uuidv4 } = require('uuid')
app.use(express.urlencoded({ extended: true }))
// Array to store messages
let messages = []
app.post('/', (req, res) => {
// Only JSON enabled requests
if ( req.headers.accept != "application/json" )
{
res.sendStatus(404)
return
}
// Locate cookie in the request
let uuid = req.cookies.uuid
// If there is no uuid cookie, create a new one
if ( uuid === undefined )
uuid = uuidv4()
// Add message first in the messages array
messages.unshift({uuid: uuid, message: req.body.message})
// Collect all previous messages for uuid
let user_entries = []
messages.forEach( (entry) => {
if ( entry.uuid == req.cookies.uuid )
user_entries.push(entry.message)
})
// Update cookie expiration date
let expires = new Date(Date.now());
expires.setDate(expires.getDate() + 30);
res.cookie('uuid', uuid, { expires: expires })
// Send back JSON response
res.json(user_entries)
})
app.get('/', (req, res) => {
// Only JSON enabled requests
if ( req.headers.accept != "application/json" )
{
res.sendStatus(404)
return
}
// Locate cookie in the request
let uuid = req.cookies.uuid
// Client's own messages
let user_entries = []
// If there is no uuid cookie, create a new one
if ( uuid === undefined ){
uuid = uuidv4()
}
else {
// Collect messages for uuid
messages.forEach( (entry) => {
if ( entry.uuid == req.cookies.uuid )
user_entries.push(entry.message)
})
}
// Update cookie expiration date
let expires = new Date(Date.now());
expires.setDate(expires.getDate() + 30);
res.cookie('uuid', uuid, { expires: expires })
// Send back JSON response
res.json(user_entries)
})
app.listen(port, host, () => {
console.log(`Server ready at http://${host}:${port}`)
})
Mantivemos a configuração dos arquivos estáticos no topo, porque em breve será útil fornecer arquivos estáticos como layout.css
. Além do middleware cookie-parser
apresentado na lição anterior, o exemplo também inclui o middleware uuid
para gerar um número de identificação único que é passado como um cookie para cada cliente que envia uma mensagem. Se eles ainda não estiverem no diretório do servidor de exemplo, instale esses módulos com o comando npm install cookie-parser uuid
.
A matriz global messages
armazena as mensagens enviadas por todos os clientes. Cada item desta matriz consiste em um objeto com as propriedades uuid
e message
.
A novidade nesse script é o método res.json()
, usado ao final das duas rotas para gerar uma resposta no formato JSON com a matriz contendo as mensagens já enviadas pelo cliente:
// Send back JSON response
res.json(user_entries)
JSON é um formato de texto simples que permite agrupar um conjunto de dados em uma única estrutura associativa: ou seja, o conteúdo é expresso na forma de chaves e valores. O JSON é particularmente útil quando as respostas serão processadas pelo cliente. Usando esse formato, um objeto ou matriz JavaScript pode ser facilmente reconstruído no lado do cliente com todas as propriedades e índices do objeto original no servidor.
Como estamos estruturando todas as mensagens em JSON, recusamos as solicitações que não contenham application/json
em seu cabeçalho accept
:
// Only JSON enabled requests
if ( req.headers.accept != "application/json" )
{
res.sendStatus(404)
return
}
Uma solicitação feita com um comando curl
simples para inserir uma nova mensagem não será aceita, porque curl
, por padrão, não especifica application/json
no cabeçalho accept
:
$ curl http://myserver:8080/ --data message="My first message" -c cookies.txt -b cookies.txt Not Found
A opção -H "accept: application/json"
altera o cabeçalho da solicitação de forma a especificar o formato da resposta, que desta vez será aceita e respondida no formato especificado:
$ curl http://myserver:8080/ --data message="My first message" -c cookies.txt -b cookies.txt -H "accept: application/json" ["My first message"]
Para obter mensagens usando a outra rota, o processo é semelhante, mas desta vez usando o método HTTP GET
:
$ curl http://myserver:8080/ -c cookies.txt -b cookies.txt -H "accept: application/json" ["Another message","My first message"]
Modelos
As respostas em formatos como o JSON são convenientes para a comunicação entre programas, mas o objetivo principal da maioria dos servidores de aplicativos web é produzir conteúdo HTML para o consumo humano. Não é uma boa ideia incorporar código HTML dentro de um código JavaScript, pois a mistura de linguagens no mesmo arquivo torna o programa mais suscetível a erros e atrapalha a manutenção do código.
O Express pode trabalhar com diferentes mecanismos de modelo (template engines) que separam o HTML para o conteúdo dinâmico; a lista completa pode ser encontrada no site de mecanismos de modelo do Express. Um dos mecanismos de modelo mais populares é o Embedded JavaScript (EJS), que permite criar arquivos HTML com tags específicas para a inserção de conteúdo dinâmico.
Como outros componentes do Express, o EJS precisa ser instalado no diretório em que o servidor está sendo executado:
$ npm install ejs
Em seguida, o mecanismo EJS deve ser definido como o renderizador padrão no script do servidor (próximo ao início do arquivo index.js
, antes das definições de rota):
app.set('view engine', 'ejs')
A resposta gerada com o modelo é enviada ao cliente com a função res.render()
, que recebe como parâmetros o nome do arquivo do modelo e um objeto contendo valores que estarão acessíveis de dentro do modelo. As rotas usadas no exemplo anterior podem ser reescritas para gerar respostas em HTML, bem como em JSON:
app.post('/', (req, res) => {
let uuid = req.cookies.uuid
if ( uuid === undefined )
uuid = uuidv4()
messages.unshift({uuid: uuid, message: req.body.message})
let user_entries = []
messages.forEach( (entry) => {
if ( entry.uuid == req.cookies.uuid )
user_entries.push(entry.message)
})
let expires = new Date(Date.now());
expires.setDate(expires.getDate() + 30);
res.cookie('uuid', uuid, { expires: expires })
if ( req.headers.accept == "application/json" )
res.json(user_entries)
else
res.render('index', {title: "My messages", messages: user_entries})
})
app.get('/', (req, res) => {
let uuid = req.cookies.uuid
let user_entries = []
if ( uuid === undefined ){
uuid = uuidv4()
}
else {
messages.forEach( (entry) => {
if ( entry.uuid == req.cookies.uuid )
user_entries.push(entry.message)
})
}
let expires = new Date(Date.now());
expires.setDate(expires.getDate() + 30);
res.cookie('uuid', uuid, { expires: expires })
if ( req.headers.accept == "application/json" )
res.json(user_entries)
else
res.render('index', {title: "My messages", messages: user_entries})
})
Note que o formato da resposta depende do cabeçalho accept
encontrado na solicitação:
if ( req.headers.accept == "application/json" )
res.json(user_entries)
else
res.render('index', {title: "My messages", messages: user_entries})
Uma resposta em formato JSON é enviada apenas se o cliente fizer expressamente essa solicitação. Caso contrário, a resposta é gerada a partir do modelo index
. A mesma matriz user_entries
alimenta a saída em JSON e o modelo, mas o objeto usado como parâmetro para este último também possui a propriedade title: "My messages"
, que será usada como um título dentro do template.
Modelos HTML
A exemplo dos arquivos estáticos, os arquivos que contêm modelos HTML residem em seu próprio diretório. Por padrão, o EJS pressupõe que os arquivos de modelo estão no diretório views/
. No exemplo, um modelo chamado index
foi usado, por isso o EJS procura pelo arquivo views/index.ejs
. A lista a seguir é o conteúdo de um modelo views/index.ejs
simples que pode ser usado com o código de exemplo:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title><%= title %></title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="/layout.css">
</head>
<body>
<div id="interface">
<form action="/" method="post">
<p>
<input type="text" name="message">
<input type="submit" value="Submit">
</p>
</form>
<ul>
<% messages.forEach( (message) => { %>
<li><%= message %></li>
<% }) %>
</ul>
</div>
</body>
</html>
A primeira tag EJS especial é o elemento <title>
na seção <head>
:
<%= title %>
Durante o processo de renderização, essa tag especial será substituída pelo valor da propriedade title
do objeto passado como parâmetro para a função res.render()
.
A maior parte do modelo é composta de código HTML convencional; assim, o modelo contém o formulário HTML para o envio de novas mensagens. O servidor de teste responde aos métodos HTTP GET
e POST
para o mesmo caminho /
, por isso temos os atributos action="/"
e method="post"
na tag de formulário.
Outras partes do modelo são uma mistura de código HTML e tags EJS. O EJS tem tags para fins específicos dentro do modelo:
<% … %>
-
Inserts flow control. No content is directly inserted by this tag, but it can be used with JavaScript structures to choose, repeat, or suppress sections of HTML. Example starting a loop:
<% messages.forEach( (message) => { %>
<%# … %>
-
Define um comentário cujo conteúdo é ignorado pelo analisador. Ao contrário dos comentários escritos em HTML, esses comentários não são visíveis para o cliente.
<%= … %>
-
Insere o conteúdo escapado da variável. É importante escapar o conteúdo desconhecido para evitar a execução de código em JavaScript, o que abriria brechas para ataques de Cross-Site Scripting (XSS). Exemplo:
<%= title %>
<%- … %>
-
Insere o conteúdo da variável sem escapar.
A combinação de código HTML e tags EJS é evidente no trecho em que as mensagens do cliente são renderizadas como uma lista HTML:
<ul>
<% messages.forEach( (message) => { %>
<li><%= message %></li>
<% }) %>
</ul>
Neste trecho, a primeira tag <% … %>
inicia uma instrução forEach
que percorre todos os elementos da matriz message
. Os delimitadores <%
e %>
permitem controlar os trechos de HTML. Um novo item de lista HTML, <li><%= message %></li>
, será criado para cada elemento de messages
. Com essas mudanças, o servidor enviará a resposta em HTML quando uma solicitação como a seguinte for recebida:
$ curl http://myserver:8080/ --data message="This time" -c cookies.txt -b cookies.txt <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>My messages</title> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="stylesheet" href="/layout.css"> </head> <body> <div id="interface"> <form action="/" method="post"> <p> <input type="text" name="message"> <input type="submit" value="Submit"> </p> </form> <ul> <li>This time</li> <li>in HTML</li> </ul> </div> </body> </html>
A separação entre o código de processamento das solicitações e o código de apresentação da resposta torna o código mais limpo e permite que uma equipe divida o desenvolvimento do aplicativo entre diferentes especialistas. Um webdesigner, por exemplo, pode se concentrar nos arquivos de modelo em views/
e nas folhas de estilo relacionadas, que são fornecidas como arquivos estáticos armazenados no diretório public/
no servidor de exemplo.
Exercícios Guiados
-
Como o
express.static
deve ser configurado para que os clientes possam solicitar arquivos no diretórioassets
? -
Como o tipo de resposta, que é especificado no cabeçalho da solicitação, pode ser identificado em uma rota do Express?
-
Qual método do parâmetro de rota
res
(resposta) gera uma resposta no formato JSON a partir de uma matriz JavaScript chamadacontent
?
Exercícios Exploratórios
-
Por padrão, os arquivos de modelo do Express ficam no diretório
views
. Como essa configuração pode ser modificada para que os arquivos de modelo sejam armazenados emtemplates
? -
Suponha que um cliente recebe uma resposta em HTML sem título (ou seja,
<title></title>
). Depois de verificar o modelo EJS, o desenvolvedor encontra a tag<title><% title %></title>
na seçãohead
do arquivo. Qual é a causa provável do problema? -
Use tags de modelo EJS para escrever uma tag HTML
<h2></h2>
com o conteúdo da variável JavaScripth2
. Essa tag deve ser renderizada somente se a variávelh2
não estiver vazia.
Resumo
Esta lição trata dos métodos básicos fornecidos pelo Express.js para gerar respostas estáticas e formatadas, mas dinâmicas. Não é difícil configurar um servidor HTTP para os arquivos estáticos, e o sistema de modelos EJS constitui uma maneira fácil de gerar conteúdo dinâmico a partir de arquivos HTML. Esta lição aborda os seguintes conceitos e procedimentos:
-
Uso do
express.static
para respostas de arquivos estáticos. -
Como criar uma resposta correspondente ao tipo de conteúdo definido no cabeçalho da solicitação.
-
Respostas estruturadas em JSON.
-
Uso de tags EJS em modelos baseados em HTML.
Respostas aos Exercícios Guiados
-
Como o
express.static
deve ser configurado para que os clientes possam solicitar arquivos no diretórioassets
?É preciso adicionar uma chamada para
app.use(express.static('assets'))
no script do servidor. -
Como o tipo de resposta, que é especificado no cabeçalho da solicitação, pode ser identificado em uma rota do Express?
O cliente define os tipos aceitáveis no campo de cabeçalho
accept
, que é mapeado para a propriedadereq.headers.accept
. -
Qual método do parâmetro de rota
res
(resposta) gera uma resposta no formato JSON a partir de uma matriz JavaScript chamadacontent
?O método
res.json()
:res.json(content)
.
Respostas aos Exercícios Exploratórios
-
Por padrão, os arquivos de modelo do Express ficam no diretório
views
. Como essa configuração pode ser modificada para que os arquivos de modelo sejam armazenados emtemplates
?O diretório pode ser definido nas configurações iniciais do script com
app.set('views', './templates')
. -
Suponha que um cliente recebe uma resposta em HTML sem título (ou seja,
<title></title>
). Depois de verificar o modelo EJS, o desenvolvedor encontra a tag<title><% title %></title>
na seçãohead
do arquivo. Qual é a causa provável do problema?A tag
<%= %>
deve ser usada para circunscrever o conteúdo de uma variável, como em<%= title %>
. -
Use tags de modelo EJS para escrever uma tag HTML
<h2></h2>
com o conteúdo da variável JavaScripth2
. Essa tag deve ser renderizada somente se a variávelh2
não estiver vazia.<% if ( h2 != "" ) { %> <h2><%= h2 %></h2> <% } %>