Fala galera, beleza?

No último post falei um pouco sobre refresh periódico com o AngularJS, quem ainda não leu ta perdendo, viu!? Corre e ler… =)

Após esse post, algumas pessoas falaram da questão do porquê não usei websocket!

“Pô Gabriel, websocket é muito melhor para um chat, não?”

Sim, concordo. Mas a intenção do post anterior era somente falar sobre refresh periódico. Assim, como vocês que mandam aqui mesmo, vamos falar um pouco sobre comunicação bidirecional? Oxe, e não é sobre websocket? Calma meu fi, leia o resto. =)

Já ouviu falar sobre o socket.io? É por causa dele que mencionei a comunicação bidirecional e não o websocket!

Porquê o socket.io?

Se você leu o post falando sobre refresh periódico, mencionei o pooling que o twitter faz para verificar se há novas atualizações para o usuário. Porém, essa estratégia pode não ser eficaz para outros cenários. Por exemplo, um chat.

Nos dias de hoje a maioria dos browsers já implementam o suporte a WebSockets (duvida? confere no Can I Use), porém a biblioteca do socket.io abstrai isso para nós! Devido essa abstração, resolvi falar sobre ele e não sobre websocket.

O Socket.io é feito em JavaScript e possui uma API muito simples de ser utilizada. Além disso, ele funciona em qualquer plataforma, navegador ou dispositivo. Ou seja, pode usar e abusar que ele funciona! Já usou o Trello? Ele usa socket.io. =)

Uma das features bacanas do socket.io é a lógica de reconexão que ele provêm, caso isso não seja útil para você dê uma olhada no engine.io que é a camada de websocket do socket.io.

No site tem alguns demos bem bacanas. O código desses demos estão disponíveis no github e servem de exemplos para nós.

Então, vamos ao código?

Para o exemplo de hoje, escrevi o backend utilizando o nodejs. Além disso, usei o framework express e socket.io no lado servidor. Já no lado cliente, o framework adotado foi o angularjs e o socket.io.

Imagine que você vai a um bar e pede uma feijoada + uma cerveja bem gelada. A cerveja, as vezes, vem rapidinho, mas a comida… Para acabar com a agonia do cliente bebedor, vamos dar a ele o prazer de saber como anda o status do seu pedido.

Os fontes estão no github e o exemplo pode ser visto rodando no heroku.

Vamos ao código?

Backend

O gist abaixo possui o código responsável pelo lado servidor. Basicamente ele implementa a solicitação dos pedidos via socket.io e realiza em um determinado intervalo de tempo o atendimento dos pedidos. Além disso, utilizei a API do express para informar que irei utilizar arquivos estáticos que estão dentro do diretório public (linha 9), pois o frontend está lá dentro

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
'use strict';

var express = require('express'),
    app = express(),
    http = require('http').Server(app),
    io = require('socket.io')(http),
    port = process.env.PORT || 5000;

app.use(express.static('public'));

var pedidos = [];
var id = 0;

io.on('connection', function(socket){
  io.to(socket.id).emit('loja pedidos', pedidos);

  socket.on('loja enviar', function(pedido){
    pedido.id = id++;
    pedido.data = new Date();
    pedido.status = 'Solicitado';
    pedidos[pedido.id] = pedido;
    io.emit('loja pedido', pedido);
    filaSolicitado(pedido.id);
  });

  function filaSolicitado(id){
    var pedido = pedidos[id]; 
    if(verificaSePodeExecutar(pedido.data,3000)){
      pedido.status = 'Em atendimento';  
      io.emit('loja pedido', {id: pedido.id, status: pedido.status});
      filaEmAtendimento(id); 
    }else{
      setTimeout(filaSolicitado,1000, id);  
    }
  }

  function filaEmAtendimento(id){
    var pedido = pedidos[id]; 
    if(verificaSePodeExecutar(pedido.data,7000)){
      pedido.status = 'Atendido';  
      io.emit('loja pedido', {id: pedido.id, status: pedido.status});
    }else{
      setTimeout(filaEmAtendimento,1000, id);  
    }
  }

  function verificaSePodeExecutar(date,time){
    var agora = new Date();
    var diff = (agora - date);
    return diff > time;
  }

});

http.listen(port, function(){
  console.log('Servidor rodando na porta:'+port);
});

Como dito antes, a API do socket.io é bem simples. Na linha 14 estamos abrindo a conexão do socket com o client. Então, quando você acessar a tela dos pedidos o frontend abrirá essa conexão com o servidor. Na linha 15, estou enviando todos os pedidos já cadastrados somente para um determinado cliente.

Já na linha 17, o socket estará ouvindo o evento “loja enviar“. Ele é o evento responsável por receber os pedidos enviados pelo client. Sempre que um novo pedido é recebido pelo servidor, ele será tratado (recebe os atributos id, status e data) e em seguida (linha 22) o pedido é enviado para todos os sockets com conexão aberta. Os demais métodos filaSolicitadofilaEmAtendimento são os responsáveis pela lógica para mudança de status e envio da atualização para o cliente.

Não entrei em detalhes sobre o código, pois acredito que está tranquilo para atendê-lo. Caso tenha alguma dúvida sobre a API do socket, favor consulte a documentação ou deixe um comentário que responderei com maior prazer.

Frontend

O código abaixo, representa o controlador responsável pela view no AngularJS. Tentei deixar o código bem simples para exemplificar melhor. Perceba que também usei o socket.io no lado cliente.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
(function() {
'use strict';

angular.module('app')
	.controller('LojaController', function($scope) {
		var socket = io();
		var vm = this;
		vm.pedido = {};
		vm.pedidos = [];
		
		vm.enviar = enviar;
		
		init();

		function init(){
			socket.on('loja pedidos', function(pedidos){
				vm.pedidos = pedidos;
				$scope.$apply();
			});
			socket.on('loja pedido', function(pedido){
				var encontrou = false;
				for(var x = 0; x < vm.pedidos.length && !encontrou; x++){
					if(vm.pedidos[x].id === pedido.id){
						vm.pedidos[x].status = pedido.status;
						encontrou = true;
					}
				}
				if(!encontrou){
					vm.pedidos.push(pedido);
				}
				$scope.$apply();
			});

		}

		function enviar() {
		    socket.emit('loja enviar', vm.pedido);
		    vm.pedido.item = '';
		}

	});
})();

De acordo com o gist, na linha 6 ocorre a abertura da conexão com o servidor. Note que não passei nenhum parâmetro indicando a URL do socket que quero abrir a conexão, pois, por default, ele irá apontar para o meu endereço atual.

Já as linhas 16 e 20, estão os listeners que irão receber os dados enviados pelo servidor. O primeiro é usado quando o servidor envia o array com todos os pedidos já cadastrados. Já o segundo, é utilizado quando o servidor envia uma atualização do status de um pedido.

Veja que na linha 31 precisei utilizar o $scope.$apply, isso é necessário devido a atualização do pedido ser realizada fora do escopo do Angular. Então, é preciso informá-lo que ele deve ser atualizado.

Por fim, a função enviar (linha 36) é a responsável por enviar os dados do cliente para o servidor. Esse envio é feito através do evento “loja enviar” no socket.

[Dica] É possível debugar o que está sendo trafegado pelo socket. Para isso, abra o console do seu browser (F12 abestado!) e digite localStorage.debug = “*”; Mais informações podem ser encontradas na documentação oficial.

As informações de como rodar o projeto deixei no README.md.

Então é isso galera, espero que curtam o post.

Como estamos no final do ano, desejo a todos boas festas e que 2016 venha com tudo e nos traga bons códigos!

Abraços e até a próxima.