Angular学习笔记(9)—服务

出于内存占用和性能的考虑,控制器只会在需要时被实例化,并且不再需要就会被销毁。这意味着每次切换路由或重新加载视图时,当前的控制器会被清除掉。
服务提供了一种能在应用的整个生命周期内保持数据的方法,它能够在控制器之间进行通信,并且能保证数据的一致性。服务是一个单例对象,在每个应用中只会被实例化一次(被$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);
});
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,598评论 18 139
  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,745评论 6 342
  • 版本:Angular 5.0.0-alpha 依赖注入是重要的应用设计模式。它使用得非常广泛,以至于几乎每个人都称...
    soojade阅读 2,985评论 0 3
  • 春去秋来,花谢花开,看着大一新生稚嫩的脸庞,飒爽的军装,惊觉原来时光竟是这般的悄无声息,迷迷茫茫,跌跌撞撞,便已在...
    时光涂鸦阅读 413评论 1 1
  • 夏天一到,让人最讨厌的就是蚊子。睡个午觉,蚊子在身边飞来飞去还嗡嗡作响,简直就想一巴掌扇死它,可是怎么扇都扇不到,...
    谁的孤独是一颗眼泪阅读 412评论 0 1