神刀安全网

Integrate Socket.IO with Angular's digest cycle using custom factories

Integrate Socket.IO with Angular’s digest cycle using custom factories

Originally written Jan 5, 2016

Integrate Socket.IO with Angular's digest cycle using custom factories Integrate Socket.IO with Angular's digest cycle using custom factories

Socket.IO enables real-time, bi-directional communication between the user’s browser and server. A cross-platform WebSocket API for Node.js, Socket.IO allows you to send messages to a server and receive event-driven responses without polling.

Before WebSockets, all communication between clients and servers relied on HTTP – but now, data can flow freely over WebSocket connections. Through Socket.IO, web apps can be transformed into interactive and real-time applications.

Socket.IO exposes an io variable on window . However, if we want to fully utilize Socket.io and incorporate it into Angular, it’s better to leverage Angular’s Dependency Injection system to encapsulate it. We’d want to take on incoming broadcasts from the server and convert them into broadcasts to Angular’s eventing system.

Instead of messing around with Socket.IO scripts, we can focus mostly on Angular, using $scope / $rootScope , $emit , $broadcast , and $on . Leveraging Angular’s DI also means the application will be far, far more easier to test — a a mock Socket factory would be enough to test an application!

Wrappers for Socket.IO’s on and emit

Socket.IO exposes an io variable on window . The factory will wrap the socket object created by Socket.IO and let Angular deal with the rest.

Here’s the wrappers for on and emit :

var socket = io.connect();  function on(eventName, callback) {   $window.socket.on(eventName, function() {     var args = arguments;     $rootScope.$apply(function() {       callback.apply($window.socket, args);     });   }); }  function emit(eventName, data, callback) {   $window.socket.emit(eventName, data, function() {     var args = arguments;     $rootScope.$apply(function() {       if (callback) {         callback.apply($window.socket, args);       }     });   }); } 

The crucial part is $rootScope.$apply that is wrapping our Socket callbacks. That will get your bindings to update! Remember that Angular is turn-based, and $apply and $digest is what informs the application that the data model has changed.

How does $apply and $digest play here? Let’s review a bit about these concepts.

A review of $apply and $digest

Under the hood, Angular calls $scope.$digest , but almost all the time we will never call it directly – we use $scope.$apply instead. Almost all Angular events or directives are wrapped in $scope.$apply , such as ng-repeat or $http .

However, in this case, Socket.IO turn-based changes aren’t being executed inside an Angular environment, and so its changes aren’t being tracked.

If you have ever tried to use setTimeout in an Angular application, you may have already encountered this. A simple example:

function Ctrl($scope) {   $scope.message = "Waiting 2000ms for update";    setTimeout(function() {     $scope.message = "Timeout called!";     // AngularJS unaware of update to $scope   }, 2000); } 

Angular isn’t notified that $scope.message is being updated. Instead, we have to wrap it with $apply :

function Ctrl($scope) {   $scope.message = "Waiting 2000ms for update";    setTimeout(function() {     $scope.$apply(function() {       $scope.message = "Timeout called!";     });   }, 2000); } 

Jim Hoskins has a great post explaining what’s happening with $scope.$apply .

The rest of the Socket.IO factory

My team created a factory for our real-time bill-splitting app, Piecemeal . The factory is below – but you can also check out our documentation for more detail :

(function() {   'use strict';    angular.module('Piecemeal')    .factory('socketFactory', socketFactory);    socketFactory.$inject = ['$rootScope', '$window'];    function socketFactory($rootScope, $window) {      var socket;     var services = {       on: on,       emit: emit,       init: init     };      return services;      function init() {       var ioRoom = $window.location.origin + '/' + $window.localStorage.code;       $window.socket = io(ioRoom);     }      function on(eventName, callback) {       $window.socket.on(eventName, function() {         var args = arguments;         $rootScope.$apply(function() {           callback.apply($window.socket, args);         });       });     }      function emit(eventName, data, callback) {       $window.socket.emit(eventName, data, function() {         var args = arguments;         $rootScope.$apply(function() {           if (callback) {             callback.apply($window.socket, args);           }         });       });     }   }  })(); 

Sidenote : Thinks this looks weird? We’re just following John Papa’s amazing Angular Style Guide (currently 17,063 stars on GitHub) . It’s an incredible guide for syntax, conventions, and structuring Angular applications that helps developers build cleaner and more modular components.

So now that we’ve made this factory, we can inject it into any controller and set up Socket listeners. Some socket listeners that we used for Piecemeal look like this:

socketFactory.on('newParticipant', function(userObj) {   services.data.users.push(userObj); });  socketFactory.on('dishAdded', function(dish) {   addDish(dish); });  socketFactory.on('dishShared', function(data) {   shareDish(data.dish_id, data.user_id); });  socketFactory.on('dishUnshared', function(data) {   unshareDish(data.dish_id, data.user_id); }); 

Now we were able to get data from Socket events, such as for Piecemeal:

Integrate Socket.IO with Angular's digest cycle using custom factories

What about Socket communication across multiple controllers?

If you have a more complex application, adding multiple socket listeners on controllers is a major antipattern. Not only is it repetitive, you could run into errors with data integrity if it’s stored on an app-wide factory.

Every view on Piecemeal required real-time communication, especially between controllers within a single client. For example, host bills and guest bills would be continuously updated with every added dish:

Integrate Socket.IO with Angular's digest cycle using custom factories

Given the communication across different controllers, we used one app-wide factory to initiate Socket listeners, and broadcast changes through $scope . In our case, since we needed app-wide communication across all controllers, we used $rootScope .

Here is a simplified shortened version of our app-wide factory methods. Check out our docs for more detail:

(function() {   'use strict';    angular.module('Piecemeal')     .factory('appFactory', appFactory);    appFactory.$inject = ['socketFactory', '$rootScope'];    function appFactory(socketFactory, $rootScope) {      var services = {       initListeners: initListeners,       /*  more methods here... */     };      return services;      function initListeners() {       socketFactory.on('joined', function(data) {         services.data = data;         $rootScope.$broadcast('joined');       });        socketFactory.on('newParticipant', function(userObj) {         services.data.users.push(userObj);       });        socketFactory.on('dishAdded', function(dish) {         addDish(dish);       });        socketFactory.on('dishShared', function(data) {         shareDish(data.dish_id, data.user_id);       });        socketFactory.on('dishUnshared', function(data) {         unshareDish(data.dish_id, data.user_id);       });        socketFactory.on('billsSentToGuests', function(data) {         services.data.billData = data;         $rootScope.$broadcast('billsSentToGuests');       });      }   } })();  

Now we’re almost done! We just have to inject SocketFactory as a dependency and set up Socket listeners on every controller.

// This initializes the socket listeners on appFactory // and sets up Angular's event system listeners. socketFactory.init(); appFactory.initListeners();  ... $scope.$on('eventFromAnotherController', function() {   // do stuff when the controller passes on data from a Socket connection }); 

Sockets from server-side

The server side code should be very much the same as any other application using Socket.IO. Here’s a snippet of how we set up Socket event listeners and emitters for Piecemeal – check out our docs for more detail:

modules.export = function(eventUrl, eventInfo, io, userObj) {    var mealEvent = io.of(eventUrl);   mealEvent.once('connection', function(socket) {      console.log('Socket connection made with server: User', userObj.id, "socket id", socket.id, "on event URL", eventUrl);      socket.emit('joined', eventInfo);      socket.broadcast.emit('newParticipant', userObj);      socket.on('addDish', function(data) {       // methods with the database on creating a dish       // ...     });      socket.on('shareDish', function(data) {       socket.broadcast.emit('dishShared', {         user_id: data.user_id,         dish_id: data.dish_id       });       // methods with the database on adding a shared guest       // ...     });      socket.on('unshareDish', function(data) {       socket.broadcast.emit('dishUnshared', {         user_id: data.user_id,         dish_id: data.dish_id       });       // methods with the database on unsharing a dish       // ...     });      socket.on('sendBillToGuests', function(data) {       console.log("Server heard: SendBillToGuests with tip", data.tipPercent, "and tax", data.taxPercent, " and fee ", data.feePercent, " and discount ", data.discountPercent);       socket.broadcast.emit('billsSentToGuests', data);        util.addTipAndTax(db, data.event_id, data.taxPercent, data.tipPercent, data.feePercent, data.discountPercent, data.billSent)         .catch(function(err) {           throw err;         });     });   }); }; 

And with that, Socket.IO can be successfully incorporated into a somewhat complicated Angular application!

Conclusion

Incorporating Socket.IO for a small angular app is easy. When it comes to more complex application, it can be more of a hurdle – but making full use of Angular’s $scope eventing system can make it easier.

转载本站任何文章请注明:转载至神刀安全网,谢谢神刀安全网 » Integrate Socket.IO with Angular's digest cycle using custom factories

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
分享按钮