七步從AngularJS菜鳥到專家(6):服務(wù)
這是"AngularJS – 七步從菜鳥到專家"系列的第六篇。
在第一篇,我們展示了如何開始搭建一個(gè)AngularaJS應(yīng)用。在第五篇我們討論了Angular內(nèi)建的directives。在這一章,我們來討論services,整理我們的代碼并完成我們的音頻播放器應(yīng)用。
通過這整個(gè)系列的教程,我們會(huì)開發(fā)一個(gè)NPR(美國全國公共廣播電臺(tái))廣播的音頻播放器,它能顯示Morning Edition節(jié)目里現(xiàn)在播出的最新故事,并在我們的瀏覽器里播放。完成版的Demo可以看看這里。
目前為止,我們把注意力都放在了如何把視圖綁定到$scope和如何用controller管理數(shù)據(jù),從內(nèi)存和效率角度出 發(fā),controllers僅當(dāng)需要的時(shí)候才會(huì)被實(shí)例化并在不需要的時(shí)候被丟棄掉,這就意味著每一次我們使用route跳轉(zhuǎn)或者重載視圖(我們會(huì)在下一篇 討論routing),當(dāng)前的controller會(huì)被銷毀。
Services可以讓我們?cè)谡麄€(gè)應(yīng)用的生命周期中保存數(shù)據(jù)并且可以讓controllers之間共享數(shù)據(jù)。
第六部分:Services
Services都是單例的,就是說在一個(gè)應(yīng)用中,每一個(gè)Serice對(duì)象只會(huì)被實(shí)例化一次(用$injector服務(wù)),主要負(fù)責(zé)提供一個(gè)接口把 特定函數(shù)需要的方法放在一起,我們就拿上一章見過的$http Service來舉例,他就提供了訪問底層瀏覽器的XMLHttpRequest對(duì)象的方法,相較于調(diào)用底層的XMLHttpRequest對(duì) 象,$http API使用起來相當(dāng)?shù)暮唵巍?/p>
Angular內(nèi)建了很多服務(wù)供我們?nèi)粘J褂?,這些服務(wù)對(duì)于在復(fù)雜應(yīng)用中建立自己的Services都是相當(dāng)有用的。
AngularJS讓我們可以輕松的創(chuàng)建自己的services,僅僅注冊(cè)service即可,一旦注冊(cè),Angular編譯器就可以找到并加載他作為依賴供程序運(yùn)行時(shí)使用,
最常見的創(chuàng)建方法就是用angular.module API 的factory模式
- angular.module('myApp.services', [])
- .factory('githubService', function() {
- var serviceInstance = {};
- // 我們的第一個(gè)服務(wù)
- return serviceInstance;
- });
當(dāng)然,我們也可以使用內(nèi)建的$provide service來創(chuàng)建service。
這個(gè)服務(wù)并沒有做實(shí)際的事情,但是他向我們展示了如何去定義一個(gè)service。創(chuàng)建一個(gè)service就是簡單的返回一個(gè)函數(shù),這個(gè)函數(shù)返回一個(gè)對(duì)象。這個(gè)對(duì)象是在創(chuàng)建應(yīng)用實(shí)例的時(shí)候創(chuàng)建的(記住,這個(gè)對(duì)象是單例對(duì)象)
我們可以在這個(gè)縱貫整個(gè)應(yīng)用的單例對(duì)象里處理特定的需求,在上面的例子中,我們開始創(chuàng)建了GitHub service,
接下來讓我們添加一些有實(shí)際意義的代碼去調(diào)用GitHub的API:
- angular.module('myApp.services', [])
- .factory('githubService', ['$http', function($http) {
- var doRequest = function(username, path) {
- return $http({
- method: 'JSONP',
- url: 'https://api.github.com/users/' + username + '/' + path + '?callback=JSON_CALLBACK'
- });
- }
- return {
- events: function(username) { return doRequest(username, 'events'); },
- };
- }]);
我們創(chuàng)建了一個(gè)只有一個(gè)方法的GitHub Service,events可以獲取到給定的GitHub用戶最新的GitHub事件,為了把這個(gè)服務(wù)添加到我們的controller中。我們建立一 個(gè)controller并加載(或者注入)githubService作為運(yùn)行時(shí)依賴,我們把service的名字作為參數(shù)傳遞給controller 函數(shù)(使用中括號(hào)[])
- app.controller('ServiceController', ['$scope', 'githubService',
- function($scope, githubService) {
- }]);
請(qǐng)注意,這種依賴注入的寫法對(duì)于js壓縮是安全的,我們會(huì)在以后的章節(jié)中深入導(dǎo)論這件事情。
我們的githubService注入到我們的ServiceController后,我們就可以像使用其他服務(wù)(我們前面提到的$http服務(wù))一樣的使用githubService了。
我們來修改一下我們的示例代碼,對(duì)于我們視圖中給出的GitHub用戶名,調(diào)用GitHub API,就像我們?cè)跀?shù)據(jù)綁定第三章節(jié)看到的,我們綁定username屬性到視圖中
- <div ng-controller="ServiceController">
- <label for="username">Type in a GitHub username</label>
- <input type="text" ng-model="username" placeholder="Enter a GitHub username, like auser" />
- <pre ng-show="username">{{ events }}</pre>
- </div>
現(xiàn)在我們可以監(jiān)視 $scope.username屬性,基于雙向數(shù)據(jù)綁定,只要我們修改了視圖,對(duì)應(yīng)的model數(shù)據(jù)也會(huì)修改
- app.controller('ServiceController', ['$scope', 'githubService',
- function($scope, githubService) {
- // Watch for changes on the username property.
- // If there is a change, run the function
- $scope.$watch('username', function(newUsername) {
- // uses the $http service to call the GitHub API
- // and returns the resulting promise
- githubService.events(newUsername)
- .success(function(data, status, headers) {
- // the success function wraps the response in data
- // so we need to call data.data to fetch the raw data
- $scope.events = data.data;
- })
- });
- }]);
因?yàn)榉祷亓?http promise(像我們上一章一樣),我們可以像直接調(diào)用$http service一樣的去調(diào)用.success方法
(示例截圖,請(qǐng)前往原文測(cè)試)
#p#
在這個(gè)示例中,我們注意到輸入框內(nèi)容改變前有一些延遲,如果我們不設(shè)置延遲,那么我們就會(huì)對(duì)鍵入輸入框的每一個(gè)字符調(diào)用GitHub API,這并不是我們想要的,我們可以使用內(nèi)建的$timeout服務(wù)來實(shí)現(xiàn)這種延遲。
如果想使用$timeout服務(wù),我們只要簡單的把他注入到我們的githubService中就可以了
- app.controller('ServiceController', ['$scope', '$timeout', 'githubService',
- function($scope, $timeout, githubService) {
- }]);
注意我們要遵守Angular services依賴注入的規(guī)范:自定義的service要寫在內(nèi)建的Angular services之后,自定義的service之間是沒有先后順序的。
我們現(xiàn)在就可以使用$timeout服務(wù)了,在本例中,在輸入框內(nèi)容的改變間隔如果沒有超過350毫秒,$timeout service不會(huì)發(fā)送任何網(wǎng)絡(luò)請(qǐng)求。換句話說,如果在鍵盤輸入時(shí)超過350毫秒,我們就假定用戶已經(jīng)完成輸入,我們就可以開始向GitHub發(fā)送請(qǐng)求
- app.controller('ServiceController', ['$scope', '$timeout', 'githubService',
- function($scope, $timeout, githubService) {
- // The same example as above, plus the $timeout service
- var timeout;
- $scope.$watch('username', function(newVal) {
- if (newVal) {
- if (timeout) $timeout.cancel(timeout);
- timeout = $timeout(function() {
- githubService.events(newVal)
- .success(function(data, status) {
- $scope.events = data.data;
- });
- }, 350);
- }
- });
- }]);
從這應(yīng)用開始,我們只看到了Services是如何把簡單的功能整合在一起,Services還可以在多個(gè)controllers之間共享數(shù)據(jù)。比 如,如果我們的應(yīng)用有一個(gè)設(shè)置頁面供用戶設(shè)置他們的GitHub username,那么我們就要需要把username與其他controllers共享。
這個(gè)系列的最后一章我們會(huì)討論路由以及如何在多頁面中跳轉(zhuǎn)。
為了在controllers之間共享username,我們需要在service中存儲(chǔ)username,記住,在應(yīng)用的生命周期中Service是一直存在的,所以可以把username安全的存儲(chǔ)在這里
- angular.module('myApp.services', [])
- .factory('githubService', ['$http', function($http) {
- var githubUsername;
- var doRequest = function(path) {
- return $http({
- method: 'JSONP',
- url: 'https://api.github.com/users/' + githubUsername + '/' + path + '?callback=JSON_CALLBACK'
- });
- }
- return {
- events: function() { return doRequest('events'); },
- setUsername: function(newUsername) { githubUsername = newUsername; }
- };
- }]);
現(xiàn)在,我們的service中有了setUsername方法,方便我們?cè)O(shè)置GitHub用戶名,在應(yīng)用的任何controller中,我們都可以調(diào)用events()方法,而根本不用操心在scope對(duì)象中的username設(shè)置是否正確。
我們應(yīng)用里的Services
在我們的應(yīng)用里,我們需要為3個(gè)元素創(chuàng)建對(duì)應(yīng)的服務(wù):audio元素,player元素,nprService。最簡單的就是audio service,切記,不要在controller中有任何的操控DOM的行為,如果這么做會(huì)污染你的controller并留下潛在的隱患。
在我們的應(yīng)用中,PlayerController中有一個(gè)audio element元素的實(shí)例
- app.controller('PlayerController', ['$scope', '$http',
- function($scope, $http) {
- var audio = document.createElement('audio');
- $scope.audio = audio;
- // ...
我們可以建立一個(gè)單例audio service,而不是在controller中設(shè)置audio元素
- app.factory('audio', ['$document', function($document) {
- var audio = $document[0].createElement('audio');
- return audio;
- }]);
注意:我們使用了另一個(gè)內(nèi)建服務(wù)$document服務(wù),這個(gè)服務(wù)就是window.document元素(所有html頁面里javascript的根對(duì)象)的引用。
現(xiàn)在,在我們的PlayController中我們可以引用這個(gè)audio元素,而不是在controller中建立這個(gè)audio元素
- app.controller('PlayerController', ['$scope', '$http', 'audio',
- function($scope, $http, audio) {
- $scope.audio = audio;
盡管看起來我們并沒有增強(qiáng)代碼的功能或者讓代碼更加清晰,但是如果有一天,PlayerController不再需要audio service了,我們只需要簡單刪除這個(gè)依賴就可以了。到那個(gè)時(shí)候你就能切身體會(huì)到這種代碼寫法的妙處了!
注意:現(xiàn)在我們可以在其他應(yīng)用中共享audio service了,因?yàn)樗]有綁定特定于本應(yīng)用的功能
為了看到效果,我們來建立下一個(gè)服務(wù): player service,在我們的當(dāng)前循環(huán)中,我們附加了play()和stop()方法到PlayController中。這些方法只跟playing audio有關(guān),所以并沒有必要綁定到PlayController,總之,使用PlayController調(diào)用player service API來操作播放器,而并不需要知道操作細(xì)節(jié)是最好不過的了。
讓我們來創(chuàng)建player service,我們需要注入我們剛剛創(chuàng)建的還熱乎的audio service 到 player service
- app.factory('player', ['audio', function(audio) {
- var player = {};
- return player;
- }]);
現(xiàn)在我們可以把原先定義在PlayerController中play()方法挪到player service中了,我們還需要添加stop方法并存儲(chǔ)播放器狀態(tài)。
- app.factory('player', ['audio', function(audio) {
- var player = {
- playing: false,
- current: null,
- ready: false,
- play: function(program) {
- // If we are playing, stop the current playback
- if (player.playing) player.stop();
- var url = program.audio[0].format.mp4.$text; // from the npr API
- player.current = program; // Store the current program
- audio.src = url;
- audio.play(); // Start playback of the url
- player.playing = true
- },
- stop: function() {
- if (player.playing) {
- audio.pause(); // stop playback
- // Clear the state of the player
- playerplayer.ready = player.playing = false;
- player.current = null;
- }
- }
- };
- return player;
- }]);
#p#
現(xiàn)在我們已經(jīng)擁有功能完善的play() and stop()方法,我們不需要使用PlayerController來管理跟播放相關(guān)的操作,只需要把控制權(quán)交給PlayController里的player service即可
- app.controller('PlayerController', ['$scope', 'player',
- function($scope, player) {
- $scope.player = player;
- }]);
(注:示例截圖,請(qǐng)到原文測(cè)試)
注意:使用player service的時(shí)候,我們不需要去考慮audio service,因?yàn)閜layer會(huì)幫我們處理audio service。
注意:當(dāng)audio播放結(jié)束,我們沒有重置播放器的狀態(tài),播放器會(huì)認(rèn)為他自己一直在播放
為了解決這個(gè)問題,我們需要使用$rootScope服務(wù)(另一個(gè)Angular的內(nèi)建服務(wù))來捕獲audio元素的ended事件,我們注入$rootScope服務(wù)并創(chuàng)建audio元素的事件監(jiān)聽器
- app.factory('player', ['audio', '$rootScope',
- function(audio, $rootScope) {
- var player = {
- playing: false,
- ready: true,
- // ...
- };
- audio.addEventListener('ended', function() {
- $rootScope.$apply(player.stop());
- });
- return player;
- }]);
在這種情況下,為了需要捕獲事件而使用了$rootScope service,注意我們調(diào)用了$rootScope.$apply()。 因?yàn)閑nded事件會(huì)觸發(fā)外圍Angular event loop.我們會(huì)在后續(xù)的文章中討論event loop。
最后,我們可以獲取當(dāng)前播放節(jié)目的詳細(xì)信息,比如,我們創(chuàng)建一個(gè)方法獲取當(dāng)前事件和當(dāng)前audio的播放間隔(我們會(huì)用這個(gè)參數(shù)顯示當(dāng)前的播放進(jìn)度)。
- app.factory('player', ['audio', '$rootScope',
- function(audio, $rootScope) {
- var player = {
- playing: false,
- // ...
- currentTime: function() {
- return audio.currentTime;
- },
- currentDuration: function() {
- return parseInt(audio.duration);
- }
- }
- };
- return player;
- }]);
在audio元素中存在timeupdate事件,我們可以根據(jù)這個(gè)事件更新播放進(jìn)度
- audio.addEventListener('timeupdate', function(evt) {
- $rootScope.$apply(function() {
- playerplayer.progress = player.currentTime();
- playerplayer.progress_percent = player.progress / player.currentDuration();
- });
- });
最后,我們一個(gè)添加canplay事件來表示視圖中的audio是否準(zhǔn)備就緒
- app.factory('player', ['audio', '$rootScope',
- function(audio, $rootScope) {
- var player = {
- playing: false,
- ready: false,
- // ...
- }
- audio.addEventListener('canplay', function(evt) {
- $rootScope.$apply(function() {
- player.ready = true;
- });
- });
- return player;
- }]);
現(xiàn)在,我們有了player service,我們需要操作nprLink directive 來讓播放器 ’play’,而不是用$scope(注意,這么做是可選的,我們也可以在PlayerController中創(chuàng)建play()和stop()方法)
在directive中,我們需要引用本地scope的player,代碼如下:
- app.directive('nprLink', function() {
- return {
- restrict: 'EA',
- require: ['^ngModel'],
- replace: true,
- scope: {
- ngModel: '=',
- player: '='
- },
- templateUrl: '/code/views/nprListItem',
- link: function(scope, ele, attr) {
- scopescope.duration = scope.ngModel.audio[0].duration.$text;
- }
- }
- });
現(xiàn)在,為了跟我們已有的模板整合,我們需要更新 index.html的npr-link調(diào)用方式
- <npr-link ng-model="program" player="player"></npr-link>
在視圖界面,我們調(diào)用play.play(ngModel),而不是play(ngModel).
- <div class="nprLink row" player="player" ng-click="player.play(ngModel)">
- <span class="name large-8 columns">
- <button class="large-2 small-2 playButton columns" ng-click="ngModel.play(ngModel)"><div class="triangle"></div></button>
- <div class="large-10 small-10 columns">
- <div class="row">
- <span class="large-12">{{ ngModel.title.$text }}</span>
- </div>
- <div class="row">
- <div class="small-1 columns"></div>
- <div class="small-2 columns push-8"><a href="{{ ngModel.link[0].$text }}">Link</a></div>
- </div>
- </div>
- </span>
- </div>
#p#
邏輯上,我們需要添加播放器視圖到總體視圖上,因?yàn)槲覀兛梢苑庋bplayer數(shù)據(jù)和狀態(tài)。查看playerView directive 和 template。
我們來創(chuàng)建最后一個(gè)service,nprService,這個(gè)service很像 githubService,我們用$http service來獲取NPR的最新節(jié)目
- app.factory('nprService', ['$http', function($http) {
- var doRequest = function(apiKey) {
- return $http({
- method: 'JSONP',
- url: nprUrl + '&apiKey=' + apiKey + '&callback=JSON_CALLBACK'
- });
- }
- return {
- programs: function(apiKey) { return doRequest(apiKey); }
- };
- }]);
在PlayerController,我們調(diào)用nprService的programs()(調(diào)用$http service)
- app.controller('PlayerController', ['$scope', 'nprService', 'player',
- function($scope, nprService, player) {
- $scope.player = player;
- nprService.programs(apiKey)
- .success(function(data, status) {
- $scope.programs = data.list.story;
- });
- }]);
我們建議使用promises來簡化API,但是為了展示的目的,我們?cè)谙乱粋€(gè)post會(huì)簡單介紹promises。
當(dāng)PlayerController初始化后,我們的nprService會(huì)獲取最新節(jié)目,這樣我們?cè)趎prService service中就成功封裝了獲取NPR節(jié)目的功能。另外,我們添加RelatedController在側(cè)邊欄顯示當(dāng)前播放節(jié)目的相關(guān)內(nèi)容。當(dāng)我們的 player service中獲取到最新節(jié)目時(shí),我們將$watc這個(gè)player.current屬性并顯示跟這個(gè)屬性相關(guān)的內(nèi)容。
- app.controller('RelatedController', ['$scope', 'player',
- function($scope, player) {
- $scope.player = player;
- $scope.$watch('player.current', function(program) {
- if (program) {
- $scope.related = [];
- angular.forEach(program.relatedLink, function(link) {
- $scope.related.push({
- link: link.link[0].$text,
- caption: link.caption.$text
- });
- });
- }
- });
- }]);
在 HTML 代碼中, we just reference the related links like we did with our NPR programs, using the ng-repeat
directive:
- <div class="large-4 small-4 columns" ng-controller="RelatedController">
- <h2>Related content</h2>
- <ul id="related">
- <li ng-repeat="s in related"><a href="{{ s.link }}">{{ s.caption }}</a></li>
- </ul>
- </div>
只要player.current內(nèi)容改變,顯示的相關(guān)內(nèi)容也會(huì)改變。
在下一章也是我們的“AngularJS – 七步從菜鳥到專家”的最后一章,我們會(huì)討論依賴注入,路由,和產(chǎn)品級(jí)別工具來讓我們更快的使用AngularJS
本系列的官方代碼庫可從github上下載:https://github.com/auser/ng-newsletter-beginner-series.
要將這個(gè)代碼庫保存到本地,請(qǐng)先確保安裝了git,clone此代碼庫,然后check out其中的part6分支:
- git clone https://github.com/auser/ng-newsletter-beginner-series.git
- git checkout -b part6
- ./bin/server.sh
原文鏈接:http://www.ng-newsletter.com/posts/beginner2expert-services.html