Wprowadzenie
AngularJS to świetny framework JavaScriptu, który zapewnia dwukierunkowe wiązanie danych, które jest łatwe w użyciu i szybkie, a także potężny system dyrektyw, który umożliwia tworzenie wielokrotnego użytku niestandardowych komponentów. Socket.IO to polyfill dla websocketów, który umożliwia tworzenie aplikacji działających w czasie rzeczywistym w różnych przeglądarkach. Co więcej, te 2 funkcje świetnie ze sobą współdziałają.
Wcześniej pisaliśmy o pisaniu aplikacji AngularJS za pomocą Expressa, ale tym razem opiszemy, jak zintegrować Socket.IO, aby dodać do aplikacji AngularJS funkcje działające w czasie rzeczywistym. W tym samouczku pokażę, jak napisać aplikację do obsługi wiadomości błyskawicznych. Ten samouczek opiera się na moim wcześniejszym samouczku (korzysta z podobnego pakietu node.js na serwerze), więc jeśli nie znasz Node.js ani Express, najpierw zapoznaj się z tym samouczkiem.
Jak zawsze, gotowy produkt znajdziesz na GitHubie.
Wymagania wstępne
Konfiguracja i integracja Socket.IO z Expressem wymagają wykonania kilku schematycznych czynności, więc utworzyłem Angular Socket.IO Seed.
Na początek możesz sklonować repozytorium angular-node-seed z GitHub:
git clone git://github.com/btford/angular-socket-io-seed my-project
Gdy już masz nasiono, musisz pobrać kilka zależności za pomocą npm. Otwórz terminal w katalogu z ziarnem i uruchom:
npm install
Po zainstalowaniu tych zależności możesz uruchomić szablon aplikacji:
node app.js
i sprawdź go w swojej przeglądarce na stronie http://localhost:3000
, aby upewnić się, że ziarno działa zgodnie z oczekiwaniami.
Wybór funkcji aplikacji
Istnieją różne sposoby tworzenia aplikacji do czatu, więc opiszmy minimalną liczbę funkcji, które nasza aplikacja będzie mieć. Będzie tylko jeden pokój czatu, do którego będą należeć wszyscy użytkownicy. Użytkownicy mogą wybrać i zmienić swoją nazwę, ale muszą być one unikalne. Serwer będzie egzekwować tę unikalność i powiadamiać, gdy użytkownicy zmieniają swoje nazwy. Klient powinien udostępniać listę wiadomości i listę użytkowników, którzy są obecnie w pokoju czatu.
Prosty interfejs
Dzięki tej specyfikacji możemy utworzyć prosty front-end z Jade, który zapewnia niezbędne elementy interfejsu. Otwórz plik views/index.jade
i w pliku block body
dodaj ten kod:
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')
Otwórz public/css/app.css
i dodaj kod CSS, aby utworzyć kolumny i przepełnienia:
/* 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;
}
Interakcja z Socket.IO
Chociaż Socket.IO udostępnia zmienną io
w obiekcie window
, lepiej jest ją zaimplementować w systemie podawania zależności w AngularJS. Zacznijmy od napisania usługi, która owija obiekt socket
zwrócony przez Socket.IO. To świetnie, ponieważ znacznie ułatwi nam późniejsze testowanie kontrolera. Otwórz plik public/js/services.js
i zastąp jego zawartość tym kodem:
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);
}
});
})
}
};
});
Zwróć uwagę, że każdą funkcję zwracającą dane o połączeniu otaczamy w ramki funkcji $scope.$apply
. Informuje to AngularJS, że musi sprawdzić stan aplikacji i zaktualizować szablony, jeśli nastąpiła zmiana po wykonaniu przekazanego wywołania zwrotnego. Wewnętrznie $http
działa w taki sam sposób. Po zwróceniu przez XHR wywołuje on $scope.$apply
, aby AngularJS mógł odpowiednio zaktualizować widoki.
Pamiętaj, że ta usługa nie obejmuje całego interfejsu Socket.IO API (to zadanie dla czytelnika ;P ). Obejmuje jednak metody używane w tym samouczku i powinna wskazać Ci właściwy kierunek, jeśli chcesz poszerzyć tę wiedzę. Możesz wrócić do pisania pełnego opakowania, ale wykracza to poza zakres tego samouczka.
Teraz w kontrolerze możemy poprosić o obiekt socket
, podobnie jak w przypadku obiektu $http
:
function AppCtrl($scope, socket) {
/* Controller logic */
}
W sterowniku dodamy logikę wysyłania i odbierania wiadomości. Otwórz plik js/public/controllers.js
i zastąp jego zawartość tym fragmentem kodu:
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 = '';
};
}
Ta aplikacja będzie zawierać tylko 1 widok, więc możemy usunąć routing z poziomu public/js/app.js
i uprościć go do:
// Declare app level module which depends on filters, and services
var app = angular.module('myApp', ['myApp.filters', 'myApp.directives']);
Tworzenie serwera
Otwórz pokój routes/socket.js
. Musimy zdefiniować obiekt do obsługi stanu serwera, aby nazwy użytkowników były niepowtarzalne.
// 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
};
}());
Zasadniczo definiuje on zestaw nazw, ale z interfejsami API, które lepiej pasują do domeny serwera czatu. Podłączmy to do gniazda serwera, aby odpowiadać na wywołania naszego klienta:
// 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);
});
};
To wszystko. Aby wypróbować tę funkcję, uruchom node app.js
. Dzięki Socket.IO aplikacja powinna być aktualizowana w czasie rzeczywistym.
Podsumowanie
Do tej aplikacji do obsługi czatu możesz dodać wiele innych rzeczy. Możesz na przykład przesłać puste wiadomości. Aby zapobiec temu po stronie klienta, możesz użyć ng-valid
, a na serwerze – ng-valid
. Może serwer mógłby przechowywać najnowszą historię wiadomości na potrzeby nowych użytkowników dołączających do aplikacji.
Pisanie aplikacji AngularJS, które korzystają z innych bibliotek, jest łatwe, gdy już zrozumiesz, jak je opakować w usługę i poinformować Angular, że model się zmienił. W kolejnym wpisie omówię korzystanie z AngularJS w połączeniu z popularną biblioteką wizualizacji D3.js.
Odniesienia
Angular Socket.IO Seed Gotowa aplikacja do błyskawicznej wymiany wiadomości AngularJS Express Socket.IO`