出于内存占用和性能的考虑,控制器只会在需要时被实例化,并且不再需要就会被销毁。这意味着每次切换路由或重新加载视图时,当前的控制器会被清除掉。
服务提供了一种能在应用的整个生命周期内保持数据的方法,它能够在控制器之间进行通信,并且能保证数据的一致性。服务是一个单例对象,在每个应用中只会被实例化一次(被$injector
实例化),并且是延迟加载的(需要时才会被创建)。服务提供了把与特定功能相关联的方法集中在一起的接口。
以$http
服务为例,它提供了对浏览器的XMLHttpRequest
对象的底层访问功能,我们可以通过$http
的API同XMLHttpRequest
进行交互,而不需要因为调用这些底层代码而污染应用。
// 示例服务,在应用的整个生命周期内保存current_user
angular.module('myApp', [])
.factory('UserService', function($http) {
var current_user;
return {
getCurrentUser: function() {
return current_user;
},
setCurrentUser: function(user) {
current_user = user;
}
};
});
在AngularJS中创建自己的服务是非常容易的:只需要注册这个服务即可。服务被注册后, AngularJS编译器就可以引用它,并且在运行时把它当作依赖加载进来。
注册一个服务
使用factory API
创建服务,是最常见也是最灵活的方式。
angular.module('myApp.services', [])
.factory('githubService', function() {
var serviceInstance = {};
// 我们的第一个服务
return serviceInstance;
});
服务的工厂函数用来生成一个单例的对象或函数,这个对象或函数就是服务,它会存在于应用的整个生命周期内。当我们的AngularJS应用加载服务时,这个函数会被执行并返回一个单例的服务对象。
同创建控制器的方法一样,服务的工厂函数既可以是一个函数也可以是一个数组。
// 用方括号声明工厂
angular.module('myApp.services', [])
.factory('githubService', ['$http',function($http) { }]);
例如,githubService
需要访问$http
服务,所以我们将$http
服务当作AngularJS应用的一个依赖,并将它注入到工厂函数中。
angular.module('myApp.services',[]).factory('githubService',function($http) {
// 我们的serviceInstance现在可以在函数定义中访问$http服务
var serviceInstance = {};
return serviceInstance;
});
现在,无论何处需要访问GitHub API
都不需要通过$http
来进行了,可以通过githubService
来代替,并让它处理所有复杂的业务逻辑和远程服务。
GitHub API
提供了一个读取用户活动流的方法(活动流就是用户记录在GitHub
中的最近的事件列表)。在我们的服务中,可以创建一个访问这个API的方法,并将API的请求结果返回。
通过将方法设置为服务对象的一个属性来将其暴露给外部。
angular.module('myApp.services', [])
.factory('githubService', function($http) {
var githubUrl = 'https://api.github.com';
var runUserRequest = function(username, path) {
// 从使用JSONP调用Github API的$http服务中返回promise
return $http({
method: 'JSONP',
url: githubUrl+'/users/username/' +
path + '?callback=JSON_CALLBACK'
});
};
// 返回带有一个events函数的服务对象
return {
events: function(username) {
return runUserRequest(username, 'events');
}
};
});
githubService
中只包含了一个方法,可以在应用的模块中调用。
使用服务
可以在控制器、指令、过滤器或另外一个服务中通过依赖声明的方式来使用服务。AngularJS会像平时一样在运行期自动处理实例化和依赖加载的相关事宜。
将服务的名字当作参数传递给控制器函数,可以将服务注入到控制器中。当服务成为了某个控制器的依赖,就可以在控制器中调用任何定义在这个服务对象上的方法。
angular.module('myApp', ['myApp.services'])
.controller('ServiceController',function($scope,githubService) {
// 我们可以调用对象的事件函数
$scope.events = githubService.events('auser');
});
githubService
服务已经被注入到ServiceController
中,可以像使用任何其他服务一样使用它。
<div ng-controller="ServiceController">
<label for="username">Type in a GitHub username</label>
<input type="text" ng-model="username" placeholder="Enter username" />
<ul>
<li ng-repeat="event in events">
<!--
event.actor and event.repo are returned by the github API.
To view the raw API, uncomment the next line:
-->
<!-- {{ event | json }} -->
{{ event.actor.login }} {{ event.repo.name }}
</li>
</ul>
</div>
基于双向数据绑定,我们现在可以通过监视$scope.username
来响应视图中的数据变化。
.controller('ServiceController',function($scope,githubService) {
// 注意username属性的变化,如果有变化就运行该函数
$scope.$watch('username', function(newUsername) {
// 从使用JSONP调用Github API的$http服务中返回promise
githubService.events(newUsername)
.success(function(data, status, headers) {
// success函数在数据中封装响应
// 因此我们需要调用data.data来获取原始数据
$scope.events = data.data;
})
});
});
由于$http
返回的是promise
对象,可以通过.success()
方法像直接调用$http
一样调用返回的对象。
在这个例子中,你可能会注意到在输入字段发生变化前,有一个延时。如果不延时,将导致输入字段中的任何一个键盘输入都会让终端对GitHub API
进行调用,这显然不是我们希望的。
通过内置服务$timeout
来介绍一下这个延时。在这个例子中$timeout
服务会取消所有网络请求,并在输入字段的两次变化之间延时350ms。换句话说,如果用户两次输入之间有350ms的间隔,就推断用户已经完成了输入,然后开始向GitHub发送请求。
app.controller('ServiceController', function($scope,$timeout,githubService) {
var timeout;
$scope.$watch('username', function(newUserName) {
if (newUserName) {
// 如果在进度中有一个超时(timeout)
if (timeout) $timeout.cancel(timeout);
timeout = $timeout(function() {
githubService.events(newUserName).success(function(data,status) {
$scope.events = data.data;
});
}, 350);
}
});
});
使用服务也是在控制器之间共享数据的典型方法。例如,如果我们的应用需要后端服务的授权,可以创建一个SessionsService
服务处理用户的授权过程,并保存服务端返回的令牌。当应用中任何地方要发送一个需要授权的请求,可以通过SessionsService
来访问令牌。
如果我们的应用中有一个用来设置GitHub用户名的设置页面,我们希望在应用中所有的控制器之间共享用户名。
为了在控制器之间共享数据,需要在服务中添加一个用来储存用户名的方法。记住,服务在应用的生命周期内是单例模式的,因此可以将用户名安全地储存在其中。
angular.module('myApp.services',[]).factory('githubService',function($http) {
var githubUrl = 'https://api.github.com',githubUsername;
var runUserRequest = function(path) {
// 从使用JSONP的Github API的$http服务中返回promise
return $http({
method: 'JSONP',
url: githubUrl + '/users/githubUsername/' +
path + '?callback=JSON_CALLBACK'
});
};
// 返回带有两个方法的服务对象、事件和setUsername
return {
events: function() {
return runUserRequest('events');
},
setUsername: function(username) {
githubUsername = username;
}
};
});
setUsername
方法用来保存当前的GitHub
用户名了。
githubService
可以注入到应用的任何一个控制器中,并可以在控制器中调用events()
方法,且无须担心当前作用域对象上的用户名是否是正确的。
angular.module('myApp',['myApp.services']).controller('ServiceController',
function($scope, githubService) {
$scope.setUsername =githubService.setUsername;
});
创建服务时的设置项
共有5种方法用来创建服务:factory()
、service()``constant()
、value()
、provider()
。
factory()
factory()
方法是创建和配置服务的最快捷方式。factory()
函数可以接受两个参数。
- name(字符串):需要注册的服务名。
- getFn(函数):这个函数会在AngularJS创建服务实例时被调用。
angular.module('myApp').factory('myService',function() {
return {'username': 'auser'};
});
因为服务是单例对象,getFn
在应用的生命周期内只会被调用一次。同其他服务一样,在定义服务时,getFn
可以接受一个包含可被注入对象的数组或函数。getFn
函数可以返回简单类型、函数乃至对象等任意类型的数据。
angular.module('myApp').factory('githubService',['$http',function($http) {
return {
getUserEvents: function(username) {
// ...
}
};
}]);
service()
使用service()
可以注册一个支持构造函数的服务,它允许我们为服务对象注册一个构造函数。
service()
方法接受两个参数。
- name(字符串):要注册的服务名称。
- constructor(函数):构造函数,我们调用它来实例化服务对象。
service()
函数会在创建实例时通过new
关键字来实例化服务对象。
var Person = function($http) {
this.getName = function() {
return $http({ method: 'GET', url: '/api/user'});
};
};
angular.service('personService', Person);
provider()
所有服务工厂都是由$provide
服务创建的,$provide
服务负责在运行时初始化这些提供者。
提供者是一个具有$get()
方法的对象,$injector
通过调用$get
方法创建服务实例。$provider
提供了数个不同的API用于创建服务,每个方法都有各自的特殊用途。
所有创建服务的方法都构建在provider
方法之上。provider()
方法负责在$providerCache
中注册服务。
从技术上说,当我们假定传入的函数就是$get()
时,factory()
函数就是用provider()
方法注册服务的简略形式。
angular.module('myApp').factory('myService',function() {
return {'username': 'auser'};
})
// 这与上面工厂的用法等价
.provider('myService', {
$get: function() {
return {'username': 'auser'};
}
});
同其他创建服务的方法不同,config()
方法可以被注入特殊的参数。
比如我们希望在应用启动前配置githubService
的URL:
// 使用`.provider`注册该服务
angular.module('myApp', [])
.provider('githubService', function($http) {
// 默认的,私有状态
var githubUrl = 'https://github.com'
setGithubUrl: function(url) {
// 通过.config改变默认属性
if (url) { githubUrl = url }
},
method: JSONP, // 如果需要,可以重写
$get: function($http) {
self = this;
return $http({method:self.method,url:githubUrl+'/events'});
}
});
通过使用.provider()
方法,可以在多个应用使用同一个服务时获得更强的扩展性。
在上面的例子中,provider()
方法在文本githubService
后添加Provider
生成了一个新的提供者,githubServiceProvider
可以被注入到config()
函数中。
angular.module('myApp', []).config(function(githubServiceProvider) {
githubServiceProvider.setGithubUrl("git@github.com");
});
如果希望在config()
函数中可以对服务进行配置,必须用provider()
来定义服务。
provider()
方法为服务注册提供者。可以接受两个参数。
- name(字符串)
name
参数在providerCache
中是注册的名字。name+Provider
会成为服务的提供者。同时name
也是服务实例的名字。
例如,如果定义了一个githubService
,那它的提供者就是githubServiceProvider
。 - aProvider(对象/函数/数组)
aProvider
可以是多种形式。
如果aProvider
是函数,那么它会通过依赖注入被调用,并且负责通过$get
方法返回一个对象。
如果aProvider
是数组,会被当做一个带有行内依赖注入声明的函数来处理。数组的最后一个元素应该是函数,可以返回一个带有$get
方法的对象。
如果aProvider
是对象,它应该带有$get
方法。
provider()
函数返回一个已经注册的提供者实例。直接使用provider()
API是最原始的创建服务的方法:
// 在模块对象上直接创建provider的例子
angular.module('myApp',[]).provider('UserService', {
favoriteColor: null,
setFavoriteColor: function(newColor) {
this.favoriteColor = newColor;
},
// $get函数可以接受injectables
$get: function($http) {
return {
'name': 'Ari',
getFavoriteColor: function() {
return this.favoriteColor||'unknown';
}
};
}
});
用这个方法创建服务,必须返回一个定义有$get()
函数的对象,否则会导致错误。
可以通过注入器来实例化服务。
// Get the injector
var injector = angular.injector(['myApp']); // Invoke our service
injector.invoke(
['UserService', function(UserService) {
// UserService returns
// {
// 'name': 'Ari',
// getFavoriteColor: function() {}
// }
}]);
.provider()
是非常强大的,可以让我们在不同的应用中共享服务。
constant()
可以将一个已经存在的变量值注册为服务,并将其注入到应用的其他部分当中。
constant()
函数可以接受两个参数。
- name(字符串):需要注册的常量的名字。
- value(常量):需要注册的常量的值(值或者对象)。
constant()
方法返回一个注册后的服务实例。
angular.module('myApp').constant('apiKey','123123123')
这个常量服务可以像其他服务一样被注入到配置函数中。
angular.module('myApp').controller('MyController',function($scope,apiKey) {
$scope.apiKey = apiKey;
});
这个常量不能被装饰器拦截。
value()
如果服务的$get
方法返回的是一个常量,那就没要必要定义一个包含复杂功能的完整服务,可以通过value()
函数方便地注册服务。
value()
方法可以接受两个参数。
- name(字符串):同样是需要注册的服务名。
- value(值):将这个值将作为可以注入的实例返回。
value()
方法返回以name
参数的值为名称的注册后的服务实例。
angular.module('myApp').value('apiKey','123123123');
何时使用value()和constant()
value()
方法和constant()
方法之间最主要的区别是,常量可以注入到配置函数中,而值不行。
通常情况下,可以通过value()
来注册服务对象或函数,用constant()
来配置数据。
angular.module('myApp', [])
.constant('apiKey', '123123123')
.config(function(apiKey) {
// 在这里apiKey将被赋值为123123123,就像上面设置的那样
})
.value('FBid','231231231')
.config(function(FBid) {
// 这将抛出一个错误,未知的provider: FBid
// 因为在config函数内部无法访问这个值
});
decorator()
$provide
服务提供了在服务实例创建时对其进行拦截的功能,可以对服务进行扩展,或者用另外的内容完全代替它。
装饰器是非常强大的,它不仅可以应用在我们自己的服务上,也可以对AngularJS的核心服务进行拦截、中断甚至替换功能的操作。
对服务进行装饰的场景有很多,比如对服务进行扩展,将外部数据缓存进localStorage
的功能,或者对服务进行封装以便在开发中进行调试和跟踪等。
decorator()
函数可以接受两个参数。
- name(字符串):将要拦截的服务名称。
- decoratorFn(函数):在服务实例化时调用该函数,这个函数由
injector.invoke
调用,可以将服务注入这个函数中。
$delegate
是可以进行装饰的最原始的服务,为了装饰其他服务,需要将其注入进装饰器。
//给githubService添加装饰器,从而为每个请求都加上一个时间戳
var githubDecorator = function($delegate,$log) {
var events = function(path) {
var startedAt = new Date();
var events = $delegate.events(path);
// 事件是一个promise
events.finally(function() {
$log.info("Fetching events took"+(new Date()-startedAt)+"ms");
});
return events;
};
return {events: events};
};
angular.module('myApp').config(function($provide) {
$provide.decorator('githubService',githubDecorator);
});