Escolar Documentos
Profissional Documentos
Cultura Documentos
io
Leonan Lupi
Esse livro está à venda em http://leanpub.com/chat-com-node-js-e-socket-io
Esse é um livro Leanpub. A Leanpub dá poderes aos autores e editores a partir do processo de
Publicação Lean. Publicação Lean é a ação de publicar um ebook em desenvolvimento com
ferramentas leves e muitas iterações para conseguir feedbacks dos leitores, pivotar até que você
tenha o livro ideal e então conseguir tração.
Introdução . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
Criando Ambiente . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
Criando o ambiente no windows . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
Criando o ambiente no linux . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
Criando o ambiente no MAC OSX . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
Principais Tecnologias . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
BackEnd . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
FrontEnd . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
APIs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
FrontEnd . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 80
Iniciando Projeto FrontEnd . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 80
CONTEÚDO
Configurando gulp . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81
Marcando aside - chatbar . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 86
Marcando section - chatbox . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88
Estilizando aside - chatbar . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90
Estilizando section - chatbox . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94
BackEnd . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 98
Configurando Express . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 98
Criando web API . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 100
Criando Servidor Socket . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 106
Conclusão . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 139
Introdução
Este ebook terá como objetivo ensinar a criar uma plataforma de chat utilizando as tecnologias
Node.js e Socket.io.
Nesta introdução mostraremos o que desenvolveremos e como faremos.
Nos primeiros capítulos apresentaremos os conceitos técnicos para aplicarmos da melhor forma.
Ensinaremos a montar o ambiente e passaremos alguns conceitos básicos como: conceitos de API,
como trabalhar com as APIs e mostraremos alguns padrões de retorno como json.
Além de mostrarmos o padrão de retorno, apresentaremos a linguagem e como se deu seu
surgimento.
Explicaremos sobre a arquitetura que utilizaremos e conforme avançarmos, comentaremos porque
estamos utilizando determinada arquitetura e outras informações que forem importantes.
Tudo que desenvolvermos, faremos uma introdução conceitual, antes de começarmos a praticar.
No decorrer dos capítulos comentaremos sobre cada tecnologia utilizada.
Ao final vocês terão um conjunto de informações importantes, de modo que consigam desenvolver
uma aplicação que seja realmente funcional em produção.
Instalando Noje.js
Você deverá instalar o Node.js para acompanhar o projeto.
Quando baixar o Node.js, a ferramenta gerenciadora de pacotes do Node, que se chama npm, é
instalada. Fique atento ao tipo de arquitetura do sistema operacional que você está utilizando, se é
32 ou 64 bits.
Há duas versões: LTS e Current. A escolha da versão a ser usada, deve ser feita a partir dos seguintes
parâmetros:
Utilize a versão LTS, que está no mercado há mais tempo e que já possui uma quantidade maior de
usuários, bem como bibliotecas que utilizam esta versão.
A versão current é a que possui as novas features da linguagem e todas as atualizações/melhorias
da plataforma, porem conta com uma quantidade menor de usuarios utilizando pelo fato de ser a
versão mais nova.
Ambas poderam ser utilizadas durante o processo de produção de nosso chat. Atenção apenas a
versão do Node.js que deverá ser maior que 4.0 para que tudo funcione perfeitamente.
Após o download, faça a instalação, aceite os termos, escolha o local da instalação e finalize o
processo.
Para confirmar se está tudo funcionando, abra o terminal e rode os comandos: nodejs -v ou node
-v e npm -v.
Criando Ambiente 3
nodejs -v
ou
node -v
npm -v
As versões deverão ser retornadas. Se obtiver este resultado, está tudo instalado e configurado.
Como terminal, sugerimos o uso do Git Bash ou do Cmder. Você também poderá utilizar o Powershell
ou CMD, que são nativos do Windows.
Instalando MongoDB
Para a instalação do MongoDB, acesse o site abaixo:
https://www.mongodb.com/
Não esqueça de dar start no serviço sempre que precisar utilizar. Note também a presença do
parâmetro dbpath que é necessário quando startamos o MongoDB pela primeira vez. Esse parâmetro
tem como objetivo dizer ao MongoDB onde deverá armazenar os arquivos gerados pela criação de
banco de dados, coleções e outros e só deve ser utilizado na primeira vez que startar o serviço do
MongoDB.
sudo su
apt-get update
apt-get upgrade
Ao utilizar o Linux, você precisará instalar outro pacote para evitar alguns erros de compilação de
alguns pacotes que utilizará durante o desenvolvimento.
Instale o pacote Build Essential:
O comando executável do Node.js no Linux será nodejs. Se precisar rodar um script ou executar
algum programa com node, você precisará rodar o comando nodejs no terminal.
Para saber as versões, rode:
nodejs -v
ou
node -v
npm -v
Caso seja necessário, atualize a versão do Node e do npm, para que possa acompanhar o conteúdo,
utilizando as mesmas versões.
Rode o comando abaixo
npm install -g n
sudo n stable
Instalando MongoDB
Acesse o site abaixo e siga o passo a passo para a sua distribuição.
https://docs.mongodb.com/manual/tutorial/install-mongodb-on-ubuntu/
Criando Ambiente 6
Estes comandos serão responsáveis pela instalação do MongoDB, bem como outras ferramentas que
são utilizadas entre o Mongo e o sistema operacional.
Sempre que for utilizar o serviço do MongoDB, você terá que iniciar o serviço, rodando o comando
abaixo:
xcode-select install
Com este comando você verifica se o sistema foi instalado. Se houver algum problema, haverá uma
mensagem informando.
Atualizando o Node e o Npm, para a última versão:
npm install n -g
sudo n stable
Para conferir as versões e saber se está tudo funcionando corretamente rode os seguintes comandos
no terminal:
node -v
npm -v
MongoDB
Criando Ambiente 8
Para este projeto indicamos o uso do Sublime Text ou Visual Studio Code, para codificar.
Mas esteja livre para escolher o editor de sua preferência.
Principais Tecnologias
Neste capítulo apresentaremos a você as principais tecnologias que serão utilizadas no backend e
frontend da aplicação:
• Node.js
• Express.js
• MongoDB
• SASS
• Javascript
BackEnd
Node.js
Esta tecnologia consegue manipular milhares de conexões, simultâneas, em uma mesma máquina.
É o que você necessita para o desenvolvimento do chat. Lógico que, se o chat for muito grande, você
deve reavaliar se apenas uma máquina será suficiente, mas comparado à outras linguagens, o Node
está muito à frente em relação a esta capacidade.
Com outras linguagens, você perceberá que não terá a mesma facilidade em trabalhar em tempo real
e para escalar o projeto.
Se comparar o Java com o Node, você verá como é mais fácil trabalhar com o Node e perceberá que
não será necessário escalar de forma contínua o projeto como um todo. Claro que toda aplicação
deve ser analisada, pontualmente.
Em linhas gerais, o Node tem mais recursos.
Para testar o Node, siga os passos abaixo:
Principais Tecnologias 10
npm init -y
Após rodar o comando, você obterá um arquivo chamado package.json na pasta raiz.
Conteúdo do arquivo package.json
1 {
2 "name": "chat",
3 "version": "1.0.0",
4 "description": "",
5 "main": "index.js",
6 "scripts": {
7 "test": "echo \"Error: no test specified\" && exit 1"
8 },
9 "author": "",
10 "license": "ISC"
11 }
Após a obtenção deste arquivo, crie um outro arquivo chamado index.js, na pasta raiz do projeto.
Veja o conteúdo deste arquivo:
Depois de adicionar este código ao arquivo index.js, execute o arquivo com o comando abaixo:
Principais Tecnologias 11
node index.js
Após rodar o comando, caso não tenha nenhum erro, você terá acesso a aplicação através do endereço
http://localhost:3000, em seu browser.
Se obtiver como resultado o Hello World, está tudo correto com o ambiente e com o Node.
Express.js
O Express.js é um framework web rápido, flexível e minimalista, para Node.js. Ele é o principal
framework de mercado e o mais utilizado em ambiente de produção.
Acesse o link: http://expressjs.com/pt-br/starter/installing.html
Para instalar este framework, rode os comandos abaixo, no terminal:
Concluída a instalação, abra o arquivo index.js novamente e comente todo código existente. Faremos
o mesmo processo anterior, porém utilizando o Express.
Assim, você terá o mesmo servidor funcionando na porta 3000. Execute o arquivo com o node e
acesse novamente o endereço abaixo, no browser:
http://localhost:3000
O Express permitiu criar e manipular um servidor web com poucas linhas de código. O mais
importante é que você pode criar rotas facilmente com este framework, pois ele disponibiliza este
recurso nativamente.
Principais Tecnologias 12
MongoDB
MongoDB é uma aplicação de código aberto, de alta performance, sem esquemas, orientado
a documentos. Foi escrito na linguagem de programação C++. Além de orientado a
documentos, é formado por um conjunto de documentos JSON. Muitas aplicações podem,
dessa forma, modelar informações de modo muito mais natural, pois os dados podem ser
aninhados em hierarquias complexas e continuar a ser indexáveis e fáceis de buscar. -
Wikipédia
• Alta escalabilidade
• Muita velocidade
O MongoDB será a infra-estrutura para guardar informações. Logo, você trabalhará com o Express
para receber e manipular as informações e, em seguida as gravará no MongoDB.
Para este procedimento você utilizará o mongoose.
http://mongoosejs.com/
Esta ferramenta tem como principal característica ajudar a manipular o MongoDB, diretamente do
Node.js. Desta forma, você conseguirá fazer praticamente tudo utilizando a mongoose, com bastante
abstração.
Com o mongoose você conseguirá:
FrontEnd
SASS
Atualmente, está em alta a utilização de pré-processadores. Trata-se de uma linguagem por trás do
CSS, como:
Principais Tecnologias 13
SASS e LESS
Cada um tem sua característica própria, vantagens e desvantagens, como qualquer outra ferramenta.
O principal intuito destes pré-processadores, é tornar o trabalho com o CSS mais organizado e
estruturado.
Com estes pré-processadores você pode:
Javascript
O Javascript tem uma característica muito útil que é trabalhar de forma assíncrona. Isso significa
que você não precisa esperar uma requisição terminar para começar outra. Além disso, você pode
implementá-lo de várias formas, como:
• Paradígma estrutural
• Orientação a objeto, que é o mais utilizado, atualmente
• Implementar na forma funcional
Com todas estas informações, conclui-se tratar de uma linguagem muito versátil e com a ajuda
do HTML, ele consegue implementar várias APIs existentes, como: websocket e notification, por
exemplo.
APIs
Nesse capítulo você entenderá melhor sobre o que são APIs, a relação que elas possuem com JSON
e também sobre Websockets.
API significa Application Program Interface, ou seja, ela fornece uma interface para que sotwares
internos ou externos consigam consumir seus serviços. Um exemplo muito simples de API pode ser
dado sobre uma consulta de CEP no sites dos Correios. Basicamente, você acessa uma URL (que nesse
caso pode ser chamada de endpoint) onde passando o CEP desejado, em um formato já especificado,
você receberá as informações de um endereço. Perceba que nesse caso, o software que está realizando
a consulta do cep está consumindo um serviço.
Um ponto importante a ser considerado, é a relação que muitas pessoas fazem entre API e JSON
(JavaScript Object Notation), ou seja, acabam confundindo que JSON é a própria API. JSON é apenas
o formato de dados de como as informações que serão disponibilizadas, ou seja, posso ter uma API
que retorna um XML ou até mesmo texto puro sem nenhuma formatação.
Exemplo de XML
Exemplo de JSON
{"menu": {
"id": "file",
"value": "File",
"popup": {
"menuitem": [
{"value": "New", "onclick": "CreateNewDoc()"},
{"value": "Open", "onclick": "OpenDoc()"},
{"value": "Close", "onclick": "CloseDoc()"}
]
}
}}
APIs 15
Por motivos óbvios, podemos sempre optar por deixar uma API pública ou privada. No caso dela ser
privada, provavelmente será necessário enviarmos credenciais corretas no momento da requisição.
APIs não foram criadas apenas para fornecer consultas de informações, mas também para realizar
transações como:
• consultar dados
• adicionar
• editar
• remover
Um dos protocolos mais utilizados para Web APIs é o protocolo HTTP, utilizando REST.
Vale lembrar que o framework Express, tem total suporte a JSON e a APIs.
Veja a alteração na rota criada no capítulo anterior. Ao invés de adicionar a mensagem Hello World
será adicionado um menu no formato JSON:
app.get('/',(req,res) => {
res.send("Hello World")
})
app.get('/',(req,res) => {
res.json({
menu: {
id: 1,
value: "1",
popup:{
}
}
})
})
app.listen(3000, () => {
console.log("Server started")
})
WebSockets
WebSocket tem o objetivo de permitir a comunicação bidirecional (envie e recebe mensagens) entre
canais através de TCP/IP. Ele não faz parte especificamente do HTML 5, porém, é um protocolo
padronizado pela IETF, bem como pela W3C. Basicamente, uma conexão persistente é criada entre
o cliente e o servidor, e ambas as partes podem enviar e receber dados a qualquer momento.
O sistema de URL é, totalmente, diferente do HTTP. No protocolo http a url começa com http ou
https para conexões seguras, já no protocolo websocket, a url começa com ws.
Para requisições não criptografadas utiliza-se ws e, para criptografadas wss.
Todos os navegadores convencionais, inclusive o Internet Explorer, a partir da versão 10, já suportam
trabalhar com Websockets, nativamente.
Implementaremos um exemplo prático, para que você consiga entender melhor como funciona o
processo em tempo real e websockets.
1 <!DOCTYPE html>
2 <html lang="en">
3 <head>
4 <meta charset="UTF-8">
5 <title>Title</title>
6 </head>
7 <body>
8 <script>
9 console.log('Hello World')
10 </script>
11 </body>
12 </html>
http-server
Tendo este recurso, já podemos começar a trabalhar com Websockets. Não vamos instalar nenhuma
biblioteca. Utilizaremos apenas a API padrão que já vem no HTML5 e vamos consumir um servidor
Websocket público e que é, somente, para teste.
Com base nisso, vamos receber e exibir mensagens.
<script>
(function() {
var URI = 'ws://echo.websocket.org/'
var websocket = null
var init = function() {
websocket = new WebSocket(URI)
websocket.onopen = function(event) {
onOpen(event)
}
}
function onOpen(e) {
console.log('Connected')
sendMessage('This is my first message from WS')
}
APIs 18
function sendMessage(str) {
websocket.send(str)
}
init()
})()
</script>
Primeiro, criamos uma função auto-executável e definimos a URI com endereço do servidor
websocket para teste e a variável websocket, como nula.
Em seguida, atribuímos uma função para a variável init. Nesta função instanciamos o Websocket e
definimos que ele executasse a função onOpen no evento de onOpen. A função onOpen executa um
console.log e envia uma mensagem para o servidor, através da função sendMessage que definimos,
logo abaixo.
A função sendMessage utiliza o método send, para enviar a mensagem que passamos como
parâmetro. Quando acessar o server, na porta 8080 e inspecionar o console, o log será mostrado
e se acessar a aba network, na seção WS, poderá ver a mensagem enviada para o servidor.
Para completar o processo, podemos adicionar o evento onMessage para receber a mensagem, em
tempo real.
<script>
(function() {
var URI = 'ws://echo.websocket.org/'
var websocket = null
var init = function() {
websocket = new WebSocket(URI)
websocket.onopen = function(event) {
onOpen(event)
}
websocket.onmessage = function(event) {
onMessage(event)
}
}
function onOpen(e) {
console.log('Connected')
sendMessage('This is my first message from WS')
}
function sendMessage(str) {
websocket.send(str)
}
function onMessage(event) {
console.log('received')
APIs 19
console.log(event.data)
}
init()
})()
</script>
Após a criação da função sendMessage, adicionamos o evento na função init e depois criamos a
função onMessage.
Abra o navegador e inspecione, novamente. Terá a mensagem sendo enviada em tempo real no
console.
Existem outras ações, pré-definidas, para serem utilizadas. Assim como temos a ação onMessage e
onOpen, temos onClosed. Basicamente, criamos a ação e depois definimos a função, para atender
determinada ação.
1 <!DOCTYPE html>
2 <html lang="en">
3 <head>
4 <meta charset="UTF-8">
5 <title>Document</title>
6 </head>
7 <body>
8 <div id="wrapper">
9 <ul id="messages">
10 </ul>
11 </div>
12 <script>
13 (function() {
14 var URI = 'ws://echo.websocket.org/'
15 var websocket = null
16 var init = function() {
17 websocket = new WebSocket(URI)
18 websocket.onopen = function(event) {
19 onOpen(event)
20 }
21 websocket.onmessage = function(event) {
22 onMessage(event)
23 }
24 websocket.onclose = function(event) {
25 onClose(event)
26 }
27 }
28 function onOpen(e) {
APIs 20
29 console.log('Connected')
30 sendMessage('This is my first message from WS')
31 }
32 function sendMessage(str) {
33 websocket.send(str)
34 }
35 function onMessage(event) {
36 console.log('Reciveid')
37 console.log(event.data)
38 var msg = document.getElementById('messages')
39 var item = document.createElement('li')
40 item.innerHTML = event.data
41 msg.appendChild(item)
42 websocket.close()
43 }
44 function onClose(event) {
45 console.log('Disconnected')
46 }
47 init()
48 })()
49 </script>
50 </body>
51 </html>
Para ver toda mudança funcionando não esqueça de parar o servidor e subir novamente com o
comando http-server.
Adicionamos a ação onclose e depois definimos a função onClose, que simplesmente faz um log
no console. Observe que adicionamos uma tag ul do HTML, para receber a mensagem quando ela é
enviada. Resgatamos o elemento ul e depois, com o javascript, criamos um elemento li e adicionamos
a mensagem enviada pelo websocket. Tudo isso, antes de fecharmos a conexão.
Observe que mostramos que podemos trabalhar com Websockets e com javascript em conjunto com
o HTML, de forma simples.
Com este exemplo, criamos a conexão, enviamos uma mensagem, recebemos esta mensagem e
imprimimos na tela, como se fosse um processo completo do chat, porém muito simples e manual,
somente para exemplificar.
Configurações iniciais do projeto
Introdução
A partir de agora, começaremos a trabalhar na prática.
O próximo passo será desenvolver o ambiente de administração do projeto. Cadastraremos algumas
configurações que serão utilizadas futuramente, no chat.
Criaremos uma estrutura muito simples, procurando utilizar tudo o que possuímos, sempre de forma
simples.
Utilizaremos:
• Handlebars
• Express
• MongoDB
Além dos recursos citados acima, utilizaremos todos os recursos que citamos, anteriormente. Nos
próximos capítulos utilizaremos estes dados, acessando-os através de uma API para o frontend.
Iniciando Projeto
Para iniciar o projeto, crie uma pasta com o nome do projeto e, dentro da pasta, rode o comando de
inicialização do npm. Chamamos o projeto de chatschool.
npm init -y
1 {
2 "name": "chatschool",
3 "version": "1.0.0",
4 "description": "chatschool",
5 "main": "app.js",
6 "scripts": {
7 "test": "echo \"Error: no test specified\" && exit 1"
8 },
9 "keywords": [
10 "chat",
11 "socket.io",
12 "express"
13 ],
14 "author": "",
15 "license": "ISC"
16 }
Com este arquivo criado, você começará a trabalhar com as dependências do projeto.
Veja a lista de ferramentas que deverá instalar para facilitar o desenvolvimento da aplicação:
Comando Descrição
npm install express --save Framework node.js
npm install body-parser --save Trasnforma um objeto json para o formato javascript
npm install express-hbs --save Template engine que utilizará no projeto
npm install morgan --save Executa logs de toda requisição http, no console
npm install mongoose --save Permite trabalhar com banco de dados MongoDB
npm install express-validator --save Valida os dados da requisição
npm install express-session --save Cria seções para a aplicação
npm install method-override --save Permite o uso de verbos HTTPs que o navegador não dá
suporte.
Observe que, além de instalar, você está salvando as dependências no arquivo package.json,
utilizando o parâmetro –save.
Após a instalação de todas as dependências, o ambiente está pronto para o iniciar o projeto.
A página de admin será bem simples e utilizaremos o protocolo HTTP para fazer a autenticação. O
express será utilizado para auxiliar na manipulação dos dados transmitidos através da requisição.
Através das requisições a aplicação será direcionada para pontos específicos. Através deste procedi-
Configurações iniciais do projeto 23
• riação
• edição
• remoção de dados na área administrativa.
• public
• src.
A pasta public será utilizada para armazenar arquivos estáticos e terá acesso direto pelo usuário.
Veja a estrutura da pasta public:
• chatschool
– public
* images
* js
* scss
A pasta src abrigará toda lógica da aplicação e terá a seguinte estrutura interna:
• chatschool
– src
* configs
* schemas
* routes
* services
* views
Configurações iniciais do projeto 24
Explicando estrutura
Pasta Descrição
configs Configurações da aplicação
routes Rotas de entrada ou endpoints para os usuários
schemas Gerenciador de schemas para cada projeto
services Onde estará toda lógica da aplicação
views Onde estarão todas visualizações, ou frontend, do projeto
• partials
• layouts.
Dentro da pasta partials teremos os templates específicos para cada setor e dentro de layouts teremos
o código padrão de todos os templates.
Dentro da pasta routes teremos outras subpastas e arquivos que serão seguidas na pasta services,
mas criaremos conforme a necessidade dos conteúdos seguintes.
Explicando estrutura
Configuração Descrição
app.set(‘port’, 9000) Define a porta de desenvolvimento
app.set(‘views’, path.join(…)) Configura o caminho das views
app.set(‘view engine’, ‘hbs’) Define qual será a template engine utilizada
app.use(morgan(‘dev’)) Configura o uso da ferramenta morgan
app.use(bodyParser.json()) Configura o uso da ferramenta bodyParse e a
linguagem utilizada
app.use(bodyParser.urlencoded({ extend: false })) Configura o uso da ferramenta bodyParse utilizando
urlencoded com extensão false
app.use(methodOverride(‘_method’)) Define o método override via query string
app.use(expressSession({…}) Configura os dados de sessão
app.use(expressValidator()) Configura as validações
app.engine(‘hbs’, hbs.express4({…}) Define template padrão e estrutura de pastas para
layouts
Após criar este arquivo com as configurações de desenvolvimento, inicie a aplicação passando estes
dados através do module.exports.
Veja o código do arquivo app.js, que é o arquivo de inicialização do projeto:
Setamos as constantes
Constante Descrição
express recebe a biblioteca do Framework
path recebe o caminho atual do arquivo
app recebe a inicialização de uma instância do express
env busca pela constante NODE_ENV, caso não encontre, determina o ambiente como
development
Um detalhe importante que deve ser considerado é o fato de termos a possibilidade de criarmos
ambientes diferentes. Atualmente, existe a possibilidade de termos três ambientes: teste, desenvol-
vimento e produção. Estamos separando cada configuração de ambiente em um arquivo diferente,
para que possamos trabalhar de forma mais organizada.
Depois, basta fazer o carregamento do ambiente e passar a instância app, para que possa iniciar a
aplicação.
1 require(env)(app)
Caso rode o arquivo app.js, no terminal, já terá o servidor rodando e a palavra server, como resposta
no terminal.
node app.js
E, por último, você pode configurar um endpoint para poder acessar no browser e ter uma mensagem
de confirmação de que o servidor já está funcionando e podendo ser acessado pela porta 9000.
14
15 app.get('/', (req, res) => {
16 res.send("School of Net")
17 })
Para ter certeza do funcionamento, basta executar o arquivo app.js, novamente, e acessar http://localhost:9000,
no navegador, para ter a aplicação rodando.
require('./src')(app)
Crie um arquivo chamado index.js, dentro da pasta src. Veja o conteúdo do arquivo index.js:
Desta forma, você está dizendo que, quando o endpoint for acessado, irá requerer o arquivo index.js
da pasta routes/main, que ainda não criou. Crie este arquivo e adicione o seguinte código:
Assim, você está atribuindo valores para as constantes: express e router. Para a constante express
carregamos o framework e para a constante router, atribuímos uma instância express.Router.
Depois, basta configurar a requisição do endpoint, para chamar um serviço que está na pasta
services/main/index.js. Este arquivo não foi criado, você deverá fazê-lo agora. Veja o conteúdo
deste arquivo:
Configurações iniciais do projeto 29
Finalmente, temos o mesmo resultado anterior, porém de forma estruturada e organizada. Este é o
formato que utilizaremos na aplicação.
Para não precisar ficar parando o servidor e subindo novamente, a cada atualização feita no
desenvolvimento, instalaremos o nodemon.
Todas estas instalações são importantes para que possa trabalhar de forma mais organizada e
profissional. Assim, você terá ferramentas que trabalharão por você em tarefas muito importantes
em um projeto, cada uma com sua atuação.
Descrevendo processo
Observe que, em primeiro lugar, carregamos todas as ferramentas que instalamos e atribuímos às
suas respectivas constantes.
Em seguida, definimos uma constante chamada path, com todas os caminhos relativos que
precisaremos para configurar nossas tasks.
Configurações iniciais do projeto 31
1 const paths = {
2 fontsSrc: 'public/fonts/',
3 htmlSrc: 'src/views/',
4 sassSrc: 'public/scss/',
5 jsSrc: 'public/js/',
6 imgSrc: 'public/images/',
7
8 buildDir: 'build/',
9 revDir: 'build/rev/',
10 distDir: 'dist/'
11 }
Definimos uma variável chamada onError, para capturar todos os erros do processo. Desta forma,
acessaremos os erros e mostraremos, sempre que for necessário:
Atribuímos à variável initServer uma função que executa o script app.js utilizando a biblioteca
nodemon. O arquivo app.js é o arquivo inicial que estávamos rodando manualmente no terminal.
Configuramos a extensão (ext) no nodemon, para que a tarefa não confunda com algum outro
arquivo que possa ter o mesmo nome.
Configuramos o evento on restart, para que execute o arquivo, disparando um livereload e uma
notificação.
O próximo passo é configurar todas as tarefas relacionadas à criação dos recursos do frontend:
1 gulp.task('build-html', () => {
2 return gulp
3 .src(paths.htmlSrc.concat('**/*.hbs'))
4 .pipe(gulp.dest(paths.buildDir.concat('/views')))
5 .pipe(livereload())
6 })
7
8 gulp.task('build-css', () => {
9 return gulp
10 .src(paths.sassSrc.concat('**/*.scss'))
11 .pipe(sass({
12 includePaths: require('node-neat').includePaths,
13 style: 'nested',
14 onError: function(){
15 console.log('SASS ERROR!')
16 }
17 }))
18 .pipe(plumber({ errorHandler: onError }))
19 .pipe(gulp.dest(paths.buildDir.concat('/css')))
20 .pipe(livereload())
21 })
22
23 gulp.task('build-js', () => {
24 return gulp
25 .src(paths.jsSrc.concat('*.js'))
26 .pipe(plumber({ errorHandler: onError }))
27 .pipe(changed(paths.buildDir.concat('/js')))
28 .pipe(gulp.dest(paths.buildDir.concat('/js')))
29 .pipe(livereload())
30 })
31
Configurações iniciais do projeto 33
32 gulp.task('build-images', () => {
33 return gulp
34 .src(paths.imgSrc.concat('**/*.+(png|jpeg|gif|jpg|svg)'))
35 .pipe(changed(paths.buildDir.concat('/images')))
36 .pipe(gulp.dest(paths.buildDir.concat('/images')))
37 .pipe(livereload())
38 })
39
40 gulp.task('build-fonts', () => {
41 return gulp
42 .src(paths.fontsSrc.concat('**/*.*'))
43 .pipe(gulp.dest(paths.buildDir.concat('/fonts')))
44 .pipe(livereload())
45 })
Repare que, criamos os nomes das tarefas de uma forma muito intuitiva. Criamos as seguintes tarefas:
• build-html
• build-css
• build-js
• build-images
• build-fonts.
Agora, você pode desenvolver os códigos css, html, js, images e fontes, que o gulp compilará tudo
para você em uma pasta chamada build. Qualquer alteração que fizer será assistida pelo gulp e ele
se encarregará de compilar tudo, da melhor maneira possível, para o projeto.
Depois de criar as tarefas, você deve executá-las de forma automática.
Primeiro, crie uma tarefa chamada build que executa todas as tarefas anteriores. Veja a tarefa:
Depois de executar todas as tarefas, ele executa a função initServer para reiniciar a aplicação com
as alterações já compiladas pelo gulp.
Ainda não executamos a tarefa build, para que todo este processo seja executado. Para isso, criaremos
uma outra tarefa chamada watch e depois, executamos a tarefa build e watch.
Tarefa watch:
Configurações iniciais do projeto 34
1 gulp.task('watch', () => {
2 gulp.watch(['src/views/**/*.hbs'], ['build-html']);
3 gulp.watch('public/scss/**', ['build-css']);
4 gulp.watch(paths.jsSrc + '**/*.js', ['js']);
5 gulp.watch(paths.imgSrc + '**/*.+(png|jpeg|jpg|svg)', ['build-images']);
6 })
Criadas as tarefas build e watch, basta configurar a variável de ambiente em uma constante env.
Configuramos esta variável para que possamos executar tarefas diferentes, em diferentes ambientes.
Por enquanto temos só o ambiente development sendo testado, no futuro poderemos criar tarefas
para outros ambientes.
Finalmente, vem a execução de todos os processos automatizados, quando executamos a tarefa
default do gulp, no terminal:
gulp default
Ou somente:
gulp
Após a execução desta tarefa, teremos todos os recursos compilados e o servidor rodando, da mesma
forma anterior. Caso tenha algum problema, com alguma biblioteca, você pode excluir a pasta
node_modules, atualizar o npm e instalar tudo novamente.
# Atualizando npm
npm install npm -g
npm install
1 <!DOCTYPE html>
2 <html lang="en">
3 <head>
4 <meta charset="UTF-8">
5 <title>{{ title }}</title>
6 <meta name="viewport" content="width=device-width, initial-scale=1"/>
7 <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-boot\
8 strap/3.3.7/css/bootstrap.min.css">
9 <link rel="stylesheet" href="/css/app.css">
10 </head>
11 <body>
12
13 {{> main/header }}
14
15 <main class="content-wrapper">
16 {{{ body }}}
17 </main>
18
19 {{> main/footer }}
20
21 <script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/js/b\
22 ootstrap.min.js"></script>
23 <script src="/js/app.js"></script>
24 </body>
25 </html>
No layout padrão temos uma estrutura HTML padrão e já aproveitamos para trabalhar com variáveis
handlebars({{ title }}) e chamamos alguns templates partials ({{> main/header }} e {{> main/footer
}}).
Observe que já carregamos o CSS e o JS do Bootstrap, no layout padrão. Os links são do site: https:
//cdnjs.com/libraries/twitter-bootstrap/, onde você pode escolher qual versão utilizar.
Configurações iniciais do projeto 36
Carregamos o arquivo css/app.css e também o js/app.js. Estes arquivos ainda não foram criados.
Crie estes arquivos com os seguintes conteúdos:
Conteúdo public/scss/app.scss
1 body {
2 background: red;
3 }
Conteúdo public/js/app.js
1 (function () {
2 console.log('Its ok')
3 })
Crie dentro da pasta /views/partials, uma outra pasta chamada main, onde criaremos os layouts
parciais. Crie dois arquivos: header.hbs e footer.hbs.
Conteúdo header.hbs:
1 <h1>Header</h1>
Conteúdo footer.hbs:
1 <h1>Footer</h1>
Porém, com a nova estrutura, devemos fazer esta chamada de outra forma. Veja a maneira correta
e altere seu arquivo:
Configurações iniciais do projeto 37
Precisamos criar a pasta src/views/main e o arquivo index.hbs, dentro da pasta views. Veja o
conteúdo do arquivo index.hbs:
1 <h1>School of Net</h1>
Notem que estamos tendo o mesmo resultado, mas agora sendo chamado um arquivo HTML e não
mais uma mensagem direta.
Com esta estrutura, estamos passando a informação title para que seja renderizada como handlebar.
Podemos enviar mais informações, caso seja necessário. Como estamos utilizando apenas a variável
title em nosso template, passamos apenas ela.
Após todas estas alterações e implementações, basta rodar no terminal a tarefa gulp, novamente.
gulp default
Ou somente:
gulp
Você poderá acessar o navegador e terá acesso às alterações. Ainda existe um erro que devemos
configurar, para que tenhamos todas as alterações compiladas.
No arquivo src/configs/env/development.js adicione o seguinte código:
O código acima faz com que os assets sejam reconhecidos, fazendo com que as alterações de css e js
sejam aplicadas ao layout padrão.
Configurações iniciais do projeto 38
1 <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js"></sc\
2 ript>
3 <script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/js/boots\
4 trap.min.js"></script>
5 <script src="/js/app.js"></script>
1 <header id="header">
2 <nav class="navbar navbar-inverse">
3 <div class="container-fluid">
4 <div class="nav-header">
5 <button type="button" class="navbar-toggle collapsed" data-toggle="c\
6 ollapse" data-target="#nav">
7 <span class="sr-only"></span>
8 <span class="icon-bar"></span>
9 <span class="icon-bar"></span>
10 <span class="icon-bar"></span>
11 </button>
12 <a href="/" class="navbar-brand">ChatSchool</a>
13 </div>
14 <div class="collapse navbar-collapse" id="nav">
15 <ul class="nav navbar-nav">
16 <li class="active">
Criando Área Administrativa 40
17 <a href="/">Home</a>
18 </li>
19 <li class="dropdown">
20 <a data-toggle="dropdown" class="dropdown-toggle" href="\
21 ">
22 Users
23 <span class="caret"></span>
24 </a>
25 <ul class="dropdown-menu">
26 <li>
27 <a href="/users">List</a>
28 <a href="/users/new">Create</a>
29 </li>
30 </ul>
31 </li>
32 <li class="dropdown">
33 <a data-toggle="dropdown" class="dropdown-toggle" href="\
34 ">
35 Rooms
36 <span class="caret"></span>
37 </a>
38 <ul class="dropdown-menu">
39 <li>
40 <a href="/rooms">List</a>
41 <a href="/rooms/new">Create</a>
42 </li>
43 </ul>
44 </li>
45 </ul>
46 </div>
47 </div>
48 </nav>
49 </header>
Para seguir a mesma linha de estilização do header, adicione o seguinte código no arquivo
public/scss/app.scss:
1 body {
2 background: #fefefe;
3 }
4
5 #header {
6 .navbar {
7 border-radius: 0;
8 }
9 }
10
11 #footer {
12 &.footer-inverse {
13 background: #222222;
14 padding: 2rem;
15 p {
16 color: white;
17 }
18 }
19 }
1 <!DOCTYPE html>
2 <html lang="en">
3 <head>
4 <meta charset="UTF-8">
5 <title>{{ title }}</title>
6 <meta name="viewport" content="width=device-width, initial-scale=1"/>
7 <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-boot\
8 strap/3.3.7/css/bootstrap.min.css">
9 <link rel="stylesheet" href="/css/app.css">
Criando Área Administrativa 42
10 </head>
11 <body>
12 {{> main/header }}
13
14 <main class="content-wrapper">
15 <div class="row">
16 <div class="container">
17 {{{ body }}}
18 </div>
19 </div>
20 </main>
21
22 {{> main/footer }}
23
24 <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js">\
25 </script>
26 <script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/js/b\
27 ootstrap.min.js"></script>
28 <script src="/js/app.js"></script>
29 </body>
30 </html>
mongod
Linux e Mac
Com o serviço ativo, criaremos o primeiro arquivo dentro da pasta schemas. Crie um arquivo
chamado users.js.
11 },
12 email: {
13 type: String,
14 required: true
15 },
16 password: {
17 type: String,
18 required: true
19 }
20 })
21
22 module.exports = mongoose.model('User', User);
Observe que configuramos o schema com um objeto e passamos os dados presentes neste objeto.
Informamos o tipo e configuramos como campos obrigatórios, utilizando o required: true.
Depois, exportamos o módulo, para dar continuidade a implementação.
Para funcionar corretamente, temos que configurar o uso do Mongoose no arquivo src/configs/env/-
development.js.
Após todas estas configurações, estamos prontos para começar a gravar dados no banco, porque já
temos o primeiro schema criado e a configuração de conexão com MongoDB.
CRUD - User
Criaremos rotas específicas para que o usuário consiga cadastrar novos usuários no banco. Siga os
seguintes passos:
Criando Administração de Usuário 45
Desta forma, configuramos todos os possíveis pontos de entrada no painel administrativo. Depois
de configurar as rotas, crie os arquivos que servirão as mesmas.
Crie dentro da pasta src/services/users, os seguintes arquivos:
• index.js
• new.js
• edit.js
• show.js
• create.js
• update.js
• remove.js
Após a criação das rotas e serviços, criaremos as views. Crie dentro da pasta src/views/users
• index.hbs
• new.hbs
Criando Administração de Usuário 46
• edit.hbs
• show.hbs
• create.hbs
• update.hbs
• remove.hbs
Observe que estamos criando validações para os campos no arquivo de rotas para usuários, utilizando
a biblioteca de validações que instalamos com o gerenciador npm.
1 createRules = require('./../validator/users/create')
2 editRules = require('./../validator/users/edit')
3 removeRules = require('./../validator/users/remove')
4 updateRules = require('./../validator/users/update')
Para que estas validações funcionem, precisamos criar os arquivos citados acima. Crie uma pasta
dentro de rotas que serão os chamados middlewares, ou seja, eles farão uma validação antes de
terminar o processo completo. Caso haja algum problema, ele retorna este erro e não finaliza.
Dentro da pasta src/routes/validator/users, crie os arquivos:
• create.js
• edit.js
• remove.js
• update.js
Até o momento vimos as estruturas de arquivos e pastas. Chegou a hora de, finalmente, começarmos
a trabalhar com códigos e fazer todos estes serviços funcionarem.
Veja os códigos dos arquivos de validações:
1 // users/create
2 module.exports = (req, res, next) => {
3 req
4 .checkBody('name', 'Field name is required')
5 .notEmpty()
6 req
7 .checkBody('email', 'Field email is required')
8 .notEmpty()
9 .isEmail()
10 req
11 .checkBody('password', 'Field password is required')
12 .notEmpty()
13
Criando Administração de Usuário 47
57 req
58 .checkParams('id', 'Field id is required')
59 .notEmpty()
60 .isMongoId()
61
62 let errors = req.validationErrors()
63
64 if (!errors) {
65 return next()
66 }
67
68 return res.redirect('/users')
69 }
Observe que todas as validações possuem o parâmetro next, o que caracteriza ser um middleware,
ou seja, se não houver erro durante as validações, ele retorna para o processo da rota e conclui o
restante do processo.
Todas as validações receberam dados de requisição(res), response(res) e o next(next), para dar
continuidade. Por isso podemos tratar os dados durante a validação e prosseguir, normalmente,
o fluxo da lógica.
As validações verificam:
• se os parâmetros existem
• se estão vazios
• se são ids do MongoDB.
Desta maneira, estamos validando todos os campos e parâmetros da url que são passados.
Agora que temos, praticamente, tudo configurado, começaremos com o serviço create.
Em resumo, o cliente acessará a url http://localhost:9000/users/new. Quando ele acessa este link o
sistema acessa o arquivo src/routes/users/index.js que, por sua vez, possui a rota /new registrada.
Esta rota acessa o serviço src/services/users/new.js.
Veja o código do arquivo src/services/users/new.js:
Este serviço gera o objeto de usuário e redireciona o processo para a view src/views/users/cre-
ate.hbs, passando o título e o objeto como parâmetro.
Veja o código da view:
Note que uma estrutura puxa a outra. Agora que criamos a view e chamamos uma partials,
precisamos criar este arquivo partials. Na pasta src/views/partials/users, criaremos o arquivo
_form.hbs.
Criando Administração de Usuário 50
1 <div class="form-group">
2 <label for="name">Name</label>
3 <input type="text" name="name" placeholder="Enter your name" class="form-control\
4 " value="{{ user.name }}">
5 </div>
6 <div class="form-group">
7 <label for="email">Email</label>
8 <input type="email" name="email" placeholder="Enter your email" class="form-cont\
9 rol" value="{{ user.email }}">
10 </div>
11 <div class="form-group">
12 <label for="password">Password</label>
13 <input type="password" name="password" placeholder="Enter your password" class="\
14 form-control" value="{{ user.password }}">
15 </div>
16 <div class="form-group">
17 <button type="submit" class="btn btn-primary">Save</button>
18 </div>
Veja que os atributos value são enviados pelo serviço através do objeto recebido, anteriormente.
Depois que o usuário preencher os dados do formulário e submeter, será enviada uma requisição do
tipo POST para a rota http://localhost:9000/users/, conforme a action do formulário nos indica.
Desta forma, caimos, novamente, no arquivo src/routes/users/index.js que trata esta url e o tipo
da requisição, conforme código abaixo:
Tratamos o slug para que tenha o formato esperado e, depois, utilizamos o objeto Users para efetuar
o cadastro no banco de dados. Caso tenhamos sucesso no cadastro, o processo é redirecionado para
a rota http://localhost:9000/users/ utilizando o método GET. Desta forma, teremos a listagem dos
usuários já cadastrados. Caso não tenhamos sucesso, o processo emite um erro para o usuário.
Como temos a rota mencionada acima, utilizando o método get, o processo identifica a seguinte rota
no arquivo de rotas:
router.get('/', require('./../../services/users/index'))
Este serviço, por sua vez, pesquisa no banco de dados todos os usuários e se encontrar renderiza a
view src/views/users/index passando o título e uma coleção de usuários, como parâmetro. Caso
não encontre nenhum usuário, teremos um erro sendo mostrado.
Veja o código da view:
34 {{/each}}
35 </tbody>
36 </table>
37 </div>
38 </div>
39 </div>
Assim, finalizamos o processo de cadastro de usuário e a listagem dos mesmos. Você pode fazer o
teste no navegador. Não esqueça que é necessário rodar o gulp e o mongod no terminal, para não
ter nenhum erro.
Você pode encontrar um erro de layout porque no código acima adicionamos uma estilização para
os botões das actions.
Veja código abaixo:
Esta estrutura refere-se a uma biblioteca de ícones que utilizaremos no projeto. Para que ela funcione,
precisamos carregá-la em nosso arquivo de layout principal src/views/layouts/main.hbs. Veja a
linha adicionada a este arquivo:
Adicione na tag head do HTML depois do css do Bootstrap e antes do nosso css app.scss.
Adicionamos uma estilização no arquivo public/scss/app.scss, também. Veja abaixo e adicione,
também:
1 .inline {
2 display: inline-block;
3 }
Veja que o protocolo HTTP só reconhece os métodos GET e POST. Porém, estamos fazendo com
que ele trabalhe como se fosse um DELETE. Esta é a função desta biblioteca.
Estamos sobrescrevendo o método e passando a query string.
10 }
11
12 return res.render('users/edit', {
13 title: 'Users - ChatSchool Admin',
14 user
15 })
16 })
17 .catch((error) => {
18 return res.send('Error : ' + error)
19 })
20 }
A lógica é procurar pelo id que é resgatado da URL. Se existir, ele direciona para a página de edição,
caso contrário é mostrado um erro.
Veja o código da view src/views/users/edit.hbs:
O código é muito parecido com a view create. Note que utilizamos a biblioteca method-override
novamente, informando que o método será do tipo PUT.
Assim que o usuário atualizar os dados e clicar no botão de atualização, estará enviando uma
requisição para o serviço src/services/users/update.js. Veja o código:
O serviço pesquisa pelo id e atualiza com os dados vindos da requisição, que já foram transformados
pela biblioteca body-parser. Caso a atualização tenha sucesso, ele redireciona para a página de
listagem de usuários. Caso contrário, informa um erro.
Temos o CRUD completo com: adição, edição e remoção de usuários na área administrativa.
Existe, apenas, um ponto a ser considerado. Em nosso arquivo de rotas, configuramos duas rotas que
executam o mesmo procedimento de atualização, porém estamos utilizando apenas uma.
Veja que as duas executam o mesmo serviço, porém são acionadas por métodos HTTPs, diferentes.
Caso queiram utilizar o método patch, basta alterar o método na view edit.hbs:
Criando Administração de Usuário 57
O serviço não precisa ser alterado, somente o método, e você terá o mesmo resultado de atualização.
Utilizaremos o PUT, porque é mais comum.
Como criamos alguns arquivos de serviços que estão em branco e eles estão presentes em algumas
rotas que criamos você deve adicionar o conteúdo padrão dos serviços para que não tenha nenhum
erro na hora de rodar o comando gulp.
Adicione o código abaixo em todos os arquivos de serviços que estiverem em branco:
Desta forma, você poderá rodar o comando não tendo nenhum erro. Ao rodar o comando faça testes
de cadastro, edição e exclusão de usuários.
Criando Administração de Salas
Definição de schema inicial - Room
Neste capítulo elaboraremos a estrutura para a criação de salas da plataforma de chat. Deixaremos
a estrutura de schema pronta para depois podermos cadastrar as salas, tranquilamente.
Dentro da pasta src/schemas, crie o arquivo room.js.
31
32 module.exports = mongoose.model('Rooms', Rooms)
Com a estrutura de schema criada, desenvolveremos o sistema de cadastro de novas salas de chat.
CRUD - Room
Começaremos pela estrutura, uma vez que ela será parecida com a estrutura do CRUD de usuários.
Crie uma pasta chamada rooms, dentro das seguintes pastas:
• src/services
• src/views
• src/routes
• src/routes/validator
• create.js
• edit.js
• index.js
• new.js
• update.js
• remove.js
• show.js
• create.hbs
• edit.hbs
• index.hbs
• index.js
• create.js
• edit.js
• remove.js
• update.js
Para iniciarmos a estrutura, faremos da mesma forma que fizemos para o CRUD de users, apontando
a rota principal para o arquivo de rotas. Veja o código adicionado ao arquivo src/index.js:
Depois de incluir este endpoint, acesse o arquivo src/routes/rooms/index.js e faça uma cópia do
arquivo de users, pois a base será a mesma:
Note que assim como no arquivo de rotas de users, estamos aplicando as validações. Veja os códigos
dos arquivos de validações:
1 // validator - create.js
2 module.exports = (req, res, next) => {
3 req
4 .checkBody('name', 'Field name is required')
5 .notEmpty()
6 req
7 .checkBody('description', 'Field description is required')
8 .notEmpty()
9 req
10 .checkBody('enable', 'Field enable is required')
11 .notEmpty()
12
13 let errors = req.validationErrors()
14
15 if (!errors) {
16 return next()
17 }
18
19 return res.redirect('/rooms')
20 }
21
22 // validator - edit.js
23 module.exports = (req, res, next) => {
24 req
25 .checkParams('slug', 'Field slug is required')
26 .notEmpty()
27
28 let errors = req.validationErrors()
29
30 if (!errors) {
31 return next()
32 }
33
Criando Administração de Salas 62
34 return res.redirect('/rooms')
35 }
36
37 // validator - remove.js
38 module.exports = (req, res, next) => {
39 req
40 .checkParams('id', 'Field id is required')
41 .notEmpty()
42 .isMongoId()
43
44 let errors = req.validationErrors()
45
46 if (!errors) {
47 return next()
48 }
49
50 return res.redirect('/rooms')
51 }
52
53 // validator - update.js
54 module.exports = (req, res, next) => {
55 req
56 .checkParams('id', 'Field id is required')
57 .notEmpty()
58 .isMongoId()
59
60 let errors = req.validationErrors()
61
62 if (!errors) {
63 return next()
64 }
65
66 return res.redirect('/rooms')
67 }
Após a criação das rotas e aplicadas as validações, criaremos os serviços para o CRUD de rooms.
Veja os códigos de todos os serviços, abaixo:
Criando Administração de Salas 63
1 // services - create.js
2 const Rooms = require('./../../schemas/rooms')
3
4 module.exports = (req, res) => {
5 req.body.slug = req.body.name.toLowerCase().replace(/ /g, '-')
6 req.body.enable = req.body.enable ? true : false
7
8 Rooms
9 .create(req.body)
10 .then((room) => {
11 return res.redirect('/rooms')
12 })
13 .catch((error) => {
14 return res.send('Error: ' + error)
15 })
16 }
17
18 // services - edit.js
19 const Rooms = require('./../../schemas/rooms')
20
21 module.exports = (req, res) => {
22 Rooms
23 .findOne({
24 slug: req.params.slug
25 })
26 .then((room) => {
27 if (!room) {
28 return res.status(404).end()
29 }
30
31 return res.render('rooms/edit', {
32 title: 'Rooms - ChatSchool Admin',
33 room
34 })
35 })
36 .catch((error) => {
37 return res.send('Error: ' + error)
38
39 })
40 }
41
42 // services - index.js
43 const Rooms = require('./../../schemas/rooms')
Criando Administração de Salas 64
44
45 module.exports = (req, res) => {
46 Rooms
47 .find()
48 .then((rooms) => {
49 return res.render('rooms/index', {
50 title: 'Rooms - ChatSchool Admin',
51 rooms
52 })
53 })
54 .catch((error) => {
55 return res.send('Error: ' + error)
56 })
57 }
58
59 // services - new.js
60 const Rooms = require('./../../schemas/rooms')
61
62 module.exports = (req, res) => {
63 let room = new Rooms()
64
65 return res.render('rooms/create', {
66 title: 'Rooms - ChatSchool Admin',
67 room
68 })
69 }
70
71 // services - remove.js
72 const Rooms = require('./../../schemas/rooms')
73
74 module.exports = (req, res) => {
75 Rooms
76 .findByIdAndRemove(req.params.id)
77 .then((room) => {
78 return res.redirect('/rooms')
79 })
80 .catch((error) => {
81 return res.send('Error: ' + error)
82 })
83 }
84
85 // services - show.js
86 module.exports = (req, res) => {
Criando Administração de Salas 65
87 // Code here
88 }
89
90 // services - update.js
91 const Rooms = require('./../../schemas/rooms')
92
93 module.exports = (req, res) => {
94 req.body.slug = req.body.name.toLowerCase().replace(/ /g, '-')
95 req.body.enable = req.body.enable ? true : false
96
97 Rooms
98 .findByIdAndUpdate(req.params.id, req.body)
99 .then((room) => {
100 return res.redirect('/rooms')
101 })
102 .catch((error) => {
103 return res.send('Error: ' + error)
104 })
105 }
Uma vez que criamos todos os serviços e eles chamarão as views, vamos aos códigos de cada view:
Nestas views estamos utilizando, praticamente, a mesma estrutura das views de users. Trocamos
alguns campos e também o formulário. Você deve criar, dentro de src/views/partials/rooms, um
Criando Administração de Salas 67
1 <div class="form-group">
2 <label for="name">Name</label>
3 <input type="text" name="name" placeholder="Enter room name" class="form-control\
4 " value="{{ room.name }}">
5 </div>
6
7 <div class="form-group">
8 <label for="description">Description</label>
9 <input type="text" name="description" placeholder="Enter room description" class\
10 ="form-control" value="{{ room.description }}">
11 </div>
12
13 <div class="form-group">
14 <label for="enable">Situação</label>
15 <input type="checkbox" name="enable" value="{{ room.enable }}" />
16 <label for="enable">
17 Ativo/Desativo?
18 </label>
19 </div>
20 <div class="form-group">
21 <button type="submit" class="btn btn-primary">Save</button>
22 </div>
1 mongoose.connect(app.get('mongo_url'))
2
3 require('./../helpers')(hbs)
Note que a primeira linha já existia no arquivo, o que adicionamos foi a segunda linha de código.
Nesta linha, como não passamos nenhum nome de arquivo específico, estamos carregando o arquivo
index.js, da pasta helpers, mas ainda não o criamos. Crie o arquivo index.js, dentro da pasta helpers
e adicione o seguinte código:
Criando Administração de Salas 68
Observe que registramos um helper chamado checkedIf, que provém do arquivo checkedIf.js, que
ainda não criamos. Este arquivo deve ser criado na pasta helpers e deve conter o seguinte código:
Desta forma, você pode criar diversos outros helpers e depois registrar no arquivo index.js.
Agora, basta utilizar o serviço na view src/views/partials/rooms/_form.hbs. Veja o código alterado
e aplicado:
1 <div class="form-group">
2 <label for="enable">Situação</label>
3 <input type="checkbox" name="enable" value="{{ room.enable }}" {{checkedIf room.\
4 enable}} />
5 <label for="enable">
6 Ativo/Desativo?
7 </label>
8 </div>
Desenvolveremos a autenticação utilizando sessões, de uma maneira muito simples e rápida. Por
este motivo, instalamos a biblioteca express-session, pois o passport-local-mongoose é totalmente
dependente desta biblioteca.
A configuração será feita no arquivo src/configs/env/development.js. Veja o código adicionado a
este arquivo:
1 // services/users/create.js
2 const Users = require('./../../schemas/users')
3
4 module.exports = (req, res) => {
5 req.body.slug = req.body.name.toLowerCase().replace(/ /g, '-')
6
7 Users
8 .register(req.body, req.body.password, (err, account) => {
9 if (err) {
10 return res.send('Error ' + error)
11 }
Criando Login e Logout 71
12
13 return res.redirect('/users')
14 })
15 }
16
17 // services/users/update.js
18 const Users = require('./../../schemas/users')
19
20 module.exports = (req, res) => {
21 Users
22 .findById(req.params.id)
23 .then((user) => {
24 user.password = req.body.password
25
26 user.setPassword(user.password, (err, updated, passErr) => {
27 if (err || passErr) {
28 return res.send('Error: ' + error)
29 }
30
31 user.save()
32
33 user.email = req.body.email
34 user.name = req.body.name
35
36 user.save()
37 return res.redirect('/users')
38
39 })
40 })
41 .catch((error) => {
42 return res.send('Error: ' + error)
43 })
44 }
Observe que os métodos continuam parecidos, porém quem se encarrega de fazer a persistência no
banco de dados são as bibliotecas que instalamos em conjunto com a programação.
Note que verificamos o tipo de usuário que está tentando fazer a persistência usando o código
(req.body.password). Caso não exista este campo, significa que o usuário não está logado e,
consequentemente, não tem acesso e nem permissão para executar a ação de persistência em banco.
Utilizamos o método register para criação e o método save para atualizações. Perceba que estamos
utilizando duas vezes o método save, em nosso serviço de update. Não é um erro. Precisamos executar
duas vezes porque o método setPassword não suportaria salvar outros dados a não ser a password.
Criando Login e Logout 72
Robomongo Passport
Criando login
Para a criação de login, manteremos a mesma regra utilizada até agora. Criaremos:
• o endpoint principal
• o arquivo de rotas dentro deste endpoint
• as views
• os serviços.
Primeiro, crie uma pasta chamada login dentro da pasta src/views e dentro desta pasta um arquivo
chamado index.hbs.
Criando Login e Logout 73
Dentro da pasta src/routes, crie uma pasta chamada login e dentro desta pasta, um arquivo index.js
com o seguinte conteúdo:
Agora, crie a pasta login em src/services e dentro desta pasta, o arquivo index.js e login.js, com
os seguintes códigos, respectivamente:
Criando Login e Logout 74
1 // arquivo index.js
2 module.exports = (req, res) => {
3 return res.render('login/index', {
4 title: 'Login - ChatSchool Admin'
5 })
6 }
1 // arquivo login.js
2 const Users = require('./../../schemas/users')
3
4 module.exports = (req, res) => {
5 Users
6 .authenticate()(req.body.email, req.body.password, (err, user, options) => {
7 if (err) {
8 return res.send('Error: ' + err)
9 }
10
11 return req.login(user, (err) => {
12 if (err) {
13 return
14 }
15
16 return res.redirect('/')
17 })
18 })
19 }
Desta forma, caso acesse http://localhost:9000/login, você deverá ser redirecionado para a view de
login e, caso adicione os dados de autenticação corretos, você deverá ser redirecionado para a página
raiz do sistema de administração. Se houver algum erro de renderização, pode tentar rodar o gulp
novamente, no terminal.
Depois de fazer o login e estar autenticado, note que qualquer usuário ainda tem acesso a qualquer
ponto do sistema, mas isso não é correto. Apenas os usuários autenticados deveriam ter este acesso.
Para resolver este problema, criaremos um recurso chamado middleware. Crie uma pasta com este
nome dentro da pasta src/routes e dentro desta pasta um arquivo chamado isloggedin.js e adicione
o seguinte código:
Criando Login e Logout 75
Observe que este código verifica se os dados do usuário existem na requisição. Se existir, ele continua
a execução. Caso contrário, ele redireciona para a página de login obrigando o usuário estar logado
para ter acesso.
Apenas criar este arquivo e adicionar este código não faz com que o middleware funcione.
Precisamos aplicá-lo em nossas rotas.
Aplicaremos na rota src/routes/main/index.js, para que você tenha o primeiro exemplo de aplica-
ção:
Note que estamos aplicando o middleware, assim como aplicamos as nossas validações.
Depois aplicar a rota main, reinicie o gulp e tente acessar novamente a rota raiz http://localhost:9000/.
Você será redirecionado para a página de login. Significa que o sistema de autenticação está
funcionando. Basta aplicar as demais rotas:
1 // arquivo src/routes/users/index.js
2 const express = require('express')
3 const router = express.Router()
4
5 const isLoggedIn = require('./../middleware/isloggedin')
6
7 const createRules = require('./../validator/users/create')
8 const editRules = require('./../validator/users/edit')
9 const removeRules = require('./../validator/users/remove')
10 const updateRules = require('./../validator/users/update')
11
Criando Login e Logout 76
Basta rodar o gulp, novamente, para evitar erros e pronto!. Já estamos com todas as rotas protegidas,
através deste middleware.
Para termos uma segurança a mais, podemos adicionar um handlebar no arquivo src/views/parti-
als/main/header.hbs. Desta forma, ocultaremos o menu principal caso o usuário não esteja logado.
Criando Login e Logout 77
Antes disso, o usuário não tinha acesso mas ele ainda via as opções possíveis no menu. Com esta
adaptação, ele não consegue exergar o menu principal.
1 <header id="header">
2 <nav class="navbar navbar-inverse">
3 <div class="container-fluid">
4 <div class="nav-header">
5 <button type="button" class="navbar-toggle collapsed" data-toggle="c\
6 ollapse" data-target="#nav">
7 <span class="sr-only"></span>
8 <span class="icon-bar"></span>
9 <span class="icon-bar"></span>
10 <span class="icon-bar"></span>
11 </button>
12 <a href="/" class="navbar-brand">ChatSchool</a>
13 </div>
14 {{#if user_logged}}
15 <div class="collapse navbar-collapse" id="nav">
16 <ul class="nav navbar-nav">
17 <li class="active">
18 <a href="/">Home</a>
19 </li>
20 <li class="dropdown">
21 <a data-toggle="dropdown" class="dropdown-toggle" href="\
22 ">
23 Users
24 <span class="caret"></span>
25 </a>
26 <ul class="dropdown-menu">
27 <li>
28 <a href="/users">List</a>
29 <a href="/users/new">Create</a>
30 </li>
31 </ul>
32 </li>
33 <li class="dropdown">
34 <a data-toggle="dropdown" class="dropdown-toggle" href="\
35 ">
36 Rooms
37 <span class="caret"></span>
38 </a>
39 <ul class="dropdown-menu">
40 <li>
Criando Login e Logout 78
41 <a href="/rooms">List</a>
42 <a href="/rooms/new">Create</a>
43 </li>
44 </ul>
45 </li>
46 </ul>
47 </div>
48 {{/if}}
49 </div>
50 </nav>
51 </header>
1 // src/services/main/index.js
2 module.exports = (req, res) => {
3 return res.render('main/index', {
4 title: 'Chatschool - Admin',
5 user_logged: req.user
6 })
7 }
8
9 // src/services/users/index.js
10 return res.render('users/index', {
11 title: 'Users - ChatSchool Admin',
12 users,
13 user_logged: req.user
14 })
15
16 // src/services/users/edit.js
17 return res.render('users/edit', {
18 title: 'Users - ChatSchool Admin',
19 user,
20 user_logged: req.user
21 })
22
23 // src/services/users/new.js
Criando Login e Logout 79
24 return res.render('users/create', {
25 title: 'Users - Chatschool Admin',
26 user,
27 user_logged: req.user
28 })
29
30 // src/services/rooms/index.js
31 return res.render('rooms/index', {
32 title: 'Rooms - ChatSchool Admin',
33 rooms,
34 user_logged: req.user
35 })
36
37 // src/services/rooms/edit.js
38 return res.render('rooms/edit', {
39 title: 'Rooms - ChatSchool Admin',
40 room,
41 user_logged: req.user
42 })
43
44 // src/services/rooms/new.js
45 return res.render('rooms/create', {
46 title: 'Rooms - ChatSchool Admin',
47 room,
48 user_logged: req.user
49 })
Adicionando a variável em todas as rotas, temos o sistema todo protegido de usuários não
autenticados.
FrontEnd
Iniciando Projeto FrontEnd
Iniciaremos o desenvolvimento do frontend do projeto.
O frontend é toda parte visual e, também, HTML e CSS.
Melhoraremos o layout do chat, conforme for necessário, até chegarmos ao objetivo final de layout.
Para isso, seguiremos os seguintes passos:
O foco inicial será no webapp. Nos próximos capítulos criaremos toda parte de estilização da área
administrativa.
Até este ponto do projeto tínhamos a pasta principal chamada chatschool e dentro desta pasta os
arquivos do projeto. A partir de agora mudaremos a estrutura.
Dentro da pasta chatschool teremos outras duas pastas:
admin e webapp.
Crie estas duas pastas. Mova todos os arquivos que tenha na pasta raiz para a pasta admin.
Começaremos a desenvolver a estrutura de layout na pasta webapp.
Dentro da pasta webapp, crie uma outra pasta chamada src. Após a criação desta pasta, acesse o
terminal e começará a trabalhar com o gerenciador de pacotes do node. O famoso npm.
Dentro da pasta webapp rode o seguinte comando:
npm init
O comando exigirá algumas informações e você deverá preencher. O importante será o arquivo final
que este comando gera:
package.json
Este arquivo será responsável por gerenciar as dependências do projeto. Veja o exemplo incial do
arquivo:
FrontEnd 81
1 {
2 "name": "chatschool",
3 "version": "1.0.0",
4 "description": "chatschool, nodejs, socketio",
5 "main": "index.js",
6 "scripts": {
7 "test": "echo \"Error: no test specified\" && exit 1"
8 },
9 "keywords": [
10 "chatschool",
11 "nodejs",
12 "socketio"
13 ],
14 "author": "",
15 "license": "ISC"
16 }
Utilizaremos algumas bibliotecas que foram usadas para o desenvolvimento do admin. Podemos
pegar a base do arquivo package.json.
Deixaremos a parte de instalação de dependências para outro momento, mas já deixe o arquivo
package.json criado na pasta raiz. Focaremos, agora, na pasta src e sua arquitetura.
• src
– fonts
– js
– scss
Configurando gulp
Começaremos configurando o gulp, para que nossas tarefas sejam todas automatizadas.
O primeiro passo:
Ainda não criamos o arquivo gulpfile. Crie o arquivo na raiz da pasta chatschool/webapp.
Para o frontend, utilizaremos as seguintes bibliotecas:
FrontEnd 82
• gulp
• gulp-notify
• gulp-livereload
• gulp-changed
• gulp-util
• del
• gulp-concat
• gulp-plumber
• gulp-imagemin
• gulp-minify-css
• gulp-minify-html
• gulp-rev
• gulp-rev-collector
• gulp-uglify
• gulp-sass
• gulp-connect
Podemos instalar todas as dependências com apenas um comando. Veja o comando abaixo:
Após a conclusão das instalações, começaremos a configurar o arquivo gulpfile.js. Este arquivo será
muito parecido com o arquivo gulpfile da seção admin, que já criamos.
Lembrando que trabalhamos com o gulp para tentarmos melhorar a produtividade durante o
desenvolvimento.
A grande diferença entre a parte administrativa e o frontend, é que no admin temos um server e no
frontend, não.
Para resolvermos este problema, instalaremos uma biblioteca que tem uma importância gigantesca
dentro do projeto. Esta biblioteca se chama gulp-connect e é responsável por criar um servidor,
automaticamente, assim que rodarmos o comando gulp.
Veja o código do arquivo gulpfile.js.
FrontEnd 83
44 .src(paths.sassSrc.concat('**/*.scss'))
45 .pipe(sass({
46 includePaths: require('node-neat').includePaths,
47 style: 'nested',
48 onError: function(){
49 console.log('SASS ERROR!')
50 }
51 }))
52 .pipe(plumber({ errorHandler: onError }))
53 .pipe(gulp.dest(paths.buildDir.concat('/css')))
54 .pipe(livereload())
55 })
56
57 gulp.task('build-js', () => {
58 return gulp
59 .src(paths.jsSrc.concat('*.js'))
60 .pipe(plumber({ errorHandler: onError }))
61 .pipe(changed(paths.buildDir.concat('/js')))
62 .pipe(gulp.dest(paths.buildDir.concat('/js')))
63 .pipe(livereload())
64 })
65
66 gulp.task('build-fonts', () => {
67 return gulp
68 .src(paths.fontsSrc.concat('**/*.*'))
69 .pipe(gulp.dest(paths.buildDir.concat('/fonts')))
70 .pipe(livereload())
71 })
72
73 gulp.task('build-images', () => {
74 return gulp
75 .src(paths.imgSrc.concat('**/*.+(png|jpeg|gif|jpg|svg)'))
76 .pipe(changed(paths.buildDir.concat('/images')))
77 .pipe(gulp.dest(paths.buildDir.concat('/images')))
78 .pipe(livereload())
79 })
80
81 gulp.task('build', ['build-html', 'build-css', 'build-js', 'build-images', 'build-fo\
82 nts'], () => {
83 return connect.server({
84 root: 'src',
85 livereload: true
86 })
FrontEnd 85
87 })
88
89 gulp.task('watch', () => {
90 gulp.watch(['src/*.html'], ['build-html']);
91 gulp.watch('src/scss/**', ['build-css']);
92 gulp.watch(paths.jsSrc + '**/*.js', ['build-js']);
93 gulp.watch(paths.imgSrc + '**/*.+(png|jpeg|jpg|svg)', ['build-images']);
94 })
95
96 const env = process.env.NODE_ENV || 'development'
97
98 if (env === 'development') {
99 return gulp.task('default', ['build', 'watch'])
100 }
Compare com o arquivo gulpfile.js, criado para o setor administrativo e verá que há poucas
diferenças.
Após criar este arquivo com toda configuração necessária, crie um arquivo chamado index.html,
dentro da pasta webapp/src. Dentro deste arquivo incluiremos algumas dependências.
Veja o código abaixo:
1 <!doctype html>
2 <html lang="en">
3 <head>
4 <meta charset="UTF-8">
5 <meta name="viewport"
6 content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-\
7 scale=1.0, minimum-scale=1.0">
8 <meta http-equiv="X-UA-Compatible" content="ie=edge">
9
10 <title>Document</title>
11 <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-boot\
12 strap/3.3.7/css/bootstrap.min.css">
13 </head>
14 <body>
15
16 </body>
17 <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js"></sc\
18 ript>
19 <script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/js/boots\
20 trap.min.js"></script>
FrontEnd 86
21 </body>
22 </html>
Temos o gulp configurado, com todas as tarefas automatizadas e o layout carregando o Bootstrap.
Veja que estamos chamando a biblioteca jQuery, que é uma exigência do Bootstrap, uma vez que
ele a utiliza para várias funcionalidades.
Elementos do layout
Posição Descrição
chatbar Listagem de salas e usuários
chatbox Corpo das mensagens
1 <body>
2 <main>
3 <aside class="col-xs-6 col-md-2">
4 SIDEBAR
5 </aside>
6 </main>
7 </body>
Se olharmos no navegador, ainda não temos modificação com o código acima. Como ainda não
testamos a configuração do gulpfile.js, este é o momento para testar.
No terminal, onde se encontra o arquivo gulpfile.js, rode o comando gulp.
gulp
ou
gulp default
FrontEnd 87
Este comando foi configurado para executar a tarefa build e watch, que serão responsáveis por
executar todas as tarefas e ficar assistindo para ver todas as alterações no projeto e refazer todas as
tarefas necessárias.
Trabalharemos com a biblioteca de fontes Awesome. Faremos a inclusão no arquivo HTML para
que possamos utilizar os recursos a qualquer momento.
1 <!doctype html>
2 <html lang="en">
3 <head>
4 <meta charset="UTF-8">
5 <meta name="viewport"
6 content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-\
7 scale=1.0, minimum-scale=1.0">
8 <meta http-equiv="X-UA-Compatible" content="ie=edge">
9
10 <title>Document</title>
11 <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-boot\
12 strap/3.3.7/css/bootstrap.min.css">
13 <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome\
14 /4.7.0/css/font-awesome.css">
15 </head>
16 <body>
17 <main>
18 <aside class="col-xs-6 col-md-2">
19 <i class="fa fa-commenting-o"></i>
20 <h4>ChatSchool</h4>
21 <ul>
FrontEnd 88
22 <li>
23 <h4>Canais</h4>
24 </li>
25 <li>Canal 1</li>
26 </ul>
27 <ul>
28 <li>
29 <h4>Mensagens</h4>
30 </li>
31 <li>Fabrício</li>
32 </ul>
33 </aside>
34 </main>
35
36 <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js">\
37 </script>
38 <script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/js/b\
39 ootstrap.min.js"></script>
40 </body>
41 </html>
Depois, estilizaremos para que fique mais bonito e com a cara do projeto final. Por enquanto, não se
preocupe com a estética, apenas com a estrutura e a marcação HTML.
Recarregue o navegador e conseguirá visualizar as modificações.
1 <!doctype html>
2 <html lang="en">
3 <head>
4 <meta charset="UTF-8">
5 <meta name="viewport"
6 content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-\
7 scale=1.0, minimum-scale=1.0">
8 <meta http-equiv="X-UA-Compatible" content="ie=edge">
9
10 <title>Document</title>
FrontEnd 89
54 </body>
55 </html>
1 @import "./_default.scss";
2 @import "./_chatbar.scss";
Estamos quase prontos para conseguirmos estilizar a sidebar. Para isso, precisaremos executar dois
procedimentos simples:
1 <head>
2 <meta charset="UTF-8">
3 <meta name="viewport"
4 content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-\
5 scale=1.0, minimum-scale=1.0">
6 <meta http-equiv="X-UA-Compatible" content="ie=edge">
7
8 <title>Document</title>
9 <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-boot\
10 strap/3.3.7/css/bootstrap.min.css">
11 <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome\
12 /4.7.0/css/font-awesome.css">
13 <link rel="stylesheet" href="./css/application.css">
14 </head>
Veja que, o último arquivo de estilização que estamos incluindo é o application.css. Este arquivo é
criado pelas tarefas do gulp, dentro da pasta webapp/build/css/.
Definimos a pasta root como sendo build. Basta acessar a pasta css com o nome do arquivo principal.
Rode o comando gulp, novamente, e você terá a pasta build criada e o css sendo carregado,
corretamente.
A partir de agora alteraremos os arquivos da pasta webapp/src. O browser renderizará os arquivos
da pasta webapp/build, pois é para lá que as tarefas estão enviando os arquivos compilados.
1 .chatbar {
2 padding: 0;
3 background: #3C382C;
4 min-height: 100%;
5 height: 100%;
6
7 .title {
8 color: white;
9 background: #322F24;
10 padding: 1rem 15px;
11 h4 {
12 display: inline-block;
13 }
14 }
15
16 .channels {
17 margin: 6rem 0;
18 .list-group-item {
19 border: 0;
20 background: #3C382C;
21 color: #857D64;
22 cursor: pointer;
23 &.title {
24 h4 {
25 display: inline-block;
26 text-transform: uppercase;
27 font-weight: bold;
28 font-size: 1em;
29 }
30 }
31 &:hover, &:active {
32 background: #322F24;
33 }
34 }
35 }
36
37 .messages {
38 margin: 3.5rem 0;
39 .list-group-item {
40 border: 0;
41 background: #3C382C;
42 color: #857D64;
43 text-transform: capitalize;
FrontEnd 94
44 cursor: pointer;
45 &.title {
46 h4 {
47 display: inline-block;
48 text-transform: uppercase;
49 font-weight: bold;
50 font-size: 1em;
51 }
52 }
53 &:hover, &:active {
54 background: #322F24;
55 }
56 }
57 }
58 }
• no arquivo _default.scss foi adicionado tudo que será comum entre os blocos principais.
• adicionamos a classe chatbar na tag aside, para que todo CSS presente no arquivo _chat-
bar.scss fosse aplicado a sidebar.
• estilizamos o título principal com um background diferente do background da sidebar, para dar
mais destaque.
• definimos a sidebar com 100% da altura, para que pegasse toda a extensão da página.
• estilizamos os títulos, de cada item da lista presente na marcação.
1 .chatbox {
2 padding: 0;
3 .header {
4 padding: 1rem 15px;
5 border-bottom: 1px solid #ebebeb;
6 .username {
7 font-size: 1em;
8 font-weight: bold;
9 }
10 }
11 .conversation {
12 .message {
13 &.first-child {
14 margin: 1em 0;
15 }
16 margin: 1em 0 0 0;
17 .avatar {
18 border-radius: 10%;
19 border: 1px solid #ebebeb;
20 padding: 1rem 0;
21 background: #3C382C;
22 h2 {
23 color: white;
24 text-align: center;
25 margin: 0;
26 font-weight: bold;
27 }
28 }
29 .text {
30 padding: 0.8em 15px;
31 font-size: 1.2em;
32 margin: 0;
33 font-weight: 300;
34 }
35 }
36 }
37 .type {
38 margin: 1rem 0 0 0;
39 form {
40 .form-control {
41 border: 1px solid #3C382C;
42 &:focus {
43 box-shadow: none;
FrontEnd 97
44 }
45 }
46 }
47 }
48 }
• no bloco de chatbox, decidimos destacar e separar o header. Criamos uma classe header e
estilizamos, adicionando uma borda inferior
• no bloco de mensagens, adicionamos uma classe principal chamada conversation
• no último bloco, definimos uma classe type, para estilizar o formulário de envio de mensagem
• na seção de mensagens, definimos o layout para que o avatar do usuário seja deslocado para
esquerda e a mensagem para a direita
• adicionamos três mensagens, manualmente, para que você possa ter uma ideia de como ficará
o layout final
• configuramos um espaçamento padrão para as mensagens, para que fique bem alinhado e
legível
Em seguida, faremos com que estas informações venham do banco de dados e faremos todas as
estilizações que forem necessárias.
BackEnd
Configurando Express
O frontend está pronto. Começaremos a trabalhar com a API, para consumir os dados vindos da
parte administrativa do projeto. Os dados serão consumidos e mostrados em um formato JSON.
Utilizaremos um projeto open source chamado Socket.io, juntamente com websocket, para nos
auxiliar nesta etapa.
Caso queira saber mais sobre o projeto Socket.io, acesse https://socket.io/. Neste site você terá acesso
a tudo que precisa saber, inclusive poderá contar com a documentação.
Este projeto facilita o trabalho em aplicações que utilizam o protocolo websocket, abstraindo alguns
métodos, tornando o trabalho muito mais produtivo.
A instalação é muito simples e o projeto é sólido, o que deve deixá-lo tranquilo para utilizar em suas
aplicações.
• Após instalado, rode o comando abaixo, para criar o esqueleto da api. Não se esqueça que deve
estar na pasta chatschool, pois será outro projeto além de admin e webapp.
express api
Diretório/Arquivo Descrição
app.js Arquivo inicial da aplicação
bin/ Pasta de executáveis da aplicação
package.json Arquivo gerenciador de dependências
public/ Pasta disponibilizada ao usuário para acesso
routes/ Pasta com todas as rotas ou endpoints, da aplicação
views/ Pasta com todas as views da aplicação
Instale todas as dependências inicias, rodando o comando abaixo, dentro da pasta api:
npm install
Além das dependências iniciais, precisaremos instalar mais algumas bibliotecas. Rode o comando
abaixo:
Depois de instalar a biblioteca mongoose, temos que fazer uma conexão para efetuarmos as buscas
no ambiente de desenvolvimento. Adicione o código abaixo no arquivo app.js da pasta raiz.
1 // Carregando mongoose
2 var mongoose = require('mongoose');
3
4 // Criando conexão
5 mongoose.connect('mongodb://127.0.0.1:27017/chatschool_dev');
Após adicionar os códigos acima, testaremos a conexão. Lembrando que você pode ter que iniciar o
serviço do mongodb na máquina, para deixar aguardando conexões, caso ainda não esteja rodando.
Para ativar, basta acessar a pasta de instalação do mongodb e rodar o comando abaixo:
Windows
Acesse a pasta binária da instalação do MongoDB (c:Program FilesMongoDBServer\3.2\bin) e rode
o comando abaixo:
BackEnd 100
mongod
Linux / Mac
O próximo passo é rodar a aplicação para checar se não há erro. Existem duas formas:
A segunda maneira é a mais indicada porque ela gera configurações relacionadas à portas e
ambientes.
1 app.use('/', index);
2 app.use('/users', users);
Adicionar:
1 require('./routes')(app);
Estamos carregando o arquivo routes.js que está na pasta raiz da API. Como este arquivo ainda não
existe, crie e adicione o seguinte código:
Assim:
O conteúdo dos dois arquivos são exatamente iguais, para este momento.
Crie os arquivos routes/users/find.js e routes/rooms/find.js. Quando o usuário acessar a rota raiz,
este arquivo que será chamado, conforme código acima.
BackEnd 102
Chegamos a um ponto que precisaremos utilizar o mongoose, para manipular os dados no banco, e
precisamos de um schema. Ainda não criamos na API estes models, mas já temos criado na seção
de admin.
Replicaremos estes mesmos models. Faremos com que possam retornar os valores no formato de
uma linguagem universal.
Crie uma pasta chamada model, dentro de sua pasta raiz, ou seja, no mesmo nível que a pasta routes.
Dentro desta pasta, crie dois arquivos: users.js e rooms.js.
Depois disso, copie os dados do model da seção de admin e cole nestes arquivos. Veja abaixo:
1 // users.js
2 var mongoose = require('mongoose')
3 //const passportLocalMongoose = require('passport-local-mongoose')
4
5 const User = new mongoose.Schema({
6 name: {
7 type: String,
8 required: true
9 },
10 slug: {
11 type: String,
12 required: true
13 },
14 email: {
15 type: String,
16 required: true
17 },
18 password: {
19 type: String,
20 required: true
21 }
22 })
23
24 //User.plugin(passportLocalMongoose, { usernameField: 'email' })
25
26 module.exports = mongoose.model('User', User);
BackEnd 103
1 // rooms.js
2 const mongoose = require('mongoose')
3
4 const Rooms = new mongoose.Schema({
5 name: {
6 type: String,
7 required: true
8 },
9 slug: {
10 type: String,
11 required: true
12 },
13 description: {
14 type: String,
15 required: true
16 },
17 enable: {
18 type: Boolean,
19 required: true,
20 default: true
21 },
22 users: [{
23 type: mongoose.Schema.Types.ObjectId,
24 ref: 'Users'
25 }],
26 created: {
27 type: Date,
28 required: true,
29 default: new Date()
30 }
31 })
32
33 module.exports = mongoose.model('Rooms', Rooms)
Com os models criados, basta criarmos os arquivos find.js, que serão responsáveis por retornar os
dados encontrados no banco, em formato json.
Conteúdo do arquivo **routes/users/find.js **
BackEnd 104
Criamos os endpoints, os models e estamos com o mongoose funcionando. Basta acessarmos as urls
abaixo e já teremos os resultados retornados:
http://localhost:3000/users
http://localhost:3000/rooms
Se precisarmos de mais rotas, para buscar dados diferentes, criaremos no decorrer do conteúdo.
O mais importante é que a API está criada e retornando dados que precisamos.
BackEnd 106
Após a instalação do pacote, começaremos a fazer as alterações e iniciaremos pelo arquivo api/app.js.
1 // Código já existente
2 var app = express();
3
4 // Código adicionado
5 var server = require('http').Server(app);
6 var io = require('socket.io')(server);
7
8 // Código alterado no final do arquivo (altere)
9 module.exports = app;
10
11 module.exports = {
12 app: app,
13 server: server
14 };
Arquivo ./bin/www.js
BackEnd 107
1 // já existente
2 mongoose.connect('mongodb://127.0.0.1:27017/chatschool_dev');
3
4 // código adicionado
5 app.use((req, res, next) => {
6 res.io = io;
7 next();
8 });
9
10 // já existente
11 require('./routes')(app);
Após adicionar este código, reinicie o servidor com npm start e já estará tudo pronto para continuar
o desenvolvendo com os servidores criados e aptos a retornar dados, utilizando o protocolo socket.io.
Já podemos começar a interagir, em tempo real, junto com o servidor próprio de websocket e também
webserver.
Criando Comunicação Via Socket
Enviando Mensagens
Já construímos a conexão junto ao servidor e começaremos a consumir os dados dentro da webapp.
A partir de agora utilizaremos o pacote socket.io para conseguirmos a conexão junto ao servidor de
websocket.
Em primeiro lugar faremos alguns carregamentos de scripts no arquivo webapp/src/index.html.
Veja abaixo:
Carregamos o script do socket.io via CDN e um arquivo chamado app.js, este arquivo ainda não
existe. O carregamento do socket.io é necessário para conseguirmos conectar ao servidor de API
juntamente com o arquivo app.js.
Dentro da pasta webapp/src/js, crie um arquivo chamado app.js. Veja o conteúdo do arquivo:
Por enquanto, deixaremos o evento socket.emit() comentado para testarmos se a conexão será bem
sucedida. Depois, enviaremos uma mensagem do frontend para o servidor.
Para fazermos o teste de conexão temos que preparar o reconhecimento do evento no arquivo
api/app.js. Veja o código adicionado:
Criando Comunicação Via Socket 109
1 // Código existente
2 app.use((req, res, next) => {
3 res.io = io
4 next()
5 })
6
7 // Código adicionado
8 io.on('connection', function () {
9 console.log('A new connection has been established')
10 })
Após adicionarmos este código, o servidor ficará aguardando uma conexão e quando o webapp
conectar ao servidor, pela url http://localhost:3000, o servidor dispara a mensagem no console.
O que precisamos fazer é executar a webapp através do gulp e depois rodar o servidor, novamente.
Veja os passos:
Se conseguirmos obter esta mensagem, significa que conseguimos trabalhar com esta conexão,
enviando e recebendo dados.
O próximo passo será enviarmos uma mensagem do usuário, que conectou no webapp, para o
servidor de API.
Precisaremos atribuir um id para o formulário presente no arquivo webapp/src/index.html.
Após receberemos o valor digitado através do seletor jQuery, o enviaremos através do comando
emit, utilizando o socket.io. Para isso altere o arquivo webapp/src/js/app.js.
Criando Comunicação Via Socket 110
Observem que o comando emit possui um identificador message e o evento on deve receber o
mesmo identificador, caso contrário a comunicação não será concluída.
Agora, basta fazer o teste de digitar uma mensagem no formulário e pressionar enter. Em seguida,
acesse o terminal e confira se recebeu a mensagem digitada. Caso tenha recebido, é porque já possui
uma conexão de sucesso e já consegue receber dados do frontend, no servidor.
Ao pressionar a tecla enter no fomulário, deverá receber o seguinte resultado no terminal:
Recebendo Mensagens
Com o envio de dados do frontend para o servidor funcionando, faremos o processo inverso,
tornando a comunicação de duas vias.
Receberemos a mensagem no servidor e a retornaremos, sem tratativa nenhuma. Poderíamos
submeter esta mensagem a alguma lógica, antes de retorná-la ao frontend.
Faremos o mesmo processo de emissão de mensagem utilizando o emit, só que agora do servidor
para o frontend. Veja a alteração no arquivo api/app.js:
Observe que estamos recebendo normalmente o evento de mensagem, assim como anteriormente,
só que gora emitimos a mesma mensagem para o frontend. O método responsável por isso é o
socket.emit, em que passamos um objeto com a propriedade message.
1 // Arquivo webapp/src/js/app.js
2 socket.on('message', function (data) {
3 var template = '<div class="col-xs-12 message">' +
4 ' <div class="avatar col-xs-6 col-md-1">' +
5 ' <h2>L</h2>' +
6 ' </div>' +
7 ' <p class="text col-xs-6 col-md-11">'+ data.message +'</p>' +
8 '</div>'
9
10 $('.conversation').append(template)
11 })
Veja que criamos um template igual ao que temos no arquivo index.html para adicionarmos as
mensagens retornadas pelo servidor.
O evento que recebe a mensagem é o mesmo que recebe no servidor, ou seja, socket.on.
Reinicie o servidor API para que as alterações sejam efetivadas e depois, teste o envio de mensagens.
Após teclar enter, a mensagem vai até o servidor que, por sua vez retorna a mesma mensagem para
o frontend. Recebemos e adicionamos um elemento completo de html, que trata-se do template e
adicionamos a classe conversation, em que ficam as outras mensagens adicionadas, anteriormente,
no layout.
Teste o envio de mensagens após reiniciar o servidor e verá que já temos o envio e o recebimento de
mensagens entre o servidor e frontend.
Comunicação em Grupo
Liberando CORS
Ensinaremos como liberar o Cross-Domain para garantir maior segurança no browser, visando
evitar que todos os domínios tenham acesso a algum ponto da aplicação.
Para trabalhar entre domínios diferentes é necessário fazer essa liberação, porque isso já vem
habilitado no header por padrão .
Estamos trabalhando com três projetos totalmente distintos, que são: admin, api e webapp.
Precisamos que a api se comunique com o webapp, ou seja, envie dados para o frontend e esta
comunicação será feita via ajax. Para este processo o express tem uma forma muito simples de
trabalhar, utilizando um pacote chamado CORS. Com este pacote, conseguimos fazer a liberação do
cross-domain de uma forma muito simples.
Basta instalarmos este pacote na API e ela estará disponível para todos os ambientes, ou seja, admin
e webapp.
Comando Descrição
npm install cors –save Instalando pacote com npm
var cors = require(‘cors’); Adicionando carregamento do pacote
app.use(cors()); Adicionando middleware
O cors é um middleware como qualquer outro e deverá ser utilizado e aplicado como tal. Utilizando-
o como um middleware, liberamos o acesso para todo e qualquer domínio e logo depois já
conseguiremos executar o ajax de uma forma tranquila e sem erros de cross-domain.
Você poderia limitar os tipos de métodos permitidos, passando um objeto de configuração dentro
do método cors(). Podemos, também, limitar as urls que seriam acessíveis, mas em nosso caso
Comunicação em Grupo 114
aceitaremos todos os tipos de métodos. Portanto, não passaremos nenhum objeto como parâmetro,
como pode ver no código acima.
A configuração padrão de instalação é suficiente para o projeto. Por este motivo, não passaremos
nenhuma configuração extra para o pacote, apenas o utilizaremos como um middleware padrão.
Mostrando Grupos
Começaremos a capturar os dados da API para exibi-los na aplicação. Deste modo, poderemos
prosseguir com o objetivo que é enviar e receber mensagens no chat.
Antes de continuarmos, teremos que fazer algumas alterações no arquivo webapp/src/js/app.js.
Veja as alterações:
1 $(document).ready(function () {
2
3 (function () {
4 var getRooms = function () {
5 return $.get('//localhost:3000/rooms', function (data) {
6 if(!data.status){
7 return
8 }
9
10 var rooms = data && data.rooms
11
12 var titleTpl = '<li class="list-group-item title">' +
13 ' <h4>Canais(' + rooms.length + ')</h4>' +
14 '</li>'
15
16 $('.channels').append(titleTpl)
17
18 rooms.forEach(function (room, index) {
19 var roomTpl = '<li class="list-group-item" channel="' + room._i\
20 d + '"><i class="fa fa-comment-o"></i> ' + room.name + '</li>'
21 $('.channels').append(roomTpl)
22 })
23 })
24 }
25
26 getRooms()
27 })()
28
29 var socket = io('//localhost:3000')
Comunicação em Grupo 115
30
31 $('#message').keypress(function (e) {
32 if(e.which == 13){
33 var val = $('#message').val()
34
35 socket.emit('message', {
36 message: val
37 })
38
39 return false
40 }
41 })
42
43 socket.on('message', function (data) {
44 var template = '<div class="col-xs-12 message">' +
45 ' <div class="avatar col-xs-6 col-md-1">' +
46 ' <h2>L</h2>' +
47 ' </div>' +
48 ' <p class="text col-xs-6 col-md-11">'+ data.message +'</p>' +
49 '</div>'
50
51 $('.conversation').append(template)
52 })
53
54 })
A primeira alteração que fizemos foi adicionar os métodos anteriores dentro de uma função que só
será executada assim que o documento estiver carregado completamente.
Para isso, utilizamos a estrutura abaixo:
1 $(document).ready(function () {
2
3 })
1 (function () {
2 var getRooms = function () {
3 return $.get('//localhost:3000/rooms', function (data) {
4 if(!data.status){
5 return
6 }
7
8 var rooms = data && data.rooms
9
10 var titleTpl = '<li class="list-group-item title">' +
11 ' <h4>Canais(' + rooms.length + ')</h4>' +
12 '</li>'
13
14 $('.channels').append(titleTpl)
15
16 rooms.forEach(function (room, index) {
17 var roomTpl = '<li class="list-group-item" channel="' + room._id + \
18 '"><i class="fa fa-comment-o"></i> ' + room.name + '</li>'
19 $('.channels').append(roomTpl)
20 })
21 })
22 }
23
24 getRooms()
25 })()
Criamos a função getRooms e a chamamos no final da execução. Assim, esta função é executada a
cada requisição, trazendo os dados atualizados de cada sala.
Para pegarmos os dados das salas, acessamos a API. Veja abaixo o método e a url de acesso:
13
14 rooms.forEach(function (room, index) {
15 var roomTpl = '<li class="list-group-item" channel="' + room._id + '"><i cl\
16 ass="fa fa-comment-o"></i> ' + room.name + '</li>'
17 $('.channels').append(roomTpl)
18 })
19 })
Esta função testa se existe o atributo status na requisição, pois se algo der errado paramos a execução.
Se a requisição obtiver sucesso, continuamos a execução e confeccionamos os valores retornados em
uma variável chamada rooms.
Logo depois, pegamos o template de listagem, presente no arquivo webapp/src/index.html, para
que possamos adicionar o título de canais utilizando a mesma estrutura. Guardamos este template
em uma variável chamada titleTpl e, em seguida, adicionamos o elemento dinamicamente dentro
do bloco que contém a classe channels. Desta foma, temos o título e a quantidade de salas de forma
dinâmica. Se adicionarmos mais salas, o título mostrará a quantidade exata de salas criadas. No
HTML este número de salas era fixo.
Fizemos o mesmo para a listagem de salas.
Pegamos o template do HTML e transportamos para uma variável chamada roomTpl e esta estrutura
é repetida em um forEach para que sejam impressas todas as salas cadastradas no banco de dados,
com os nomes reais de cadastro.
Achamos necessário adicionar um atributo chamado channel com o id de cada sala, para podermos
diferenciar uma sala da outra. Este processo será útil para que cada sala tenha identidade única.
Você pode utilizar somente um atributo personalizado chamado channel, como fizemos:
Ou, pode utilizar o padrão recomendado pela W3C, em que adicionamos atributos personalizados
utilizando o prefixo data-. Ficaria da seguinte forma:
Após realizar esta alteração, você estará pronto para continuar o desenvolvimento.
Recarregue a página do chat para conferir os dados das salas vindos do banco de dados. Inspecione
o elemento HTML na ferramenta de desenvolvedor do navegador, para conferir o id de cada sala
sendo impresso em seu atributo.
Mensagens em Grupo
Refatoraremos o código e utilizaremos a base que já temos de desenvolvimento, para finalizarmos
esta parte de mensagens em grupo.
O primeiro passo para que alguém possa enviar uma mensagem dentro de uma determinada sala será
utilizar a feature do socket.io que se chama socket.room. No momento, as mensagens são enviadas
mas aparecem em qualquer sala, pois ainda não definimos esta separação.
Em qualquer projeto em que haja distinção de sala ou setor, teremos que utilizar uma ferramenta
para fazer a separação. Assim, a aplicação sabe para qual sala estamos enviando a mensagem, caso
contrário a mensagem não teria um destino correto. Utilizaremos socket.room para este fim.
Para conseguirmos diferenciar uma sala da outra já adicionamos, anteriormente, um identificador
através de um atributo personalizado. Informaremos este id para o servidor assim que o usuário
clicar em algum item da listagem de salas. O servidor, por sua vez, se encarregará de fazer com que
o usuário acesse esta sala e possa fazer o envio de mensagens.
O primeiro passo para fazermos esta comunicação e conexão com a sala via id é criando o evento
de click no servidor. Adicione o código abaixo ao arquivo webapp/src/js/app.js.
1 // Código existente
2 var socket = io('//localhost:3000')
3
4 $('.channels').on('click', '.channel', function(){
5 var roomId = $(this).attr('channel')
6
7 console.log(roomId)
8
9 socket.emit('join room', {
10 room: roomId
11 })
12
13 return false
14 })
Veja que estamos assistindo um evento de click em uma classe chamada channel. Porém esta classe
ainda não está no código gerado pelo loop das salas. Portanto, adicione conforme o código abaixo.
Comunicação em Grupo 119
Agora, sempre que clicarmos na classe channel, teremos o evento emitido para o servidor.
Criamos uma variável chamada roomId e atribuímos o valor do atributo personalizado. Neste caso
é o id da sala que está vindo do MongoDB.
Veja como acompanhar o processo:
1. logamos no console o valor do id capturado. Se clicar, poderá ver o evento ser disparado.
2. emitimos um evento via socket, em que passamos um objeto room com o id da sala.
Para recebermos este evento no servidor, teremos que criar o evento receptor. Abra o arquivo
api/app.js e adicione o seguinte código:
O evento connection utiliza a instância socket diretamente, para disparar os eventos de comunica-
ção.
O próximo passo é recebermos o evento join room, que enviamos da aplicação webapp.
Comunicação em Grupo 120
1 // Código existente
2 var socket = io('//localhost:3000')
3
4 socket.on('join room', function (data) {
5 socket.room = data.room
6 socket.join(socket.room)
7
8 socket.emit('joined room', data)
9 })
Observe que estamos recebendo e atribuindo uma função callback para atribuir uma propriedade
room ao objeto socket, onde passamos o valor do id, que passamos no evento anterior. O id está
dentro de data.room.
Depois, utilizamos o método join para acessarmos a sala, passando o id que acabamos de receber.
O próximo passo é enviar um outro evento, informando o webapp que conseguimos acessar a sala.
Enviamos um evento, chamado joined room, passando todos os dados processados em uma variável
data.
Agora, temos que receber este evento novamente, em webapp.
1 // Código existente
2 var socket = io('//localhost:3000')
3
4 // código adicionado
5 var currentRoom = undefined
6
7 // Códico existente
8 $('#message').on('keypress', function (e) {
9 // código
10 })
11
12 // código adicionado
13 socket.on('joined room', function (data) {
14 currentRoom = data.room
15 console.log(currentRoom)
16 })
1 $(document).ready(function () {
2
3 (function () {
4 var getRooms = function () {
5 return $.get('//localhost:3000/rooms', function (data) {
6 if(!data.status){
7 return
8 }
9
10 var rooms = data && data.rooms
11
12 var titleTpl = '<li class="list-group-item title">' +
13 ' <h4>Canais(' + rooms.length + ')</h4>' +
14 '</li>'
15
16 $('.channels').append(titleTpl)
17
18 rooms.forEach(function (room, index) {
19 var roomTpl = '<li class="list-group-item channel" channel="' +\
20 room._id + '"><i class="fa fa-comment-o"></i> ' + room.name + '</li>'
21 $('.channels').append(roomTpl)
22 })
23 })
24 }
Comunicação em Grupo 122
25
26 getRooms()
27 })()
28
29 var socket = io('//localhost:3000')
30 var currentRoom = undefined
31
32 // Events jQuery
33 $('.channels').on('click', '.channel', function(){
34 var roomId = $(this).attr('channel')
35
36 console.log(roomId)
37
38 socket.emit('join room', {
39 room: roomId
40 })
41
42 return false
43 })
44
45 $('#message').on('keypress', function (e) {
46 if(e.which == 13 || e.keyCode == 13){
47 var message = $('#message').val()
48
49 if(!message){
50 return
51 }
52
53 socket.emit('message room', {
54 message: message,
55 room: currentRoom
56 })
57
58 var msgTpl = '<div class="col-xs-12 message">' +
59 ' <div class="avatar col-xs-6 col-md-1">' +
60 ' <h2>L</h2>' +
61 ' </div>' +
62 ' <p class="text col-xs-6 col-md-11">'+ message +'</p>\
63 ' +
64 '</div>'
65
66 $('.conversation').append(msgTpl)
67 $('#message').val('')
Comunicação em Grupo 123
68
69 return false
70 }
71 })
72
73 // Events sockets
74 socket.on('joined room', function (data) {
75 currentRoom = data.room
76 console.log(currentRoom)
77 })
78
79 socket.on('messaged room', function (data) {
80 if(!data.message){
81 return
82 }
83
84 var msgTpl = '<div class="col-xs-12 message">' +
85 ' <div class="avatar col-xs-6 col-md-1">' +
86 ' <h2>L</h2>' +
87 ' </div>' +
88 ' <p class="text col-xs-6 col-md-11">'+ data.message +'</p\
89 >' +
90 '</div>'
91
92 $('.conversation').append(msgTpl)
93 })
94 })
O wich funciona na maioria dos navegadores, mas no Internet Explorer teríamos problema.
Adicionamos o keyCode para que todos os navegadores reconheçam o evento.
Depois, verificamos se existe mensagem. Se não tiver mensagem, interrompemos a operação, para
evitarmos o envio de mensagens vazias sem resultado algum para o servidor.
1 if(!message){
2 return
3 }
Após a verificação, emitimos um evento chamado message room, que anteriormente se chamava
message, passando a mensagem capturada no campo input e também a sala corrente.
1 socket.emit('message room', {
2 message: message,
3 room: currentRoom
4 })
Depois, criamos o template e adicionamos a mensagem na tela do usuário que enviou e limpamos o
campo de texto para que possa ser adicionada outra mensagem.
Comunicação em Grupo 125
Recebemos o evento message room e disparamos uma resposta com broadcast, que na verdade
disparará o mesmo retorno para todas as salas de mesmo acesso. Todos os usuários que estiverem
na mesma sala, receberão a mensagem ao mesmo tempo.
Veja como fizemos:
1. utilizamos o método broadcast junto com o método in, em que passamos o id da sala
Comunicação em Grupo 126
2. disparamos o evento emit com o nome de message room com a mensagem e a sala.
O processo está, praticamente, completo. Falta receber este evento, novamente, no webapp cujo
código já foi adicionado acima:
Veja que verificamos se a mensagem existe, antes de continuarmos o processo. Caso exista, formamos
o mesmo template anterior e adicionamos dentro da classe conversation. Desta forma, este processo
será executado em todas as salas de mesmo id e, consequentemente, todos receberão a mensagem
enviada.
Veja que o processo com socket geralmente é executado através de um envio, um recebimento e um
retorno.
Já tínhamos feito o mesmo processo, porém, retornávamos apenas para o mesmo usuário que havia
enviado a mensagem. Agora, utilizamos o broadcast para que o retorno seja enviado em cascata,
afetando todos os usuários da mesma sala.
Depois de realizar estas alterações, reinicie os serviços, tanto no servidor quanto a aplicação webapp,
para certificar que está tudo atualizado. Faça testes de envio de mensagens abrindo várias janelas e
cada uma acessando uma sala diferente, para ver os eventos disparados. Acesse salas iguais para ver
os eventos enviados para as salas, em navegadores diferentes.
Com tudo funcionando, você já pode apagar as mensagens que havíamos colocado de teste para o
layout. Veja o código abaixo:
Comunicação em Grupo 127
Melhorando usabilidade
Como adicionamos no layout o nome da sala manualmente, alteramos a sala e não temos nenhuma
alteração no layout, nenhuma modificação informando qual sala estamos.
Melhoraremos este processo, adicionando o nome de cada sala ao selecioná-la.
O primeiro passo será adicionar outro atributo personalizado que guardará o nome da sala. No
mesmo lugar que adicionamos o atributo de identificação, adicionaremos mais um atributo, veja
abaixo:
Comunicação em Grupo 128
Depois, temos que resgatar este valor e enviar para o servidor, assim como fizemos com o id da sala:
Após enviar os dados com o evento join room, não precisaremos alterar o evento de recebimento,
porque ele já retorna todos os dados para o webapp, através do evento joined room.
O que precisamos fazer é imprimir no HTML o nome da sala, através deste dado enviado. O primeiro
passo é apagar o valor fixo que imprimíamos no HTML, veja abaixo:
Comunicação em Grupo 129
Desta forma, adicionamos os valores, dinamicamente. Lembrando que você pode precisar subir os
servidores, novamente, caso não tenham as alterações aplicadas.
Criaremos um botão para sair da sala, já que estamos criando um acesso temos que criar uma saída
desta sala.
Para isso, adicione um botão no HTML logo após o código que adicionamos o nome da sala, abaixo
da classe username.
Adicionamos um botão com as classes btn btn-default pull-right, que caracterizam um botão
default do Bootstrap e alinhamos a direita, para que não fique junto com o nome. Além disso,
atribuímos um id para que possamos adicionar eventos a este botão.
Agora, resgataremos o id da sala para enviar para o servidor e efetuarmos a saída da sala, através
de um novo evento que chamaremos de leave room. Adicione ao arquivo
Comunicação em Grupo 130
1 $('#btn_leave').on('click', function(e){
2 var roomId = $(this).attr('channel')
3
4 socket.emit('leave room', {
5 room: roomId
6 })
7
8 return false
9 })
Como emitimos um novo evento de retorno, temos que receber este evento em webapp.
1 (function () {
2 $('.chatbox').hide()
3 var getRooms = function () {
4 // Conteúdo da função
5 }
6
7 getRooms()
8 })()
Ocultamos o conteúdo e temos que adicionar o evento show, para que seja mostrado ao clicar nas
salas. Adicionaremos este método ao evento de retorno de acesso às salas.
Além de mostrar a chatbox, limpamos as mensagens já existentes, para que uma mensagem não seja
mostrada na mudança de uma sala para outra.
Depois, precisamos fazer com que seja ocultado, novamente, quando o usuário clicar no botão Sair.
Adicionaremos o método hide ao evento de retorno de saída das salas.
1. fizemos uma requisição ajax para o endereço //localhost:3000/users, assim como na listagem
de salas.
2. verificamos se existe a propriedade status no resultado retornado, se não existir, encerramos o
processo.
3. exstindo status, atribuímos o resultado a uma variável chamada users, esta mesma variável
conterá o resultado completo e, também, os resultados de usuários.
4. fizemos um forEach para percorrer todos os resultados do banco e imprimir todos os resultados,
utilizando o template que pegamos do HTML.
Lembrando que o HTML estava fixo e você deverá remover o item, deixando apenas o título,
conforme código abaixo:
Finalizando a Chatbar 133
Observe que o forEach está adicionando os elementos através de um append na classe messages.
$('.messages').append(userTpl)
Veja que adicionamos a classe user junto com a classe list-group-item e também, os atributos user
e username. Os valores são concatenados, dinamicamente, através da variável user do forEach.
O próximo passo será criar o evento jQuery ao clicar em cada item da listagem, ou seja, em cada
usuário.
Quando o usuário clicar em algum membro da sala, criamos duas variáveis: user e username. Para
estas variáveis atribuímos o id e o username, que vêm do banco de dados.
Resgatamos os dados, através dos atributos personalizados que criamos.
Em seguida, emitimos um evento via socket, chamado join user, passando um objeto com user e
username. Como você já sabe, quando emitimos um evento temos que recebê-lo em nosso servidor.
Alterando o arquivo api/app.js.
Ao recebermos os dados, criamos uma propriedade chamada user no objeto socket, passando o id
do usuário que foi passado através da variável data. Em seguida, acessamos o usuário através do
método join, passando o id do usuário como parâmetro.
Depois, emitimos um outro evento como resposta, chamado joined user. Receberemos este evento
em webapp.
Finalizando a Chatbar 135
1 // Código existente
2 var currentRoom = undefined
3
4 // Código adicionado
5 var currentUser = undefined
6
7 socket.on('joined user', function (data) {
8 currentUser = data.user
9 $('.username').html('@' + data.username)
10 $('.conversation').html('')
11 $('.chatbox').show()
12 })
Tivemos que mostrar o bloco porque, assim como nas rooms, os blocos começam ocultos, inicial-
mente.
Lembre-se de criar a variável currentUser com valor inicial undefined, assim como fizemos para
currentRoom.
13 })
14 }
15
16 if(currentRoom){
17 socket.emit('message room', {
18 message: message,
19 room: currentRoom
20 })
21 }
22
23 var msgTpl = '<div class="col-xs-12 message">' +
24 ' <div class="avatar col-xs-6 col-md-1">' +
25 ' <h2>L</h2>' +
26 ' </div>' +
27 ' <p class="text col-xs-6 col-md-11">'+ message +'</p>' +
28 '</div>'
29
30 $('.conversation').append(msgTpl)
31 $('#message').val('')
32
33 return false
34 }
35 })
Veja que o código é muito parecido com o anterior, apenas estamos fazendo dois testes lógicos para
saber qual evento emitir.
Observem que, trata-se de uma mensagem entre usuários. Estamos chamando o evento message
user, caso contrário chamamos o evento message room.
Finalizando a Chatbar 137
1 if(!currentRoom){
2 socket.emit('message user', {
3 message: message,
4 user: currentUser
5 })
6 }
7
8 if(currentRoom){
9 socket.emit('message room', {
10 message: message,
11 room: currentRoom
12 })
13 }
Esta foi a alteração principal neste bloco, o restante continua igual. Vale a pena lembrar que cada
evento está passando a mensagem, que é a mesma para ambos, e o currentUser ou currentRoom, de
acordo com cada contexto.
Como emitimos um evento via socket, temos que receber o evento no servidor, assim como todos os
processos até agora.
Repare que ambas executam o evento broadcast, que utilizamos em salas e o código é realmente
muito parecido. Em ambos os casos retornamos um mesmo evento de retorno chamado messaged,
pois utilizamos o mesmo código para envio de mensagem.
Anteriormente, o evento de retorno de rooms se chamava messaged room, basta renomear para
apenas messaged, para reaproveitamento de código.
Para finalizarmos o processo, basta recebermos este evento no webapp.
Finalizando a Chatbar 138
Note que anteriormente, este evento tinha o identificador messaged room, porque ele servia
apenas para receber o evento de mensagem para salas. Como estamos reaproveitando o código o
renomeamos para messaged, desta forma ele recebe tanto o evento de mensagem para rooms quanto
para users.
Depois de realizar todas estas alterações e adaptações, reinicie os servidores e faça o teste de envio
de mensagens para usuários.
Lembre-se de abrir várias abas do navegador para visualizar o evento broadcast funcionando
corretamente.
Conclusão
O objetivo de criar o chat em tempo real, foi concluído.
Criamos uma aplicação em tempo real, com poucos recursos, utilizando apenas o Node.js, jQuery e
Socket.io.
Este projeto prático, apesar se ser simples, abrangeu diversos conceitos que são utilizados, inclusive,
em projetos de grande escala, como:
• Node.js
• Framework express
• Protocolo HTTP e websocket
• API
• Socket.io
• Template engine handlebars
• Javascript e jQuery
Socket.io nos auxiliou ao trabalharmos com Node, juntamente com o protocolo websocket.
O handlebars é um template engine muito versátil, que nos permite criar helpers, views customizadas
e partials views.
Utilizamos um pouco de javascript, mas focamos mais no uso do jQuery, que apesar de estar um
pouco esquecido, ainda é muito bom para quem está iniciando e nos possibilita fazer tudo que os
outros frameworks mais modernos fazem.
Caso tenha ficado alguma dúvida, deixe as questões no fórum da School of Net, que um de nossos
tutores responderá o mais rápido possível.