简介
AngularJS 是一款出色的 JavaScript 框架,可为您提供易于使用且快速的双向数据绑定,以及强大的指令系统,让您可以创建可重复使用的自定义组件,还有很多其他功能。Socket.IO 是 WebSocket 的跨浏览器封装容器和 polyfill,可让您轻松开发实时应用。顺便提一下,这两者搭配使用效果非常不错!
我之前曾介绍过如何使用 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
或者下载 ZIP 文件。
有了种子后,您需要使用 npm 获取一些依赖项。打开一个连接到包含种子的目录的终端,然后运行以下命令:
npm install
安装这些依赖项后,您可以运行框架应用:
node app.js
并在浏览器中查看 http://localhost:3000
,确保种子按预期运行。
确定应用功能
编写聊天应用的方法有很多,下面我们来介绍一下我们的聊天应用将具备的基本功能。所有用户都将属于一个聊天室。用户可以选择和更改自己的姓名,但姓名必须是唯一的。服务器会强制执行此唯一性要求,并在用户更改姓名时进行通知。客户端应显示消息列表和当前在聊天室中的用户列表。
简单的前端
有了这个规范,我们就可以使用 Jade 构建一个简单的前端,提供必要的界面元素。打开 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)。不过,它涵盖了本教程中所用的方法,如果您想对其进行扩展,应该能为您指明正确的方向。我可能会再回过头来介绍如何编写完整封装容器,但这超出了本教程的范围。
现在,我们可以在控制器中请求 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 应用了。接下来,我打算介绍如何将 AngularJS 与热门可视化库 D3.js 搭配使用。
参考
Angular Socket.IO Seed 已完成的即时通讯应用 AngularJS Express Socket.IO`