Introdução
O AngularJS é um framework JavaScript incrível que oferece vinculação de dados bidirecional fácil de usar e rápida, um sistema de diretivas poderoso que permite criar componentes personalizados reutilizáveis e muito mais. O Socket.IO é um wrapper e um polyfill para WebSockets que facilita o desenvolvimento de aplicativos em tempo real em vários navegadores. A propósito, as duas funcionam muito bem juntas.
Já escrevi sobre como criar um app AngularJS com o Express, mas desta vez vou falar sobre como integrar o Socket.IO para adicionar recursos em tempo real a um aplicativo AngularJS. Neste tutorial, vou mostrar como escrever um app de mensagens instantâneas. Ele é baseado no meu tutorial anterior (que usa uma pilha node.js semelhante no servidor). Portanto, se você não conhece o Node.js ou o Express, recomendo que o consulte primeiro.
Como sempre, você pode acessar o produto final no GitHub.
Pré-requisitos
Há um pouco de boilerplate para configurar e integrar o Socket.IO ao Express. Por isso, criei a Angular Socket.IO Seed.
Para começar, clone o repositório angular-node-seed do GitHub:
git clone git://github.com/btford/angular-socket-io-seed my-project
ou faça o download como um arquivo ZIP.
Depois de ter a semente, você precisa pegar algumas dependências com o npm. Abra um terminal no diretório com a semente e execute:
npm install
Com essas dependências instaladas, é possível executar o app de esqueleto:
node app.js
e confira no seu navegador em http://localhost:3000
para garantir que a semente esteja funcionando conforme o esperado.
Como decidir os recursos do app
Há várias maneiras diferentes de escrever um aplicativo de chat. Vamos descrever os recursos mínimos que o nosso terá. Haverá apenas uma sala de chat para todos os usuários. Os usuários podem escolher e mudar o nome, mas ele precisa ser exclusivo. O servidor vai aplicar essa exclusividade e anunciar quando os usuários mudarem de nome. O cliente precisa expor uma lista de mensagens e uma lista de usuários que estão na sala de chat.
Um front-end simples
Com essa especificação, podemos criar um front-end simples com Jade que fornece os elementos de interface necessários. Abra views/index.jade
e adicione este código dentro de block body
:
div(ng-controller='AppCtrl')
.col
h3 Messages
.overflowable
p(ng-repeat='message in messages') :
.col
h3 Users
.overflowable
p(ng-repeat='user in users')
.clr
form(ng-submit='sendMessage()')
| Message:
input(size='60', ng-model='message')
input(type='submit', value='Send')
.clr
h3 Change your name
p Your current user name is
form(ng-submit='changeName()')
input(ng-model='newName')
input(type='submit', value='Change Name')
Abra public/css/app.css
e adicione o CSS para fornecer colunas e transbordamentos:
/* app css stylesheet */
.overflowable {
height: 240px;
overflow-y: auto;
border: 1px solid #000;
}
.overflowable p {
margin: 0;
}
/* poor man's grid system */
.col {
float: left;
width: 350px;
}
.clr {
clear: both;
}
Como interagir com o Socket.IO
Embora o Socket.IO exponha uma variável io
no window
, é melhor encapsular essa variável no sistema de injeção de dependências do AngularJS. Então, vamos começar escrevendo um serviço para agrupar o objeto socket
retornado pelo Socket.IO. Isso é ótimo, porque vai facilitar muito o teste do controlador mais tarde. Abra public/js/services.js
e substitua o conteúdo por:
app.factory('socket', function ($rootScope) {
var socket = io.connect();
return {
on: function (eventName, callback) {
socket.on(eventName, function () {
var args = arguments;
$rootScope.$apply(function () {
callback.apply(socket, args);
});
});
},
emit: function (eventName, data, callback) {
socket.emit(eventName, data, function () {
var args = arguments;
$rootScope.$apply(function () {
if (callback) {
callback.apply(socket, args);
}
});
})
}
};
});
Observe que envolvemos cada callback de soquete em $scope.$apply
. Isso informa ao AngularJS que ele precisa verificar o estado do aplicativo e atualizar os modelos se houver uma mudança após a execução do callback transmitido a ele. Internamente, o $http
funciona da mesma maneira: após alguns retornos de XHR, ele chama $scope.$apply
para que o AngularJS possa atualizar as visualizações.
Esse serviço não envolve toda a API Socket.IO (isso é deixado como um exercício para o leitor ;P ). No entanto, ele abrange os métodos usados neste tutorial e deve apontar a direção certa se você quiser expandir. Posso voltar a escrever um wrapper completo, mas isso está fora do escopo deste tutorial.
Agora, no controlador, podemos solicitar o objeto socket
, assim como faríamos com $http
:
function AppCtrl($scope, socket) {
/* Controller logic */
}
No controlador, vamos adicionar a lógica para enviar e receber mensagens. Abra js/public/controllers.js
e substitua o conteúdo por este:
function AppCtrl($scope, socket) {
// Socket listeners
// ================
socket.on('init', function (data) {
$scope.name = data.name;
$scope.users = data.users;
});
socket.on('send:message', function (message) {
$scope.messages.push(message);
});
socket.on('change:name', function (data) {
changeName(data.oldName, data.newName);
});
socket.on('user:join', function (data) {
$scope.messages.push({
user: 'chatroom',
text: 'User ' + data.name + ' has joined.'
});
$scope.users.push(data.name);
});
// add a message to the conversation when a user disconnects or leaves the room
socket.on('user:left', function (data) {
$scope.messages.push({
user: 'chatroom',
text: 'User ' + data.name + ' has left.'
});
var i, user;
for (i = 0; i < $scope.users.length; i++) {
user = $scope.users[i];
if (user === data.name) {
$scope.users.splice(i, 1);
break;
}
}
});
// Private helpers
// ===============
var changeName = function (oldName, newName) {
// rename user in list of users
var i;
for (i = 0; i < $scope.users.length; i++) {
if ($scope.users[i] === oldName) {
$scope.users[i] = newName;
}
}
$scope.messages.push({
user: 'chatroom',
text: 'User ' + oldName + ' is now known as ' + newName + '.'
});
}
// Methods published to the scope
// ==============================
$scope.changeName = function () {
socket.emit('change:name', {
name: $scope.newName
}, function (result) {
if (!result) {
alert('There was an error changing your name');
} else {
changeName($scope.name, $scope.newName);
$scope.name = $scope.newName;
$scope.newName = '';
}
});
};
$scope.sendMessage = function () {
socket.emit('send:message', {
message: $scope.message
});
// add the message to our model locally
$scope.messages.push({
user: $scope.name,
text: $scope.message
});
// clear message box
$scope.message = '';
};
}
Este aplicativo vai ter apenas uma visualização. Portanto, podemos remover o roteamento de public/js/app.js
e simplificá-lo para:
// Declare app level module which depends on filters, and services
var app = angular.module('myApp', ['myApp.filters', 'myApp.directives']);
Como gravar o servidor
Abra routes/socket.js
. Precisamos definir um objeto para manter o estado do servidor, para que os nomes dos usuários sejam exclusivos.
// Keep track of which names are used so that there are no duplicates
var userNames = (function () {
var names = {};
var claim = function (name) {
if (!name || userNames[name]) {
return false;
} else {
userNames[name] = true;
return true;
}
};
// find the lowest unused "guest" name and claim it
var getGuestName = function () {
var name,
nextUserId = 1;
do {
name = 'Guest ' + nextUserId;
nextUserId += 1;
} while (!claim(name));
return name;
};
// serialize claimed names as an array
var get = function () {
var res = [];
for (user in userNames) {
res.push(user);
}
return res;
};
var free = function (name) {
if (userNames[name]) {
delete userNames[name];
}
};
return {
claim: claim,
free: free,
get: get,
getGuestName: getGuestName
};
}());
Isso basicamente define um conjunto de nomes, mas com APIs que fazem mais sentido para o domínio de um servidor de chat. Vamos conectar isso ao soquete do servidor para responder às chamadas feitas pelo cliente:
// export function for listening to the socket
module.exports = function (socket) {
var name = userNames.getGuestName();
// send the new user their name and a list of users
socket.emit('init', {
name: name,
users: userNames.get()
});
// notify other clients that a new user has joined
socket.broadcast.emit('user:join', {
name: name
});
// broadcast a user's message to other users
socket.on('send:message', function (data) {
socket.broadcast.emit('send:message', {
user: name,
text: data.message
});
});
// validate a user's name change, and broadcast it on success
socket.on('change:name', function (data, fn) {
if (userNames.claim(data.name)) {
var oldName = name;
userNames.free(oldName);
name = data.name;
socket.broadcast.emit('change:name', {
oldName: oldName,
newName: name
});
fn(true);
} else {
fn(false);
}
});
// clean up when a user leaves, and broadcast it to other users
socket.on('disconnect', function () {
socket.broadcast.emit('user:left', {
name: name
});
userNames.free(name);
});
};
Com isso, o aplicativo estará completo. Faça um teste executando node app.js
. O aplicativo precisa ser atualizado em tempo real, graças ao Socket.IO.
Conclusão
Há muito mais coisas que você pode adicionar a esse app de mensagens instantâneas. Por exemplo, é possível enviar mensagens vazias. Você pode usar ng-valid
para evitar isso no lado do cliente e uma verificação no servidor. Talvez o servidor possa manter um histórico recente de mensagens para o benefício de novos usuários que se juntam ao app.
Depois de entender como agrupar em um serviço e notificar o Angular de que um modelo foi alterado, fica fácil criar apps do AngularJS que usam outras bibliotecas. No próximo artigo, vou abordar o uso do AngularJS com a D3.js, a conhecida biblioteca de visualização.
Referências
Angular Socket.IO Seed App de mensagens instantâneas concluído AngularJS Express Socket.IO`