More Responsive Single-Page Applications With AngularJS & Socket.IO: Creating the Library

http://code.tutsplus.com/tutorials/more-responsive-single-page-applications-with-angularjs-socketio-creating-the-library--cms-21738

by  Maciej Sopyło21 Jul 2014 5 Comments

In the first part of this tutorial we will create a reusable AngularJS service for Socket.IO. Because of that reusable part, this will be a little trickier than just usingmodule.service() or module.factory(). These two functions are just syntactic sugar on top of the more low-level module.provider() method, which we will use to provide some configuration options. If you've never used AngularJS before, I strongly advise you to at least follow the official tutorial and some of the tutorials here on Tuts+.

Before we start writing our AngularJS module, we need a simple back-end for testing. If you are already familiar with Socket.IO you can just scroll down to the end of this section, copy the back-end source and proceed to the next one, if not - read on.

We will only need socket.io. You can either install it directly using the npm command like this:

1
npm install socket.io

Or create a package.json file, put this line in the dependencies section:

1
"socket.io" : "0.9.x"

And execute the npm install command.

Since we don't need any complicated web framework like Express, we can create the server using Socket.IO:

1
var io = require( 'socket.io' )(8080);

That's all you need to setup the Socket.IO server. If you start your app, you should see similar output in the console:

And you should be able to access the socket.io.js file in your browser athttp://localhost:8080/socket.io/socket.io.js:

We will handle all incoming connections in the connection event listener of theio.sockets object:

1
2
3
io.sockets.on( 'connection' , function (socket) {
 
});

The socket attribute passed to the callback is the client that connected and we can listen to events on it.

Now we will add a basic event listener in the callback above. It will send the data received, back to the client using the socket.emit() method:

1
2
3
socket.on( 'echo' , function (data) {
     socket.emit( 'echo' , data);
});

echo is the custom event name that we will use later.

We will also use acknowledgments in our library. This feature allows you to pass a function as the third parameter of the socket.emit() method. This function can be called on the server to send some data back to the client:

1
2
3
socket.on( 'echo-ack' , function (data, callback) {
     callback(data);
});

This allows you to respond to the client without requiring it to listen to any events (which is useful if you want to just request some data from the server).

Now our test back-end is complete. The code should look like this (this is the code you should copy if you omitted this section):

01
02
03
04
05
06
07
08
09
10
11
var io = require( 'socket.io' )(8080);
 
io.sockets.on( 'connection' , function (socket) {
     socket.on( 'echo' , function (data) {
         socket.emit( 'echo' , data);
     });
 
     socket.on( 'echo-ack' , function (data, callback) {
         callback(data);
     });
});

You should now run the app and leave it running before proceeding with the rest of the tutorial.

We will of course need some HTML to test our library. We have to include AngularJS,socket.io.js from our back-end, our angular-socket.js library and a basic AngularJS controller to run some tests. The controller will be inlined in the <head> of the document to simplify the workflow:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
<! DOCTYPE html>
< html >
< head >
     < script src = "http://localhost:8080/socket.io/socket.io.js" ></ script >
     < script src = "angular-socket.js" ></ script >
 
     < script type = "application/javascript" >
         
     </ script >
</ head >
< body >
 
</ body >
</ html >

This is all we need for now, we will get back to the empty script tag later since we don't have the library yet.

In this section we will create the angular-socket.js library. All of the code must be insterted into this file.

Let's start with creating the module for our lib:

1
var module = angular.module( 'socket.io' , []);

We don't have any dependencies, so the array in the second argument ofangular.module() is empty, but do not remove it completely or you will get an$injector:nomod error. This happens because the one-argument form ofangular.module() retrieves a reference to the already existing module, instead of creating a new one.

Providers are one of the ways to create AngularJS services. The syntax is simple: the first argument is the name of the service (not the name of the provider!) and second one is the constructor function for the provider:

1
2
3
module.provider( '$socket' , $socketProvider() {
 
});

To make the library reusable, we will need to allow changes in Socket.IO's configuration. First let's define two variables that will hold the URL for the connection and the configuration object (code in this step goes to the $socketProvider()function):

1
2
var ioUrl = '' ;
var ioConfig = {};

Now since these variables are not available outside of the $socketProvider()function (they are kind of private), we have to create methods (setters) to change them. We could of course just make them public like this:

1
2
this .ioUrl = '' ;
this .ioConfig = {};

But:

  1. We would have to use Function.bind() later to access the appropriate context for this
  2. If we use setters, we can validate to make sure the proper values are set - we don't want to put false as the 'connect timeout' option

A full list of options for Socket.IO's Client can be seen on their GitHub wiki. We will create a setter for each of them plus one for the URL. All of the methods look similar, so I will explain the code for one of them and put the rest below.

Let's define the first method:

1
this .setConnectionUrl = function setConnectionUrl(url) {

It should check the type of parameter passed in:

1
if ( typeof url == 'string' ) {

If it's the one we expected, set the option:

1
ioUrl = url;

If not, it should throw TypeError:

1
2
3
4
     } else {
         throw new TypeError( 'url must be of type string' );
     }
};

For the rest of them, we can create a helper function to keep it DRY:

1
2
3
4
5
6
7
function setOption(name, value, type) {
     if ( typeof value != type) {
         throw new TypeError( "'" + name + "' must be of type '" + type + "'" );
     }
 
     ioConfig[name] = value;
}

It just throws TypeError if the type is wrong, otherwise sets the value. Here is the code for the rest of the options:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
this .setResource = function setResource(value) {
     setOption( 'resource' , value, 'string' );
};
this .setConnectTimeout = function setConnectTimeout(value) {
     setOption( 'connect timeout' , value, 'number' );
};
this .setTryMultipleTransports = function setTryMultipleTransports(value) {
     setOption( 'try multiple transports' , value, 'boolean' );
};
this .setReconnect = function setReconnect(value) {
     setOption( 'reconnect' , value, 'boolean' );
};
this .setReconnectionDelay = function setReconnectionDelay(value) {
     setOption( 'reconnection delay' , value, 'number' );
};
this .setReconnectionLimit = function setReconnectionLimit(value) {
     setOption( 'reconnection limit' , value, 'number' );
};
this .setMaxReconnectionAttempts = function setMaxReconnectionAttempts(value) {
     setOption( 'max reconnection attempts' , value, 'number' );
};
this .setSyncDisconnectOnUnload = function setSyncDisconnectOnUnload(value) {
     setOption( 'sync disconnect on unload' , value, 'boolean' );
};
this .setAutoConnect = function setAutoConnect(value) {
     setOption( 'auto connect' , value, 'boolean' );
};
this .setFlashPolicyPort = function setFlashPolicyPort(value) {
     setOption( 'flash policy port' , value, 'number' )
};
this .setForceNewConnection = function setForceNewConnection(value) {
     setOption( 'force new connection' , value, 'boolean' );
};

You could replace it with a single setOption() method, but it seems easier to type the option's name in camel case, rather than pass it as a string with spaces.

This function will create the service object that we can use later (for example in controllers). First, let's call the io() function to connect to the Socket.IO server:

1
2
this .$get = function $socketFactory($rootScope) {
     var socket = io(ioUrl, ioConfig);

Note that we are assigning the function to the $get property of the object created by the provider - this is important since AngularJS uses that property to call it. We also put$rootScope as its parameter. At this point, we can use AngularJS's dependency injection to access other services. We will use it to propagate changes to any models in Socket.IO callbacks.

Now the function needs to return an object:

1
2
3
4
     return {
 
     };
};

We will put all methods for the service in it.

This method will attach an event listener to the socket object, so we can utilize any data sent from the server:

1
on: function on(event, callback) {

We will use Socket.IO's socket.on() to attach our callback and call it in AngularJS's$scope.$apply() method. This is very important, because models can only be modified inside of it:

1
socket.on(event, function () {

First, we have to copy the arguments to a temporary variable so we can use them later. Arguments are of course everything that the server sent to us:

1
var args = arguments;

Next, we can call our callback using Function.apply() to pass arguments to it:

1
2
3
4
5
         $rootScope.$apply( function () {
             callback.apply(socket, args);
         });
     });
},

When socket's event emitter calls the listener function it uses $rootScope.$apply()to call the callback provided as the second argument to the .on() method. This way you can write your event listeners like you would for any other app using Socket.IO, but you can modify AngularJS's models in them.

This method will remove one or all event listeners for a given event. This helps you to avoid memory leaks and unexpected behavior. Imagine that you are using ngRouteand you attach few listeners in every controller. If the user navigates to another view, your controller is destroyed, but the event listener remains attached. After a few navigations and we'll have a memory leak.

1
off: function off(event, callback) {

We only have to check if the callback was provided and callsocket.removeListener() or socket.removeAllListeners():

1
2
3
4
5
6
     if ( typeof callback == 'function' ) {
         socket.removeListener(event, callback);
     } else {
         socket.removeAllListeners(event);
     }
},

This is the last method that we need. As the name suggests, this method will send data to the server:

1
emit: function emit(event, data, callback) {

Since Socket.IO supports acknowledgments, we will check if the callback was provided. If it was, we will use the same pattern as in the on() method to call the callback inside of $scope.$apply():

1
2
3
4
5
6
7
8
if ( typeof callback == 'function' ) {
     socket.emit(event, data, function () {
         var args = arguments;
 
         $rootScope.$apply( function () {
             callback.apply(socket, args);
         });
     });

If there is no callback we can just call socket.emit():

1
2
3
4
     } else {
         socket.emit(event, data);
     }
}

To test the library, we will create a simple form that will send some data to the server and display the response. All of the JavaScript code in this section should go in the<script> tag in the <head> of your document and all HTML goes in its <body>.

First we have to create a module for our app:

1
var app = angular.module( 'example' , [ 'socket.io' ]);

Notice that 'socket.io' in the array, in the second parameter, tells AngularJS that this module depends on our Socket.IO library.

Since we will be running from a static HTML file, we have to specify the connection URL for Socket.IO. We can do this using the config() method of the module:

1
2
3
app.config( function ($socketProvider) {
     $socketProvider.setConnectionUrl( 'http://localhost:8080' );
});

As you can see, our $socketProvider is automatically injected by AngularJS.

The controller will be responsible for all of the app's logic (the application is small, so we only need one):

1
app.controller( 'Ctrl' , function Ctrl($scope, $socket) {

$scope is an object that holds all of the controller's models, it's the base of AngularJS's bi-directional data binding. $socket is our Socket.IO service.

First, we will create a listener for the 'echo' event that will be emited by our test server:

1
2
3
$socket.on( 'echo' , function (data) {
     $scope.serverResponse = data;
});

We will display $scope.serverResponse later, in HTML, using AngularJS's expressions.

Now there will also be two functions that will send the data - one using the basicemit() method and one using emit() with acknowledgment callback:

01
02
03
04
05
06
07
08
09
10
11
12
     $scope.emitBasic = function emitBasic() {
         $socket.emit( 'echo' , $scope.dataToSend);
         $scope.dataToSend = '' ;
     };
 
     $scope.emitACK = function emitACK() {
         $socket.emit( 'echo-ack' , $scope.dataToSend, function (data) {
             $scope.serverResponseACK = data;
         });
         $scope.dataToSend = '' ;
     };
});

We have to define them as methods of $scope so that we can call them from thengClick directive in HTML.

This is where AngularJS shines - we can use standard HTML with some custom attributes to bind everything together.

Let's start by defining the main module using an ngApp directive. Place this attribute in the <body> tag of your document:

1
< body ng-app = "example" >

This tells AngularJS that it should bootstrap your app using the example module.

After that, we can create a basic form to send data to the server:

1
2
3
4
5
6
7
< div ng-controller = "Ctrl" >
     < input ng-model = "dataToSend" >
     < button ng-click = "emitBasic()" >Send</ button >
     < button ng-click = "emitACK()" >Send (ACK)</ button >
     < div >Server Response: {{ serverResponse }}</ div >
     < div >Server Response (ACK): {{ serverResponseACK }}</ div >
</ div >

We used a few custom attributes and AngularJS directives there:

  • ng-controller - binds the specified controller to this element, allowing you to use values from its scope
  • ng-model - creates a bi-directional data bind between the element and the specified scope property (a model), which allows you to get values from this element as well as modifying it inside of the controller
  • ng-click - attaches a click event listener that executes a specified expression (read more on AngularJS expressions)

The double curly braces are also AngularJS expressions, they will be evaluated (don't worry, not using JavaScript's eval()) and their value will be inserted in there.

If you have done everything correctly, you should be able to send data to the server by clicking the buttons and see the response in the appropriate <div> tags.

In this first part of the tutorial, we've created the Socket.IO library for AngularJS that will allow us to take advantage of WebSockets in our single-page apps. In the second part, I will show you how you can improve the responsiveness of your apps using this combination.


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值