這是"AngularJS – 七步從菜鳥到專家"系列的第六篇。
在第一篇,我們展示了如何開始搭建一個(gè)AngularaJS應(yīng)用。在第五篇我們討論了Angular內(nèi)建的directives。在這一章,我們來(lái)討論services,整理我們的代碼并完成我們的音頻播放器應(yīng)用。
通過(guò)這整個(gè)系列的教程,我們會(huì)開發(fā)一個(gè)NPR(美國(guó)全國(guó)公共廣播電臺(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都是單例的,就是說(shuō)在一個(gè)應(yīng)用中,每一個(gè)Serice對(duì)象只會(huì)被實(shí)例化一次(用$injector服務(wù)),主要負(fù)責(zé)提供一個(gè)接口把 特定函數(shù)需要的方法放在一起,我們就拿上一章見過(guò)的$http Service來(lái)舉例,他就提供了訪問(wèn)底層瀏覽器的XMLHttpRequest對(duì)象的方法,相較于調(diào)用底層的XMLHttpRequest對(duì) 象,$http API使用起來(lái)相當(dāng)?shù)暮?jiǎn)單。
Angular內(nèi)建了很多服務(wù)供我們?nèi)粘J褂?#xff0c;這些服務(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來(lái)創(chuàng)建service。
這個(gè)服務(wù)并沒(méi)有做實(shí)際的事情,但是他向我們展示了如何去定義一個(gè)service。創(chuàng)建一個(gè)service就是簡(jiǎn)單的返回一個(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,
接下來(lái)讓我們添加一些有實(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了。
我們來(lái)修改一下我們的示例代碼,對(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è)試)
在這個(gè)示例中,我們注意到輸入框內(nèi)容改變前有一些延遲,如果我們不設(shè)置延遲,那么我們就會(huì)對(duì)鍵入輸入框的每一個(gè)字符調(diào)用GitHub API,這并不是我們想要的,我們可以使用內(nèi)建的$timeout服務(wù)來(lái)實(shí)現(xiàn)這種延遲。
如果想使用$timeout服務(wù),我們只要簡(jiǎn)單的把他注入到我們的githubService中就可以了
app.controller('ServiceController',?['$scope',?'$timeout',?'githubService',? ????function($scope,?$timeout,?githubService)?{? }]);?
注意我們要遵守Angular services依賴注入的規(guī)范:自定義的service要寫在內(nèi)建的Angular services之后,自定義的service之間是沒(méi)有先后順序的。
我們現(xiàn)在就可以使用$timeout服務(wù)了,在本例中,在輸入框內(nèi)容的改變間隔如果沒(méi)有超過(guò)350毫秒,$timeout service不會(huì)發(fā)送任何網(wǎng)絡(luò)請(qǐng)求。換句話說(shuō),如果在鍵盤輸入時(shí)超過(guò)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是如何把簡(jiǎn)單的功能整合在一起,Services還可以在多個(gè)controllers之間共享數(shù)據(jù)。比 如,如果我們的應(yīng)用有一個(gè)設(shè)置頁(yè)面供用戶設(shè)置他們的GitHub username,那么我們就要需要把username與其他controllers共享。
這個(gè)系列的最后一章我們會(huì)討論路由以及如何在多頁(yè)面中跳轉(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。最簡(jiǎn)單的就是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頁(yè)面里javascript的根對(duì)象)的引用。
現(xiàn)在,在我們的PlayController中我們可以引用這個(gè)audio元素,而不是在controller中建立這個(gè)audio元素
app.controller('PlayerController',?['$scope',?'$http',?'audio',? ??function($scope,?$http,?audio)?{? ??$scope.audio ?=?audio;?
盡管看起來(lái)我們并沒(méi)有增強(qiáng)代碼的功能或者讓代碼更加清晰,但是如果有一天,PlayerController不再需要audio service了,我們只需要簡(jiǎn)單刪除這個(gè)依賴就可以了。到那個(gè)時(shí)候你就能切身體會(huì)到這種代碼寫法的妙處了!
注意:現(xiàn)在我們可以在其他應(yīng)用中共享audio service了,因?yàn)樗](méi)有綁定特定于本應(yīng)用的功能
為了看到效果,我們來(lái)建立下一個(gè)服務(wù): player service,在我們的當(dāng)前循環(huán)中,我們附加了play()和stop()方法到PlayController中。這些方法只跟playing audio有關(guān),所以并沒(méi)有必要綁定到PlayController,總之,使用PlayController調(diào)用player service API來(lái)操作播放器,而并不需要知道操作細(xì)節(jié)是最好不過(guò)的了。
讓我們來(lái)創(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? ????????player player.ready ?=?player .playing ?=? false ;? ????????player.current ?=? null ;? ??????}? ????}? ??};? ??return?player;? }]);
現(xiàn)在我們已經(jīng)擁有功能完善的play() and stop()方法,我們不需要使用PlayerController來(lái)管理跟播放相關(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é)束,我們沒(méi)有重置播放器的狀態(tài),播放器會(huì)認(rèn)為他自己一直在播放
為了解決這個(gè)問(wèn)題,我們需要使用$rootScope服務(wù)(另一個(gè)Angular的內(nèi)建服務(wù))來(lái)捕獲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()?{? ??????player player.progress ?=?player.currentTime();? ??????player player.progress_percent ?=?player.progress?/?player.currentDuration();? ????});? ??});?
最后,我們一個(gè)添加canplay事件來(lái)表示視圖中的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 來(lái)讓播放器 ’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)?{? ??????scope scope.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 > ?
邏輯上,我們需要添加播放器視圖到總體視圖上,因?yàn)槲覀兛梢苑庋bplayer數(shù)據(jù)和狀態(tài)。查看playerView directive?和?template。
我們來(lái)創(chuàng)建最后一個(gè)service,nprService,這個(gè)service很像 githubService,我們用$http service來(lái)獲取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來(lái)簡(jiǎn)化API,但是為了展示的目的,我們?cè)谙乱粋€(gè)post會(huì)簡(jiǎn)單介紹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í)別工具來(lái)讓我們更快的使用AngularJS
本系列的官方代碼庫(kù)可從github上下載:https://github.com/auser/ng-newsletter-beginner-series.
要將這個(gè)代碼庫(kù)保存到本地,請(qǐng)先確保安裝了git,clone此代碼庫(kù),然后check out其中的part6分支:
git?clone?https://github.com/auser/ng-newsletter-beginner-series.git? git?checkout?-b?part6? ./bin/server.sh?
總結(jié)
以上是生活随笔 為你收集整理的七步从AngularJS菜鸟到专家(6):服务 的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
如果覺(jué)得生活随笔 網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔 推薦給好友。