這是"AngularJS – 七步從菜鳥到專家"系列的第六篇。
在第一篇,我們展示了如何開始搭建一個AngularaJS應(yīng)用。在第五篇我們討論了Angular內(nèi)建的directives。在這一章,我們來討論services,整理我們的代碼并完成我們的音頻播放器應(yīng)用。
通過這整個系列的教程,我們會開發(fā)一個NPR(美國全國公共廣播電臺)廣播的音頻播放器,它能顯示Morning Edition節(jié)目里現(xiàn)在播出的最新故事,并在我們的瀏覽器里播放。完成版的Demo可以看看這里。
目前為止,我們把注意力都放在了如何把視圖綁定到$scope和如何用controller管理數(shù)據(jù),從內(nèi)存和效率角度出 發(fā),controllers僅當(dāng)需要的時候才會被實(shí)例化并在不需要的時候被丟棄掉,這就意味著每一次我們使用route跳轉(zhuǎn)或者重載視圖(我們會在下一篇 討論routing),當(dāng)前的controller會被銷毀。
Services可以讓我們在整個應(yīng)用的生命周期中保存數(shù)據(jù)并且可以讓controllers之間共享數(shù)據(jù)。
第六部分:Services
Services都是單例的,就是說在一個應(yīng)用中,每一個Serice對象只會被實(shí)例化一次(用$injector服務(wù)),主要負(fù)責(zé)提供一個接口把 特定函數(shù)需要的方法放在一起,我們就拿上一章見過的$http Service來舉例,他就提供了訪問底層瀏覽器的XMLHttpRequest對象的方法,相較于調(diào)用底層的XMLHttpRequest對 象,$http API使用起來相當(dāng)?shù)暮唵巍?/p>
Angular內(nèi)建了很多服務(wù)供我們?nèi)粘J褂?#xff0c;這些服務(wù)對于在復(fù)雜應(yīng)用中建立自己的Services都是相當(dāng)有用的。
AngularJS讓我們可以輕松的創(chuàng)建自己的services,僅僅注冊service即可,一旦注冊,Angular編譯器就可以找到并加載他作為依賴供程序運(yùn)行時使用,
最常見的創(chuàng)建方法就是用angular.module API 的factory模式
angular.module('myApp.services',?[])? ??.factory('githubService',?function()?{? ????var?serviceInstance ?=?{};? ????//?我們的第一個服務(wù)? ????return?serviceInstance;? ??});?
當(dāng)然,我們也可以使用內(nèi)建的$provide service來創(chuàng)建service。
這個服務(wù)并沒有做實(shí)際的事情,但是他向我們展示了如何去定義一個service。創(chuàng)建一個service就是簡單的返回一個函數(shù),這個函數(shù)返回一個對象。這個對象是在創(chuàng)建應(yīng)用實(shí)例的時候創(chuàng)建的(記住,這個對象是單例對象)
我們可以在這個縱貫整個應(yīng)用的單例對象里處理特定的需求,在上面的例子中,我們開始創(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)建了一個只有一個方法的GitHub Service,events可以獲取到給定的GitHub用戶最新的GitHub事件,為了把這個服務(wù)添加到我們的controller中。我們建立一 個controller并加載(或者注入)githubService作為運(yùn)行時依賴,我們把service的名字作為參數(shù)傳遞給controller 函數(shù)(使用中括號[])
app.controller('ServiceController',?['$scope',?'githubService',? ????function($scope,?githubService)?{? }]);?
請注意,這種依賴注入的寫法對于js壓縮是安全的,我們會在以后的章節(jié)中深入導(dǎo)論這件事情。
我們的githubService注入到我們的ServiceController后,我們就可以像使用其他服務(wù)(我們前面提到的$http服務(wù))一樣的使用githubService了。
我們來修改一下我們的示例代碼,對于我們視圖中給出的GitHub用戶名,調(diào)用GitHub API,就像我們在數(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ù)綁定,只要我們修改了視圖,對應(yīng)的model數(shù)據(jù)也會修改
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方法
?
(示例截圖,請前往原文測試)
在這個示例中,我們注意到輸入框內(nèi)容改變前有一些延遲,如果我們不設(shè)置延遲,那么我們就會對鍵入輸入框的每一個字符調(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不會發(fā)送任何網(wǎng)絡(luò)請求。換句話說,如果在鍵盤輸入時超過350毫秒,我們就假定用戶已經(jīng)完成輸入,我們就可以開始向GitHub發(fā)送請求
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還可以在多個controllers之間共享數(shù)據(jù)。比 如,如果我們的應(yīng)用有一個設(shè)置頁面供用戶設(shè)置他們的GitHub username,那么我們就要需要把username與其他controllers共享。
這個系列的最后一章我們會討論路由以及如何在多頁面中跳轉(zhuǎn)。
為了在controllers之間共享username,我們需要在service中存儲username,記住,在應(yīng)用的生命周期中Service是一直存在的,所以可以把username安全的存儲在這里
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方法,方便我們設(shè)置GitHub用戶名,在應(yīng)用的任何controller中,我們都可以調(diào)用events()方法,而根本不用操心在scope對象中的username設(shè)置是否正確。
我們應(yīng)用里的Services
在我們的應(yīng)用里,我們需要為3個元素創(chuàng)建對應(yīng)的服務(wù):audio元素,player元素,nprService。最簡單的就是audio service,切記,不要在controller中有任何的操控DOM的行為,如果這么做會污染你的controller并留下潛在的隱患。
在我們的應(yīng)用中,PlayerController中有一個audio element元素的實(shí)例
app.controller('PlayerController',?['$scope',?'$http',? ??function($scope,?$http)?{? ??var?audio ?=? document .createElement('audio');? ??$scope.audio ?=?audio;? ??//?...?
我們可以建立一個單例audio service,而不是在controller中設(shè)置audio元素
app.factory('audio',?['$document',?function($document)?{? ??var?audio ?=?$document[0].createElement('audio');? ??return?audio;? }]);?
注意:我們使用了另一個內(nèi)建服務(wù)$document服務(wù),這個服務(wù)就是window.document元素(所有html頁面里javascript的根對象)的引用。
現(xiàn)在,在我們的PlayController中我們可以引用這個audio元素,而不是在controller中建立這個audio元素
app.controller('PlayerController',?['$scope',?'$http',?'audio',? ??function($scope,?$http,?audio)?{? ??$scope.audio ?=?audio;?
盡管看起來我們并沒有增強(qiáng)代碼的功能或者讓代碼更加清晰,但是如果有一天,PlayerController不再需要audio service了,我們只需要簡單刪除這個依賴就可以了。到那個時候你就能切身體會到這種代碼寫法的妙處了!
注意:現(xiàn)在我們可以在其他應(yīng)用中共享audio service了,因?yàn)樗]有綁定特定于本應(yīng)用的功能
為了看到效果,我們來建立下一個服務(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方法并存儲播放器狀態(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來管理跟播放相關(guān)的操作,只需要把控制權(quán)交給PlayController里的player service即可
app.controller('PlayerController',?['$scope',?'player',? ??function($scope,?player)?{? ??$scope.player ?=?player;? }]);?
(注:示例截圖,請到原文測試)
注意:使用player service的時候,我們不需要去考慮audio service,因?yàn)閜layer會幫我們處理audio service。
注意:當(dāng)audio播放結(jié)束,我們沒有重置播放器的狀態(tài),播放器會認(rèn)為他自己一直在播放
為了解決這個問題,我們需要使用$rootScope服務(wù)(另一個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事件會觸發(fā)外圍Angular event loop.我們會在后續(xù)的文章中討論event loop。
最后,我們可以獲取當(dāng)前播放節(jié)目的詳細(xì)信息,比如,我們創(chuàng)建一個方法獲取當(dāng)前事件和當(dāng)前audio的播放間隔(我們會用這個參數(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ù)這個事件更新播放進(jìn)度
audio.addEventListener('timeupdate',?function(evt)?{? ????$rootScope.$apply(function()?{? ??????player player.progress ?=?player.currentTime();? ??????player player.progress_percent ?=?player.progress?/?player.currentDuration();? ????});? ??});?
最后,我們一個添加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)?{? ??????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。
我們來創(chuàng)建最后一個service,nprService,這個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,但是為了展示的目的,我們在下一個post會簡單介紹promises。
當(dāng)PlayerController初始化后,我們的nprService會獲取最新節(jié)目,這樣我們在nprService service中就成功封裝了獲取NPR節(jié)目的功能。另外,我們添加RelatedController在側(cè)邊欄顯示當(dāng)前播放節(jié)目的相關(guān)內(nèi)容。當(dāng)我們的 player service中獲取到最新節(jié)目時,我們將$watc這個player.current屬性并顯示跟這個屬性相關(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)容也會改變。
在下一章也是我們的“AngularJS – 七步從菜鳥到專家”的最后一章,我們會討論依賴注入,路由,和產(chǎn)品級別工具來讓我們更快的使用AngularJS
本系列的官方代碼庫可從github上下載:https://github.com/auser/ng-newsletter-beginner-series.
要將這個代碼庫保存到本地,請先確保安裝了git,clone此代碼庫,然后check out其中的part6分支:
git?clone?https://github.com/auser/ng-newsletter-beginner-series.git? git?checkout?-b?part6? ./bin/server.sh?
總結(jié)
以上是生活随笔 為你收集整理的七步从AngularJS菜鸟到专家(6):服务 的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔 網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔 推薦給好友。