Providers
每個(gè)我們搭建的網(wǎng)站應(yīng)用都是由多個(gè)對(duì)象組成的送挑,它們互相之間交互來(lái)完成所有的工作训桶。要使得網(wǎng)站順利運(yùn)行午笛,這些對(duì)象需要首先被初始化并且聯(lián)系在一起渣刷。在AngularJS中鱼辙,大部分這樣的對(duì)象都是自動(dòng)的被Injector Service(注入器服務(wù))
來(lái)初始化和編織在一起的廉嚼。
Injector
服務(wù)創(chuàng)建兩種類型的對(duì)象:Services
(服務(wù))和特殊對(duì)象
。
Service
的API是由開(kāi)發(fā)者自己定義的倒戏。
特殊對(duì)象
則對(duì)應(yīng)了AngularJS框架自帶的一些API怠噪。這些對(duì)象的類型有:Controller(控制器)
,Directive(指令)
杜跷,Filter(過(guò)濾器)
和Animation(動(dòng)畫)
傍念。
注入器需要知道按照什么方式來(lái)創(chuàng)建這些對(duì)象。我們可以通過(guò)注冊(cè)一個(gè)recipe(配方)
定制創(chuàng)建對(duì)象的方式葛闷。一共有5種配方憋槐。
最冗長(zhǎng),但是最全面的一個(gè)是Provider配方淑趾。剩下的四種是:Value阳仔、Factory、Service和Constant扣泊,這些配方的功能和Provider一樣近范,僅僅是一些語(yǔ)法糖。
下面延蟹,我們來(lái)看一些不同的場(chǎng)景评矩,學(xué)習(xí)下如何通過(guò)不通的配方類型來(lái)創(chuàng)建和使用服務(wù)。
說(shuō)些關(guān)于模塊(Modules)的事
為了讓Injector
知道如何創(chuàng)建對(duì)象并且編織它們阱飘,我們需要注冊(cè)對(duì)應(yīng)的配方
斥杜。每個(gè)配方含有一個(gè)對(duì)象的標(biāo)識(shí)以及創(chuàng)建它的方式描述。
每個(gè)配方肯定是屬于某個(gè)Angular模塊的沥匈。一個(gè)Angular模塊就像一個(gè)包一樣蔗喂,包含了一個(gè)或者多個(gè)配方。由于手動(dòng)加載模塊依賴的其他對(duì)象是很麻煩的咐熙,所以模塊中也可以包含了需要依賴的其他模塊的信息弱恒。
當(dāng)一個(gè)Angular應(yīng)用以某個(gè)給定的模塊啟動(dòng)時(shí),Angular將先創(chuàng)建一個(gè)Injector
的實(shí)例棋恼,然后這個(gè)實(shí)例再把定義在ng模塊的所有的配方返弹、應(yīng)用模塊 锈玉、依賴等作為一個(gè)總體統(tǒng)一注冊(cè)。
值配方Value Recipe
我們先來(lái)創(chuàng)建一個(gè)簡(jiǎn)單的clientId
服務(wù)义起,它將提供一個(gè)字符串拉背,是一個(gè)遠(yuǎn)程API的通信認(rèn)證碼。你可以下面這樣定義:
var myApp = angular.module('myApp', []);
myApp.value('clientId', 'a12345654321x');
這里我們創(chuàng)建了一個(gè)叫做myApp
的Angular模塊默终,并且定制了一張配方
來(lái)創(chuàng)建clientId
服務(wù)椅棺,這里的配方就是'a12345654321x'
這個(gè)字符串,這個(gè)例子很簡(jiǎn)單不是么齐蔽?
下面你就可以通過(guò)數(shù)據(jù)綁定來(lái)顯示它了:
myApp.controller('DemoController', ['clientId', function DemoController(clientId) {
this.clientId = clientId;
}]);
<html ng-app="myApp">
<body ng-controller="DemoController as demo">
Client ID: {{demo.clientId}}
</body>
</html>
在這個(gè)例子中两疚,我們使用了值配方來(lái)定義了一個(gè)服務(wù)的返回值。當(dāng)DemoController
調(diào)用這個(gè)clientId
服務(wù)的時(shí)候含滴,將返回這個(gè)值诱渤。
相信你可以舉一反三!
工廠配方Factory Recipe
值配方寫起來(lái)非常簡(jiǎn)便谈况,但同時(shí)也缺少了一些我們創(chuàng)建服務(wù)時(shí)需要的重要特性勺美。下面我們來(lái)看看值配方的兄弟——工廠配方。工廠配方比值配方多了以下功能:
- 使用其他服務(wù)(能依賴其他服務(wù))
- 服務(wù)初始化
- 延遲加載
這個(gè)工廠配方將通過(guò)一個(gè)可帶參數(shù)(對(duì)其他服務(wù)的依賴)的函數(shù)來(lái)創(chuàng)建一個(gè)服務(wù)碑韵。函數(shù)的返回值就是按照此配方創(chuàng)建的服務(wù)實(shí)例赡茸。
注意:Angular中所有的服務(wù)都是單例的。這意味著注入器將且僅將使用一次創(chuàng)建某個(gè)對(duì)象的配方祝闻。然后注入器就會(huì)緩存所有的實(shí)例引用占卧,以便將來(lái)的使用。
工廠配方是值配方的一個(gè)更高級(jí)的形式治筒,所以上面的例子使用工廠配方同樣可以創(chuàng)建屉栓。如下:
myApp.factory('clientId', function clientIdFactory() {
return 'a12345654321x';
});
但是這里的token是簡(jiǎn)單的字符串,使用值配方似乎更加簡(jiǎn)單耸袜,并且代碼也更加簡(jiǎn)潔友多。
我們來(lái)做點(diǎn)復(fù)雜的操作,下面我們來(lái)創(chuàng)建一個(gè)能計(jì)算token用于校驗(yàn)遠(yuǎn)程API的服務(wù)堤框。這個(gè)token的叫做apiToken
域滥,它將由存儲(chǔ)在瀏覽器本地內(nèi)存的一個(gè)密鑰和客戶端編號(hào)clientId
計(jì)算而來(lái)。
myApp.factory('apiToken', ['clientId', function apiTokenFactory(clientId) {
var encrypt = function(data1, data2) {
// NSA-proof encryption algorithm:
return (data1 + ':' + data2).toUpperCase();
};
var secret = window.localStorage.getItem('myApp.secret');
var apiToken = encrypt(clientId, secret);
return apiToken;
}]);
在上述代碼中蜈抓,apiToken
服務(wù)是通過(guò)工廠配方創(chuàng)建的启绰,而這個(gè)工廠配方依賴了clientId
服務(wù)。這個(gè)工廠配方中使用了NSA-proof加密算法來(lái)產(chǎn)生一個(gè)驗(yàn)證token沟使。
最佳實(shí)踐: 工廠函數(shù)我們一般命名成
<serviceId>Factory
(比如: apiTokenFactory). 當(dāng)然命名公約不是必須的委可,它在查找代碼和查看堆棧時(shí)很有用。
就像值配方一樣,工廠配方可以創(chuàng)建一個(gè)任何類型的服務(wù)着倾,可以是基本類型拾酝,也可以是函數(shù),或者自定義類型的實(shí)例卡者。
服務(wù)配方 Service Recipe
JavaScript開(kāi)發(fā)者經(jīng)常使用自定義類型去編寫面像對(duì)象的代碼蒿囤。下面是一個(gè)自定義實(shí)例——unicornLauncher
服務(wù),它將把我們的獨(dú)角獸發(fā)射到空中【注解:我也沒(méi)看懂為啥舉這個(gè)例子崇决,獨(dú)角獸有其他含義材诽?】
function UnicornLauncher(apiToken) {
this.launchedCount = 0;
this.launch = function() {
// Make a request to the remote API and include the apiToken
...
this.launchedCount++;
}
}
現(xiàn)在我們已經(jīng)準(zhǔn)備好發(fā)射獨(dú)角獸了,不過(guò)apiToken
服務(wù)還沒(méi)有依賴進(jìn)來(lái)恒傻,我們通過(guò)工廠配方來(lái)將其引用進(jìn)來(lái):
myApp.factory('unicornLauncher', ["apiToken", function(apiToken) {
return new UnicornLauncher(apiToken);
}]);
然而這個(gè)例子最佳的解決方案是使用服務(wù)配方脸侥。
服務(wù)配方也能像工廠配方和值配方一樣創(chuàng)建一個(gè)服務(wù),但它是通過(guò)使用new
操作符來(lái)調(diào)用一個(gè)構(gòu)造器
來(lái)實(shí)現(xiàn)的碌冶。這個(gè)構(gòu)造器可以接受0個(gè)或者多個(gè)參數(shù)湿痢,用于傳入這個(gè)服務(wù)實(shí)例所依賴的服務(wù)涝缝。
注意:服務(wù)配方的設(shè)計(jì)方式遵循構(gòu)造器注入
上文中我們已經(jīng)為UnicornLauncher設(shè)計(jì)了一個(gè)構(gòu)造器扑庞,所以我們只需要通過(guò)下面的代碼就可以把上文中的工廠配方編程服務(wù)配方了:
myApp.service('unicornLauncher', ["apiToken", UnicornLauncher]);
是不是更加簡(jiǎn)單了!>艽罐氨!
注意: 我們把配方的一種叫做了“服務(wù)”。我們很后悔這么做滩援,而且我們知道我們?cè)缤頃?huì)被懲罰的栅隐。這就像我們把我們的子孫叫做“孩子”一樣,這樣他的老師就會(huì)很郁悶了(注解:老師喊他的名字的時(shí)候玩徊,其他小孩就會(huì)納悶)租悄。
提供者配方Provider Recipe
之前我們提到過(guò),提供者配方是核心配方恩袱,其他配方只是在其基礎(chǔ)之上構(gòu)建的語(yǔ)法糖而已泣棋。提供者配方非常冗長(zhǎng)繁瑣,但是其功能也最強(qiáng)大畔塔,只是對(duì)于大多數(shù)情況來(lái)說(shuō)潭辈,使用它有點(diǎn)過(guò)了。
提供者配方語(yǔ)法上是一個(gè)實(shí)現(xiàn)了$get
方法的自定義類型澈吨。這個(gè)方法其實(shí)是一個(gè)工廠方法把敢,和我們之前討論的工廠配方?jīng)]什么不通。實(shí)際上谅辣,當(dāng)你創(chuàng)建了一個(gè)工廠配方時(shí)修赞,本質(zhì)是創(chuàng)建了一個(gè)提供者配方,只是其中的$get
方法指向了你的工廠方法而已桑阶,當(dāng)然這一切對(duì)用戶來(lái)說(shuō)都是透明的柏副。
使用提供者配方只有一個(gè)場(chǎng)景熙尉,那就是當(dāng)你需要暴露一些可以在應(yīng)用啟動(dòng)之前配置應(yīng)用的API的時(shí)候。這在各個(gè)AngularJS應(yīng)用重用服務(wù)的時(shí)候特別有用搓扯。
我們的unicornLauncher
服務(wù)太了检痰,所以很多應(yīng)用(AngularJS應(yīng)用)都在用它。默認(rèn)的情況下锨推,獨(dú)角獸號(hào)沒(méi)有護(hù)盾铅歼。但是在很多星球上,大氣層非常的厚换可,以至于我們必須要給獨(dú)角獸號(hào)裝上錫紙護(hù)盾椎椰,這樣它就可以翱翔在銀河系中惹,而不是燒毀在大氣層中U傣(PS:老外的教程就是這么體貼慨飘,連舉例都像那么回事!一點(diǎn)不馬虎)译荞。要是能夠在每個(gè)發(fā)射應(yīng)用中配置發(fā)射時(shí)是否安裝錫紙護(hù)盾就好了瓤的,應(yīng)用就可以根據(jù)自己的大氣層情況決定是否安裝了。 答案當(dāng)然是可以的吞歼。我們可以讓它變成可配置的:
myApp.provider('unicornLauncher', function UnicornLauncherProvider() {
var useTinfoilShielding = false;
this.useTinfoilShielding = function(value) {
useTinfoilShielding = !!value;
};
this.$get = ["apiToken", function unicornLauncherFactory(apiToken) {
// let's assume that the UnicornLauncher constructor was also changed to
// accept and use the useTinfoilShielding argument
return new UnicornLauncher(apiToken, useTinfoilShielding);
}];
});
為了開(kāi)啟錫紙護(hù)盾圈膏,我們需要?jiǎng)?chuàng)建一個(gè)配置模塊,并且把UnicornLauncherProvider
注入進(jìn)去篙骡。
myApp.config(["unicornLauncherProvider", function(unicornLauncherProvider) {
unicornLauncherProvider.useTinfoilShielding(true);
}]);
注意獨(dú)角獸號(hào)提供者已經(jīng)被注入了配置函數(shù)稽坤。這次注入是由提供者注入器來(lái)注入的,它不同于普通的實(shí)例注入器糯俗。提供者注入器只注入和綁定所有的提供者實(shí)例尿褪。
在應(yīng)用啟動(dòng)階段,在Angular創(chuàng)建所有服務(wù)之前得湘,它配置并且初始化所有的提供者杖玲。我們把這個(gè)階段叫做配置階段,它是Angular應(yīng)用生命周期的一部分忽刽。在這個(gè)階段中天揖,服務(wù)是無(wú)法被訪問(wèn)的,因?yàn)樗鼈冞€沒(méi)有被初始化跪帝。
一旦配置階段結(jié)束后今膊,與提供者的交互舊結(jié)束了,配置和初始化服務(wù)的階段開(kāi)始了伞剑,我們把后面的這個(gè)階段叫做運(yùn)行階段斑唬。
常量配方 Constant Recipe
我們剛剛知道了Angular把生命周期分為配置階段和運(yùn)行階段,并且你應(yīng)該也知道了你可以通過(guò)配置函數(shù)來(lái)配置你的應(yīng)用。因?yàn)榕渲秒A段中恕刘,所有服務(wù)實(shí)例都是無(wú)法被訪問(wèn)的缤谎,簡(jiǎn)單的值配方實(shí)例(Value Objects)也是如此。
但是確實(shí)有些時(shí)候我們需要在配置階段訪問(wèn)一些變量褐着,而不是采用硬編碼坷澡。而且大多數(shù)時(shí)候我們需要在配置和運(yùn)行階段都能訪問(wèn)到這些變量。這就輪到常量配方( Constant Recipe)來(lái)大顯身手了含蓉。
下面我們要在配置階段在準(zhǔn)備發(fā)射時(shí)為我們的獨(dú)角獸號(hào)打上星球名稱的標(biāo)簽频敛。星球名稱是應(yīng)用指定的,并且在運(yùn)行時(shí)期能被各種各樣的控制器使用馅扣。為了達(dá)到這個(gè)效果斟赚,我們可以創(chuàng)建一個(gè)常量:
myApp.constant('planetName', 'Greasy Giant');
然后,我們就可以像下面這樣來(lái)配置unicornLauncherProvider
了:
myApp.config(['unicornLauncherProvider', 'planetName', function(unicornLauncherProvider, planetName) {
unicornLauncherProvider.useTinfoilShielding(true);
unicornLauncherProvider.stampText(planetName);
}]);
因?yàn)槌A颗浞皆谶\(yùn)行階段也能被訪問(wèn)差油,所以我們也可以在控制器和模版中使用它:
myApp.controller('DemoController', ["clientId", "planetName", function DemoController(clientId, planetName) {
this.clientId = clientId;
this.planetName = planetName;
}]);
<html ng-app="myApp">
<body ng-controller="DemoController as demo">
Client ID: {{demo.clientId}}
<br>
Planet Name: {{demo.planetName}}
</body>
</html>
特殊目的對(duì)象 Special Purpose Objects
之前我們談到過(guò)有一些不同于服務(wù)的特殊目的對(duì)象拗军。這些對(duì)象是Angular框架的插件式擴(kuò)展,因此它們都實(shí)現(xiàn)了Angular指定的接口蓄喇。這些接口包含:控制器发侵、指令(Directive)、過(guò)濾器和動(dòng)畫公罕。
注入器創(chuàng)建特殊對(duì)象時(shí)(以及控制器對(duì)象的異常)所使用的指令是通過(guò)工廠配方實(shí)現(xiàn)的器紧,當(dāng)然這個(gè)實(shí)現(xiàn)過(guò)程對(duì)用戶來(lái)說(shuō)是透明的。
下面我們舉個(gè)簡(jiǎn)單的例子來(lái)說(shuō)明如果通過(guò)指令A(yù)PI(它依賴于我們之前的那個(gè)常量配方plantName
楼眷,在本例中它叫"Plant Name:Greasy Giant")創(chuàng)建一個(gè)簡(jiǎn)單的控件。
由于指令的注冊(cè)是通過(guò)工廠配方來(lái)完成的熊尉,因此我們可以使用和工廠配方相同的語(yǔ)法:
myApp.directive('myPlanet', ['planetName', function myPlanetDirectiveFactory(planetName) {
// directive definition object
return {
restrict: 'E',
scope: {},
link: function($scope, $element) { $element.text('Planet: ' + planetName); }
}
}]);
然后我們就可以像下面一樣使用這個(gè)控件了:
<html ng-app="myApp">
<body>
<my-planet></my-planet>
</body>
</html>
通過(guò)這種工廠配方的方式罐柳,我們可以定義AnuglarJS的過(guò)濾器和注解,但是定義控制器則有點(diǎn)特別狰住。我們定義一個(gè)控制器為自定義類型张吉,并且通過(guò)構(gòu)造函數(shù)的參數(shù)申明它的依賴服務(wù),然后控制器將作為模塊的一部分被注冊(cè)催植。我們來(lái)看看之前我們的一個(gè)例子DemoController
:
myApp.controller('DemoController', ['clientId', function DemoController(clientId) {
this.clientId = clientId;
}]);
每次應(yīng)用需要一個(gè)DemoController
實(shí)例的時(shí)候(在我們的例子應(yīng)用中肮蛹,只有一次),DemoController
都將通過(guò)構(gòu)造函數(shù)來(lái)創(chuàng)建一個(gè)新的實(shí)例创南。因此控制器和服務(wù)不一樣伦忠,因?yàn)樗鼈儾皇菃卫摹C慨?dāng)控制器的構(gòu)造函數(shù)被調(diào)用時(shí)稿辙,都會(huì)調(diào)用所依賴的服務(wù)(本例子中是clientId
服務(wù))昆码。
結(jié)論
讓我們總結(jié)幾點(diǎn)重要的:
- 注入器使用配方來(lái)創(chuàng)建兩類對(duì)象:服務(wù) 和 特殊對(duì)象
- 一共有5種配方:工廠配方、值配方、服務(wù)配方赋咽、提供者配方旧噪、常量配方附帽。
- 工廠配方和服務(wù)配方是最常用的配方啊奄。它們兩者的區(qū)別在于:服務(wù)配方適用于創(chuàng)建自定義類型的實(shí)例蟀苛,而工廠配方可以更好的創(chuàng)建JavaScript基本類型和函數(shù)的實(shí)例怠肋。
- 提供者配方是核心配方蛮寂,并且其他的配方都是基于提供者配方的語(yǔ)法糖莉钙。
- 提供者是最復(fù)雜的配方類型纯路,除非你需要為一些重用的代碼做全局多態(tài)化配置宛瞄,否則盡量少用缤骨。(注解:舉個(gè)例子爱咬,你是老板,有3個(gè)司機(jī)(多應(yīng)用)和一輛寶馬(公用代碼)绊起,你希望每次喊他們服務(wù)來(lái)服務(wù)的時(shí)候精拟,都能開(kāi)你的寶馬,但是3個(gè)司機(jī)各有的坐墊癖好虱歪,有人喜歡皮的蜂绎,有人喜歡布的,有人則不喜歡坐墊笋鄙。這就是對(duì)同一輛寶馬的多太)
- 所有的特殊對(duì)象除了控制器师枣,都是通過(guò)工廠配方定義的。
Features Recipe type | Factory | Service | Value | Constant | Provider |
---|---|---|---|---|---|
can have dependencies | yes | yes | no | no | yes |
uses type friendly injection | no | yes | yes* | yes* | no |
object available in config phase | no | no | no | yes | yes** |
can create functions | yes | yes | yes | yes | yes |
can create primitives | yes | no | yes | yes | yes |
* 需要使用new
操作符來(lái)完成初始化
** 在配置階段是無(wú)法訪問(wèn)服務(wù)對(duì)象的萧落,但是可以訪問(wèn)提供者對(duì)象践美。