使用 Socket.IO 编写 AngularJS 应用程序
AngularJS 是一个很棒的 JavaScript 框架,它为您提供易于使用和快速的双向数据绑定,一个强大的指令系统,让您可以使用创建可重用的自定义组件,以及更多。 Socket.IO 是一个跨浏览器的 websockets 包装器和 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
获得种子后,您需要使用 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 公开了一个 io
上的变量 window
,最好将其封装在 AngularJS 的 依赖注入系统中 。 因此,我们将首先编写一个服务来包装 socket
Socket.IO 返回的对象。 这太棒了,因为它将使以后测试我们的控制器变得更加容易。 打开 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 流行的可视化库。
References
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论