سرویس ها در AngularJS

۱- AngularJS Service / Factory Tutorial با مثال
وظیفه ی سرویس ها در انگولار انجام وظائف محوله است!
این سرویس ها وظیفه ی انجام یک سری کارهای مربوط به لایه ی تجاری نرم‌افزار شما را بر عهده دارند. در طراحی انگولار موارد نگران کننده ی برنامه شما از هم جدا می‌شوند. کنترلر شما باید مسئول اتصال داده‌ها از model به view را از طریق $scope داشته باشد. البته این نوع انتقال شامل انتقال منطق نیست ، یک نوع واکشی داده یا دستکاری آن است. حالا اینجا لازم است یک لایه که شامل توابعی است که محاسبات را انجام می‌دهد به برنامه اضافه شود و آن هم سرویس ها هستند.انگولار امکانات مختلفی برای مدیریت این لایه در نظر گرفته است.هر گاه می‌خواهیم از سرویس ها استفاده نماییم ، فقط می بایست نام آن را صدا بزنیم و سپس انگولار یک مدل تزریق جادویی ، اشیاء سرویس را برای شما وارد یا تزریق می‌کند که شامل یک شی stateless بوده و شامل یک سری توابع کاربردی هستند. این توابع از هر جایی قابل فراخوانی هستند مثل Controllers, Directive, Filters and … در نتیجه می‌توانیم برنامه خود را یک سری یونیت های منطقی تقسیم کنیم.پس می‌توانیم در منطق تجاری خود یک سری url استفاده نماییم که داده‌ها را از سرویس دریافت می‌کنند و در آبجکت های سرویس قرار میدهند.
قرار دادن منطق های تجاری نرم‌افزار در لایه‌ای جداگانه مزایای فراوانی دارد. بعنوان اولین مورد می‌توان به تفکیک وظائف و نوعی تبعیض وظیفه در نرم‌افزار رسید که کار کنترل محاسبات را ساده‌تر می کند. دوم در این روش می‌توان موقعیت های بیشتری را برای تست پذیر بودن برنامه بوجود آورد.

شکل بالا را در نظر بگیرید. ما برنامه ی خود را به کنترلر تقسیم کرده ایم: ۱- پروفایل ۲-داشپورد .
هر کدام از این کنترلر ها نیاز به یک سری داده‌های خاص از سرور دارند. لذا بجای تکرار مکررات فراخوانی داده‌ها در ویو ها ما یک سوریس بنام User Service ساخته و مسائل مربوط به سرور را از آن طریق حل می کنیم. و از این روش حداقل پیچیدگی در فراخوانی های تکراری را کمتر می‌کنیم.
انگولار بصورت خودکار User Service را در پروفایل وداشبورد تزریق خواهد کرد و این تزریق خودکار امکان تست پذیری لایه ها و ابجکت ها را ساده‌تر می نماید.

۲- سرویس های توکار انگولار
انگولار بصورت خود دارای سرویس های مختلفی است که در برنامه‌هایمان قابل استفاده هستند.مثل $http ضمناً تمامی این سرویس های داخلی با $ شروع میشوند.البته سرویس های دیگری نیز وجود دارند مثل $route, $window, $location و غیره.
این سرویس ها را می‌توان بوسیله ی تمامی کنترل های که این سرویس ها بعنوان وابسته معرفی می نمایند فراخوانی نمود مثل :

module.controller('FooController', function($http){
//...
});

module.controller('BarController', function($window){
//...
});

۳- سرویس های دستی در انگولار
در انگولار می‌توان سوریس هایی که خود طراحی می‌کنیم را هر جایی که لازم دانستیم فراخوانی نماییم.
روش‌های مختلفی برای فراخوانی سرویس های انگولار وجود دارند. دو روش زیر از ساده‌ترین ها هستند :

var module = angular.module('myapp', []);

module.service('userService', function(){
this.users = ['John', 'James', 'Jake'];
});

یا می‌توان از متد factory استفاده نمود.

module.factory('userService', function(){

var fac = {};

fac.users = ['John', 'James', 'Jake']; 

return fac;

});

این دو روش برای فراخوانی سرویس ها بکار برده میشوند.البته به تفاوت‌های بین factory() و service() اشاره خواهد شد. البته باید توجه داشت که هر دوی این روش‌ها بعنوان ساخت یک سرویس معرفی شده‌اند و در همه جا قابل دسترس هستند مثل : controllerها فیلترها ، Directives و …
۴- تفاوت بین Factory و Services در انگولار
سرویس های توکار انگولار همانطور که قبلاً اشاره شد اشیایی تک قلو هستند. این در نرم‌افزار کاربرد گسترده ای دارند. لذا این شی سرویس یکبار ایجاد شده و همه جا می‌توان استفاده نمود.
همانطور که اشاره شده است دو روش برای ساخت سرویس ها وجود دارد . استفاده از module.factory و module,service

module.service( 'serviceName', function );

module.factory( 'factoryName', function );

وقتی نام سرویس بعنوان یک آرگومان قابل تزریق در ویو شما قرار داده می‌شود شما یک نمونه از تابع را برای خود فراهم می آورید. به عبارت دیگر تابع YouPassedToService() را درنظر بگیرید. این نمونه از شی بعنوان یک شی در سرویس قرارداده شده و انگولار آن را در سرویس ها ثبت نموده تا بتوان آن را در سایر سرویس ها و کنترلر ها استفاده نمود.
ولی وقتی از factoryName بعنوان یک سرویس فراخوانی می‌شود تنها نتیجه ی سرویس بازگشت داده شده و یک نمونه از شی فراخوانده نمی شود.
در مثال زیر MyService به دو روش مختلف فراخوانده شده است.

AngularJS .service

module.service('MyService', function() {
this.method1 = function() {
//..
}

this.method2 = function() {
//..
}
});
AngularJS .factory

module.factory('MyService', function() {

var factory = {}; 

factory.method1 = function() {
//..
}

factory.method2 = function() {
//..
}

return factory;
});

۵- تزریق وابستگی‌ها در سرویس ها
انگولار ابزارهایی را برای مدیریت وابستگی‌ها فراهم آورده است. در ویکی پدیا تزریق وابستگی اینگونه تفسیر شده است:
تزریق وابستگی الگویی برای طراحی نرم‌افزار است که اجازه می‌دهد تا واسط ها و وابستگی‌های پیچیده و کد بر کم شده و اجازه میدهد در حال اجرا آن را تغییرداده و یا با مدل های مختلف بکار برد.
در مثال‌های بالا نحوه ی تزریق وابستگی و مدیریت آن‌ها اشاره شد. ما $scope را کلاس کنترلر بکار بستیم.

حالا یک مثال عملی میزنیم تا کمی کنترل ها و سرویس ها را استفاده نماییم.

1- MathService : یک سرویس ساده دارای متدهای add , subtract , multiply and devide البته در این مثال فقط از multiply‌استفاده خواهیم کرد

2- CalculatorService :‌دارای دو متد square and cube

3- CalculatorController : این هم یک کنترلر ساده است برای عملیات های کاربر. برای UI مل یک TextBox داریم که کاربر یک عدد را وارد نموده و سپس با دو دکمه square and multiply آنرا مشاهده خواهد نمود.

۵-۱- HTML

<div ng-app="app">
    <div ng-controller="CalculatorController">
        Enter a number:
        <input type="number" ng-model="number" />
        <button ng-click="doSquare()">X<sup>2</sup></button>
        <button ng-click="doCube()">X<sup>3</sup></button>
        
        <div>Answer: {{answer}}</div>
    </div>
</div>

۵-۲- جاوا اسکریپت

var app = angular.module('app', []);

app.service('MathService', function() {
    this.add = function(a, b) { return a + b };
    
    this.subtract = function(a, b) { return a - b };
    
    this.multiply = function(a, b) { return a * b };
    
    this.divide = function(a, b) { return a / b };
});

app.service('CalculatorService', function(MathService){
    
    this.square = function(a) { return MathService.multiply(a,a); };
    this.cube = function(a) { return MathService.multiply(a, MathService.multiply(a,a)); };

});

app.controller('CalculatorController', function($scope, CalculatorService) {

    $scope.doSquare = function() {
        $scope.answer = CalculatorService.square($scope.number);
    }

    $scope.doCube = function() {
        $scope.answer = CalculatorService.cube($scope.number);
    }
});

۵-۳- دموی آنلاین

و مثالی دیگر :

AngularJS Controller

کنترلر ها هیچ چیزی جز توابع ساده جاوا اسکریپت نیستند که به یک اسکوپ متصل هستند. Controllers منطق را به view اهدا میکنند.۱- اسکوپ ها چیستند ؟اسکوب در ویو و کنترلر فعالیت می کند. این شی داده هایی را در خود نگه می دارد که قرار است به view‌ تحویل داده شوند. اسکوب ها از اتصال داده ی دو طرفه ی مخصوص انگولاری استفاده میکند که دیتای مدل ها را به view‌ می رساند.پس می توان این گونه نتیجه گرفت که اسکوب شی ای است که کنترلر را به view متصل می کند. به همین منظور این وظیفه ی کنترلر است که داده هایی که قرار است در view نمایش داده شوند را به به ان برساند.این عملیات ها تماما در $scope انجام میشوند.

حالا مثال کوچکی از این ارتباط میزنم :

<!DOCTYPE html>
<html ng-app>
<head>
<title>Hello World, AngularJS - ViralPatel.net</title>
<script type="text/javascript" 
	src="http://ajax.googleapis.com/ajax/libs/angularjs/1.0.7/angular.min.js"></script>
</head>
<body>
<div ng-controller="ContactController">
	 Email:<input type="text" ng-model="newcontact"/>
	<button ng-click="add()">Add</button>
	<h2>Contacts</h2>

	<ul>
		<li ng-repeat="contact in contacts"> {{ contact }} </li>
	</ul>

</div>
<script type="text/javascript">
	function ContactController($scope) {
	    $scope.contacts = ["hi@email.com", "hello@email.com"];

	    $scope.add = function() {
		$scope.contacts.push($scope.newcontact);
		$scope.newcontact = "";
	    }
	}
</script>
</body>
</html>

۱.۱. دموی آنلاین :

در مثال بالا یک دکمه ی ساده در نظر گرفته شده است تا با فشردن آن محتوای آن در یک آرایه قرار گرفته و در یک لیست به نمایش گذاشته میشود.

۱.۲- ng-controller

این خصیصه همانطور که در مثال بالا هم قابل مشاهده است وظیفه ی معرفی یک کنترلر به view را دارد. در مثال بالا ما کنترلری بنام ContactController در یک Div بوسیله ی ng-controller تعریف نموده ایم. لذا هر گاه ما به درون Div سر سرک بکشیم با حاکمیت کنترلر ContactController در ارتباط خواهیم بود.

ContactController چیزی جز یک تابع ساده ‌vanilla JavaScript  نیست. در دموی بالا ما این کنترلر را بصورت یک تابع بکار بستیم.  در تابع ContactController شی ای بنام $scope بعنوان ارگومان در نظر گرفته شده است که همان وظیفه ی اتصال کنترلر به ویو را بر عهده دارد. وقتی انگولار این کنترلر را ساخت و پرداخت می کند بوسیله $scope بصورت خودکار در ویو پیاده سازی شده و تزریق میشود.

۱.۳- ng-repeat

در مثال بالا به نحوه ی نمایش contacts با استفاده از ng-repeat اشاره می کنیم.

	<li ng-repeat="contact in contacts">{{ contact }}</li>

ng-repeat یکی از پر کاربردترین خصیصه ها در ویوهای انگولاری است. این خصیصه یک آرایه را دریافت کرده و به تعداد آنها المنت های زیر شاخه ی خود را ساخته و تکرار میکند.

از این خصیصه در نشان دادن جداول ، گرید ها و … استفاده میکنند

۲- ساخت و پرداخت یک شی در اسکوپ

$scope.contacts = ["hi@email.com", "hello@email.com"]

با توجه به مثال بالا هرگاه می خواهیم یک برنامه با انگولار بسازیم می بایست وضعیت scope‌انگولار را روشن نماییم.

در مثال بالا در اسکوب هایمان لیستی از ایمیل های مختلف را در اسکوب contacts قرارداده ایم. وقتی انگولار این خط را بررسی می کند در سرویس $scope شی contacts از نوع آرایه را ساخته و ایمیل ها نسبت می دهد و آماده می شوند تا بروزرسانی ، حذف و یا در لیستی بوسیله ی ng-repeat قرارداده شوند.

انگولار نوع زیبایی از ارتباط بین کنترلر و ویو برقرار می کند که امکان میدهد امکان میدهد گاهی کنترلر دیتای درون contacts را دستکاری نماید و گاهی نیز view ، این نیز یکی از امکانات بسیار زیبای انگولار است.

۲.۱- ng-click

در مثال بالا ما یه متد به شکل زیر داریم

$scope.add = function() {
		...
	}

بوسیله ی ng-click می توان آنرا اجرا نمود. در اصل این خصیصه یه onClick یک المنت متصل شده است. ضمنا اینگونه توابع نیز در اسکوب نوشته میشوند

در این تابع ما به آرایه ی contacts نام جدیدی را push میکنیم و به محض اضافه سازی می توان آنرا در لیست مشاهده نمود

زیبا نیست :)))

۳- چگونه یک کنترلر را پیاده سازی نماییم

کمی درباره ی اینکه کنترها چگونه هستند صحبت و اشاره شد که این توابع تنها توابع ساده vanilla JavaScript هستند که مقداری کدهای تجاری در آن وارده شده و به ویو متصل می شوند.

function ContactController($scope) {

	//...

}

روشی که در مثال بالا بکار بسته شد ساده ترین روش استفاده ی کنترلر است ولی پیشنهاد می کنم اینگونه آنرا بکار نبرید.

روش بهتری برای استفاده پیاده سازی وجود دارد که در ادامه اشاره خواهد شد. این روش بما اجازه میدهد که کنترلرها را همراه با Module‌ها پیاده سازی کنیم که روشی است استاندارد.

۳.۱- AngularJS Modules

مدل ها یک موجودیت منطقی هستند که شما تصمیم می گیرید به چه تعداد و چگونه بکار ببندید به همین ترتیب برنامه شما می تواند تعداد متعددی module داشته باشد باشد مثل Transaction, Report, … هر ماژول نمایان گر یک موجودیت منطقی در برنامه شماست.

هر مدل می تواند دارای چندیدن کنترلر باشد. همانطور که در شکل بالا مشاهده می کنید. پس می توان یک یا چند کنترلر به یک مدل اضافه کرد. حالا یک نمونه را مشاهده می کنیم :

var myApp = angular.module('myApp',[]);
 
myApp.controller('ContactController', ['$scope', function($scope) {
	$scope.contacts = ["hi@email.com", "hello@email.com"];

	$scope.add = function() {
		$scope.contacts.push($scope.contact);
		$scope.contact = "";
	}
}]);

در مثال بالا مدلی بنام myApp بوسیله ی angular.module() ساختیم. سپس یک کنترلر بنام ContactController به آن افزدیم. البته این روش هم یکی از روش های موجود برای این کار است که من همین روش را پیشنهاد میکنم.

به نحوه ی تعریف کنترل ها در myApp.controller() توجه نمایید. ما یک آرای به آن اضافه کردیم که یکی و تنها آرایه استفاده شده در آن $scope است و نام سرویس های عمومی انگولار است و ارگومان بعدی ContactController‌ است مربوط به شرح توابع مربوط به کنترلر در آن قرار میگیرد.

۴- Nested Controllers

انگولار از کنترلرهای تو در تو نیز پشتیبانی می کند :

<div ng-controller="CarController">
	My name is {{ name }} and I am a {{ type }}
	
	<div ng-controller="BMWController">
		My name is {{ name }} and I am a {{ type }}

		<div ng-controller="BMWMotorcycleController">
			My name is {{ name }} and I am a {{ type }}

		</div>
	</div>
</div>

<script>
function CarController($scope) {

	$scope.name = 'Car';
	$scope.type = 'Car';

}

function BMWController($scope) {

	$scope.name = 'BMW';

}

function BMWMotorcycleController($scope) {

	$scope.name = 'BMWMotorade';
	$scope.type = 'Motorcycle';

}


</script>

دموی آنلاین :

۵- وراثت در کنترلر ها

هنگام استفاده از کنترلهای تو در تو ممکن است بخواهید از قدرت وراثت در کنترل های انگولار استفاده نمایید.

ممکن است شما BaseController‌ داشته باشید و مابقی کنترل ها نیز زیر شاخه ی آن باشند.

برای استفاده از این امکان شما میبایست از سرویس $injector استفاده نمایید :

<div ng-controller="BMWController">
	
	My name is {{ name }} and I am a {{ type }}

	<button ng-click="clickme()">Click Me</button> 

</div>
    

<script>
function CarController($scope) {
 
	$scope.name = 'Car';
	$scope.type = 'Car';
 
	$scope.clickme = function() {
		alert('This is parent controller "CarController" calling');
	}

}
 
function BMWController($scope, $injector) {
    
    $injector.invoke(CarController, this, {$scope: $scope});
    
    $scope.name = 'BMW';
 
}
</script>

و در آخر یه پروژه ی نمونه دیگر :

لطفا بروی این پروژه متمرکز شوید :


Play on JSFiddle – http://jsfiddle.net/viralpatel/JFYLH/

AngularJS Routing and Views

روترها بشما کمک می کنند تا برنامه ی خود را به ویو های مختلف تقسیم کرده و آنها را به کنترلر های مختلف متصل نمایید

 در شکل بالا ما دو Route url به نام های /ShowOrders و /AddNewOrder ایجاد کردیم. هر کدام از این نقاط مربوط به ویو های خاصی سات و بوسیله ی کنترلر خود مدیریت میشوند. 
۱- معرفی $routeProviderامکانات بسیار کاربردی انگولار در سرویسی بنام $routeProvider مهیا شده است. استفاده از این سرویس استفاده تنگاتنگ کنترلر ، ویو ، تمپلیت و مدیریت URL را بهینه نموده است. استفاده از این امکان نیز برای مرورگر بهینه شده است تا با استفاده از دکمه back  or forward مدیریت نرم افزار به هم نخورد.نحوه ی ایجاد روتینگدر نمونه سورس پایین اشاره ای به نحوه ی نگاشت روتینگ شده است. در مثال زیر .when() and .otherwise() برای شرح روتینگ در نظر گرفته شده است.

var sampleApp = angular.module('phonecatApp', []);
 
sampleApp .config(['$routeProvider',
  function($routeProvider) {
    $routeProvider.
      when('/addOrder', {
        templateUrl: 'templates/add-order.html',
        controller: 'AddOrderController'
      }).
      when('/showOrders', {
        templateUrl: 'templates/show-orders.html',
        controller: 'ShowOrdersController'
      }).
      otherwise({
        redirectTo: '/addOrder'
      });
  }]);

در نمونه کد بالا ما دو url داریم /addorder and /showOrder و آنها را به ویو های templates/add-order.html and template/show-orders.html اتصال داده ایم. هر گاه  http://app/#addOrder را در مرورگر باز کنیم، انگولار بصورت خودکار این آدرس را با روت های انگولار چک کرده و add-order.html را فراخوانی می کند و سپس AddOrderController را به ویو اضافه می نماید.

۱.۱- اولین پروژه با AngularJS + Routing

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>AngularJS Routing example</title>
  </head>

  <body ng-app="sampleApp">

    <div class="container">
		<div class="row">
		<div class="col-md-3">
			<ul class="nav">
				<li><a href="#AddNewOrder"> Add New Order </a></li>
				<li><a href="#ShowOrders"> Show Order </a></li>
			</ul>
		</div>
		<div class="col-md-9">
		  	<div ng-view></div>
		</div>
		</div>
    </div>
	<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.0.7/angular.min.js"></script>
	<script src="app.js"></script>
  </body>
</html>

همانطور که مشاهده می کنید دو دکمه وجود دارد با لینک های #AddNewOrder  و #ShowOrders که هر کدام یک تمپلیت قرار است لود کنند.

ng-view

این تگ مشخص کننده محل لود شدن تمپلیت است و در اصل نوعی placeholder‌ است.

ng-view را می توان به یکی از صورت های زیر در تمپلیت اصلی تعرف نمود

<div ng-view></div>
..
<ng-view></ng-view>
..
<div class="ng-view"></div>

۱.۲- اضافه سازی Routing  در AngularJS

app.js :

//Define an angular module for our app
var sampleApp = angular.module('sampleApp', []);

//Define Routing for app
//Uri /AddNewOrder -> template add_order.html and Controller AddOrderController
//Uri /ShowOrders -> template show_orders.html and Controller AddOrderController
sampleApp.config(['$routeProvider',
  function($routeProvider) {
    $routeProvider.
      when('/AddNewOrder', {
		templateUrl: 'templates/add_order.html',
		controller: 'AddOrderController'
	}).
      when('/ShowOrders', {
		templateUrl: 'templates/show_orders.html',
		controller: 'ShowOrdersController'
      }).
      otherwise({
		redirectTo: '/AddNewOrder'
      });
}]);


sampleApp.controller('AddOrderController', function($scope) {
	
	$scope.message = 'This is Add new order screen';
	
});


sampleApp.controller('ShowOrdersController', function($scope) {

	$scope.message = 'This is Show orders screen';

});

با توجه به syntax بالا ، با دانش کمی از زبان انگلیسی می توان فهمید که روتینگ قراره چه فعالیت هایی رو انجام میده ، این فوق العادست و همینطور ساده

۱.۳- اضافه کرده HTML Template files

با توجه به فایل app.js نیاز است دو فایل در تمپلیت داشته باشیم و این فایل ها می بایست partial بوده و بعنوان مثال بصورت زیر باشند ، البته مثال زیر بعنوان محتوای فایل templates/add_order.html در نظر گرفته شده است

templates/add_order.html

<h2>Add New Order</h2>

{{ message }}

templates/show_orders.html

<h2>Show Orders</h2>

{{ message }}

دموی آنلاین :

Demo link: Plnkr.co

۲- نحوه ارسال پارامترها در Route Urls

با توجه به شکل بالا می خواهیم با کلیک بروی هر سفارش کاربر بتواند جزئیاتی از سفارش مورد نظر را مشاهده نماید. به همین خاطر نیاز است کد سفارش همراه با روتر به کنترلر ارسال شده تا ویوی مورد نظر فراخوانی شود.

به سورس زیر دقت نمایید :

when('/ShowOrder/:orderId', {
      templateUrl: 'templates/show_order.html',
      controller: 'ShowOrderController'
});

:orderId همان محل قرار گیری کد است رو روتر می فهمد قرار است در اینجا چیزی نوشته شود که آنرا تحویل کنترلر دهد

حالا طبق مثال زیر کنترلر این ورودی را از روتر تحویل میگیرد :

...
$scope.order_id = $routeParams.orderId;
...

حالا با دقت به مثال دموی آنلاین توجه نمایید :

Demo link: Plnkr.co

۳- نحوه فراخوانی ویوهای محلی همراه تگ اسکریپت !!

قرار نیست همیشه تمپلیت شما از فایل های مختلفی تشکیل شود و Url شما فقط از یک سری partial files استفاده نماید ، می توان به در مواقعی که urlrouting‌ شما قرار است کد کوچکی را فراخوانی نماید که شما دیگر نیاز نبینید که در یک فایل جداگانه آنرا قرار دهید می توانید آنرا در بدنه صفحه ی اصلی خود بکار ببندید و از همان صفحه فراخوانی را انجام دهید

۳.۱- ng-template directive

<script type="text/ng-template" id="add_order.html">
	<h2> Add Order </h2>
	{{message}}
</script>

سورس بالا یک نمونه تمپلیت توکار است.

و حالا یک دموی آنلاین برای نمایش بهتر این امکان :

Demo link: Plnkr.co
۴- اضافه کردن داده به RouteProvider

همانطور که اشاره کردم $routePovider متدهای when and otherwise رو برای پیاده سازی url ها بکار میبره. در مواقعی نیاز هست که ما می خواهیم بجز url query داده های دیگری را به controller‌از طریق روت برسونیم. مثلا شاید شما از یک کنترلر برای چند قسمت مختلف می خواهید استفاده کنید.

به مثال زیر توجه کنید :

when('/AddNewOrder', {
	templateUrl: 'templates/add_order.html',
	controller: 'CommonController',
	foodata: 'addorder'
}).
when('/ShowOrders', {
	templateUrl: 'templates/show_orders.html',
	controller: 'CommonController',
	foodata: 'showorders'
});

sampleApp.controller('CommonController', function($scope, $route) {
	//access the foodata property using $route.current
	var foo = $route.current.foodata;
	
	alert(foo);
	
});

در مثال بالا foodata بعدا بوسیله ی controller قابل دریافت خواهد بود. و کنترلر نیز بوسیله $route.current.foodata به آن خواهد رسید.

AngularJS WebSocket Service Example

من رو نجات داد

وقتی وب سوکت کار می کنیم و یه کاری زیادی طول میکشه و کار دوم قبل از تموم شدن کار اول باید انجا بشه :

At my curent company we are using Angular.js for a new desktop application (yes, a desktop application, but I won’t get into that). Our app gets its data and events from a web service via a WebSocket connection. Angular comes bundled with some great tools to connect to REST servers, but it doesn’t come with anything to help you with real-time data (and it probably shouldn’t).

Here is an example of an Angular service (factory) that uses WebSockets to get data:

angular.module('MyApp').factory('MyService', ['$q', '$rootScope', function($q, $rootScope) {
    // We return this object to anything injecting our service
    var Service = {};
    // Keep all pending requests here until they get responses
    var callbacks = {};
    // Create a unique callback ID to map requests to responses
    var currentCallbackId = 0;
    // Create our websocket object with the address to the websocket
    var ws = new WebSocket("ws://rahsoon.com:8000/socket/");
    
    ws.onopen = function(){  
        console.log("Socket has been opened!");  
    };
    
    ws.onmessage = function(message) {
        listener(JSON.parse(message.data));
    };

    function sendRequest(request) {
      var defer = $q.defer();
      var callbackId = getCallbackId();
      callbacks[callbackId] = {
        time: new Date(),
        cb:defer
      };
      request.callback_id = callbackId;
      console.log('Sending request', request);
      ws.send(JSON.stringify(request));
      return defer.promise;
    }

    function listener(data) {
      var messageObj = data;
      console.log("Received data from websocket: ", messageObj);
      // If an object exists with callback_id in our callbacks object, resolve it
      if(callbacks.hasOwnProperty(messageObj.callback_id)) {
        console.log(callbacks[messageObj.callback_id]);
        $rootScope.$apply(callbacks[messageObj.callback_id].cb.resolve(messageObj.data));
        delete callbacks[messageObj.callbackID];
      }
    }
    // This creates a new callback ID for a request
    function getCallbackId() {
      currentCallbackId += 1;
      if(currentCallbackId > 10000) {
        currentCallbackId = 0;
      }
      return currentCallbackId;
    }

    // Define a "getter" for getting customer data
    Service.getCustomers = function() {
      var request = {
        type: "get_customers"
      }
      // Storing in a variable for clarity on what sendRequest returns
      var promise = sendRequest(request); 
      return promise;
    }

    return Service;
}])

The Details

To explain this code in detail I will walk you through a usage scenario and step through each function and talk about what it does. Assume we have an angular controller called “customerList”. We need to access customer data in our new controller and our customer data comes from a websocket service somewhere in Canada. So you inject your new websocket service into the scope of your controller and you are able to call getCustomers(). Quick and dirty example for illustration purposes:

angular.module('MyApp')
  .controller('customerList', ['MyService', function(MyService){
    $scope.customers = MyService.getCustomers();
  }]);

So the getCustomers function is called and we see that the getCustomers function creates a request object literal and passes that to the sendRequest() function:

// Define a "getter" for getting customer data
    Service.getCustomers = function() {
      var request = {
        type: "get_customers"
      }
      // Storing in a variable for clarity on what sendRequest returns
      var promise = sendRequest(request); 
      return promise;
    }

You can see I am storing the response from sendRequest() in a variable called promise. I then return that promise. Let’s look at what sendRequest() actually does:

function sendRequest(request) {
      var defer = $q.defer();
      var callbackId = getCallbackId();
      callbacks[callbackId] = {
        time: new Date(),
        cb:defer
      };
      request.callback_id = callbackId;
      console.log('Sending request', request);
      ws.send(JSON.stringify(request));
      return defer.promise;
    }

The sendRequest function first creates a defer object from the Q library that is bundled with Angular. (For more information on deferred objects and promises in angular I highly recommend the egghead.io video on promises) After that it creates a new callbackId variable and then adds an object literal to the callbacks object using the new callbackId as the index.

So why have a callback ID and a callbacks object?

The callbacks variable is where I will store all requests that haven’t received a response yet. Because services implemented on the websocket side can be asynchronous, you could potentially send several requests to the websocket and the websocket could return responses in a different order than it received requests. This is where callback Ids come into play. Usually websocket servers will have a way for you to map responses from the websocket server to requests that you sent to it. Sending a user-generated callback_id to the websocket is one way to do this. In my case, I start at 0 and work my way up to 10000 then start over. You can see this in my getCallback() function:

// This creates a new callback ID for a request
    function getCallbackId() {
      currentCallbackId += 1;
      if(currentCallbackId > 10000) {
        currentCallbackId = 0;
      }
      return currentCallbackId;
    }

Now back to sendRequest. After the callbackId is generated, and the deferred is stored in the callbacks variable, we add the new callbackId to the request message:

request.callback_id = callbackId;

Then we send the request object to the websocket and return a promise:

ws.send(JSON.stringify(request));
    return defer.promise;

Now out in Canada somewhere, our websocket server processes the request and sends back a list of customers to us through the websocket. When data comes in from the websocket we call the listener function:

ws.onmessage = function(message) {
        listener(message);
    };

The listener looks at the message coming in and sees that it looks something like this:

{
  "result": true,
  "callback_id": 1,
  "data": [
    {
      first_name: Danny,
      last_name: Ocean
    },
    {
      first_name: Rusty,
      last_name: Ryan
    }
  ]
}

The listener() function sees the callback_id property and looks in our callbacks variable to see if we have a pending request waiting to be resolved. If there is one, it resolves the deferred object and deletes the callback object from the callbacks object-literal/dictionary/named-array:

if(callbacks.hasOwnProperty(messageObj.callback_id)) {
      console.log(callbacks[messageObj.callback_id]);
      $rootScope.$apply(callbacks[messageObj.callback_id].cb.resolve(messageObj.data));
      delete callbacks[messageObj.callbackID];
    }
:-)

And then, lo and behold, our scope variable, $scope.customers, is populated with our new customer list! And now you have a functioning websocket service. 

I know this all can seem like a lot if you are new to angular or haven’t heard of promises before. Feel free to ask any questions in the comments or email me on my contact form if you need help. I am usually pretty good about getting back to you.