引言
AngularJS 是一個出色的 JavaScript 架構,可提供簡單好用且快速的雙向資料繫結。強大的指令系統,可讓你建立可重複使用的自訂元件,以及更多功能。Socket.IO 是跨瀏覽器的包裝函式和 polyfill (適用於 websocket),讓開發即時應用程式變得輕而易舉。順帶一提,兩者的搭配起來相當不錯!
我曾在使用 Express 編寫 AngularJS 應用程式前寫過,但這次來信是想瞭解如何整合 Socket.IO,以便為 AngularJS 應用程式加入即時功能。在這個教學課程中,我會逐步說明如何編寫即時通訊應用程式。這項工具是以我先前的教學課程為基礎 (在伺服器上使用類似的 node.js 堆疊),因此建議您先確認自己對 Node.js 或 Express 不熟悉。
與往常一樣,您可以在 GitHub 上取得成品。
必備條件
要設定 Socket.IO 並與 Express 整合,我們需要進行一些樣板,因此我建立了 Angular Socket.IO Seed。
如要開始使用,您可以從 GitHub 複製 angular-node-seed 存放區:
git clone git://github.com/btford/angular-socket-io-seed my-project
取得種子後,您需要使用 npm 擷取一些依附元件。開啟包含種子目錄的終端機,然後執行下列指令:
npm install
安裝這些依附元件後,您就可以執行架構應用程式:
node app.js
,然後在瀏覽器中查看 http://localhost:3000
,確保種子可正常運作。
決定應用程式功能
編寫即時通訊應用程式的方法有很多種,以下將介紹一些基本功能。每個使用者只會加入一個聊天室。使用者可以選擇及變更自己的名稱,但名稱不得重複。伺服器會強制要求此唯一性,並在使用者變更名稱時發出通知。用戶端應公開訊息清單,以及目前聊天室中的使用者清單。
簡單的前端
透過這項規格,我們可以使用 Jade 建立簡易的前端,以提供必要的 UI 元素。開啟 views/index.jade
並將其新增至 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')
開啟 public/css/app.css
並加入 CSS 以提供欄和溢位:
/* 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;
}
與 Socket.IO 互動
雖然 Socket.IO 會在 window
上公開 io
變數,但最好將其封裝在 AngularJS 的依附元件插入系統中。因此,我們首先請編寫服務,以納入 Socket.IO 傳回的 socket
物件。這項功能很棒,稍後可更輕鬆地測試控制器。開啟 public/js/services.js
並將內容替換為:
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);
}
});
})
}
};
});
請注意,我們會將每個通訊端回呼納入 $scope.$apply
中。這會告知 AngularJS 需要檢查應用程式的狀態,並在執行傳遞回呼的回呼後發生變更時更新範本。$http
在內部的運作方式相同;有些 XHR 返回後,會呼叫 $scope.$apply
,讓 AngularJS 可據此更新檢視畫面。
請注意,這項服務不會納入整個 Socket.IO API (這是讀取器 ;P 的練習),但涵蓋了本教學課程中使用的方法,建議您指向適當的方向,以便展開 API。我可以回想一下,重新編寫完整的包裝函式,但這已超出本教學課程的範圍。
現在,我們可以在控制器中要求 socket
物件,就像使用 $http
時一樣:
function AppCtrl($scope, socket) {
/* Controller logic */
}
在控制器內,我們開始新增用來收發訊息的邏輯。開啟 js/public/controllers.js
並將內容替換為以下內容:
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 = '';
};
}
這個應用程式只會含有一個檢視畫面,因此我們可以從 public/js/app.js
中移除轉送路徑,並簡化以下作業:
// Declare app level module which depends on filters, and services
var app = angular.module('myApp', ['myApp.filters', 'myApp.directives']);
寫入伺服器
開啟 routes/socket.js
。我們必須定義物件來維持伺服器狀態,讓使用者名稱不會重複。
// 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
};
}());
基本上,這組名稱是由系統定義,但 API 對即時通訊伺服器的網域更有意義。讓我們將此連接至伺服器的通訊端,以回應用戶端發出的呼叫:
// 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);
});
};
因此應用程式應該已經完成。執行 node app.js
即可試用。因為 Socket.IO,應用程式應該會即時更新。
結語
這個即時通訊應用程式還有更多其他功能。例如,你可以提交空白訊息。您可以使用 ng-valid
在用戶端防止發生這種情形,並在伺服器中檢查。也許伺服器可以保留近期的訊息記錄,以協助新使用者加入應用程式。
只要您瞭解如何把這些程式庫納入服務,並通知 Angular 模型已變更,就能輕鬆編寫運用其他程式庫的 AngularJS 應用程式。接下來,我打算說明如何搭配 D3.js (這個常用的視覺化程式庫) 使用 AngularJS。
參考資料
Angular Socket.IO Seed 完成的即時通訊應用程式 AngularJS Express Socket.IO`