這是用前端框架AngularJS構建一個簡單應用的部分代碼,首頁配製一個路由。當然,AngularJS路由功能是一個純前端的解決方案,與我們熟悉的後臺路由不太一樣。前端路由需要提前對指定 的(ng-app),定義路由規則(routeProvider),然後通過不同的URL,告訴(ng-app)加載哪個頁面(HTML),再渲染到(ng-app)視圖(ng-view)中。雖然URL輸入不一樣,頁面展示不一樣,其實完成的是單頁(ng-app)視圖(ng-view)的局部刷新。
由上圖可以知道:
控制器:"courseCtrl"是我們定義的一個控制器,在定義這個控制器的時候我們注入了一個$scope和helloAngular。當一個應用中兩個控制有相同內容時,我們就定義一個公共函數來存放,這個公共函數在Angular中就叫服務,這個例子中就是helloAngular。
指令:上圖中diretive函數就是定義一個指令。名稱爲ezNamecard。然而指令有自定義的,也有內置的指令,如ng-repeat就是一個內置指令。
那麼問題來了,我們該如何定義一個控制器,定義一個服務,定義一個指令?注入又是一個什麼情況?Angular又是如何啓動的?關鍵是AngularJS到底又是怎麼樣解析這些瀏覽器根本不認識的指令的了?
或許這些都是你的疑惑,又或許這些都是你想知道的。因此,就有了下面這篇文章。
爲了讓整篇文章讀起來更容易點的,我羅列了一個大概的目錄:
第一部分:AngularJS開發方式
託互聯網日新月異發展的福,瀏覽器變成了人們接入互聯網的入口,而JavaScript這個超級醜小鴨,在21世紀第一個十年後期,隨着google一系列WEB應用的流行,終於成功地站到了舞臺的中央,喚起了開發者對JavaScript的興趣。
jQuery同一小撮對瀏覽器極其熟稔的極客負責抹平不同瀏覽器的差異,其他開發者只需要基於jQuery進行開發,可以更好地關注業務實現,而不是把時間花在適配不同的瀏覽器上。
試着用jQuery實現一個簡單的時鐘頁面,實現思路很簡單。
- 引入jQuery庫
- 在DOM文檔就緒後,啓動一個1秒1次的定時器
- 在定時器每次觸發時,取當前時間值,更新div#clock的文本
1 <body>
2 <div id="clock"></div>
3 </body>
1 $(function(){
2 setInterval(function(){
3 var d = new Date();
4 $("#clock").text(d.toString());
5 },1000)
6 })
世界上永遠有人在追求進步,jQuery有點像C語言,威力很大,不過要弄出點像樣的前端界面,還得花不少功夫處理瑣碎的事情。
還能再簡單些嗎?Misko Hevery認爲可以。於是,AngularJS誕生了。
AngularJS引入了以下三個主要的概念,期望讓前端開發更系統化一些。
方式一:聲明式界面開發
AngularJS最大的賣點是靜態的HTML文檔,就可以定義具有動態的頁面。
改寫時鐘例子:
1 <body ng-app="ezstuff">
2 <ez-clock></ez-clock>
3 </body>
1 angular.module("ezstuff",[])
2 .directive("ezClock",function(){
3 return {
4 restrict : "E",
5 template : "<div></div>",
6 replace : true,
7 link : function(scope,element,attrs){
8 setInterval(function(){
9 var d = new Date();
10 element.text(d.toString());
11 },1000);
12 }
13 }
14 })
對比分析一下上面列出來的這個時鐘例子:
詮釋DOM結構
在Angular中,這個HTML文件被稱爲模板,而ng-app稱之爲指令。指令可以爲HTML元素添加額外的行爲(讓HTML動起來)。在這個例子中,我們使用了一個ng-app指令,這個指令用來通知Angular自動引導應用;這個例子中還使用了一個自定義的ez-clock指令,這個指令是我們自己實現的,用來產生那個小時鐘。tips:AngularJS中指令可分爲內置指令和自定義指令,可以提前思考一下內置指令是如何使用的、自定義指令是如何定義的,以上代碼段replace=true加和不加有什麼區別,還有指令中link函數的各個參數分別起什麼作用......。這裏只負責詮釋結構,所有的問題文章後面有詳細分析。
思考代碼執行過程
我們在模板中指定了一個自定義的標籤ez-clock,而它變成了一個會動的時鐘。這中間發生了什麼?
當然,瀏覽器不會理解ez-clock這個標籤,是腳本做了這個工作。
angular.min.js引入了基本的angularJS框架,它會在瀏覽器載入HTML文檔並且建立好DOM樹後,執行以下操作:
- 找到有ng-app屬性的DOM節點
- 以這個節點爲根節點,重新解釋DOM樹,具體說就是子樹
- 在解釋過程中,發現ez-clock這個指令
- 調用ez-clock指令的實現進行展開
ez-clock的展開操作如下:
- 使用一個div元素替換這個自定義標籤
- 創建一個定時器,在定時器觸發時刷新DIV元素的innerText
ez-clock這個自定義的標籤,在AngularJS中被稱爲指令/directive,意思是看到這個指令,AngularJS基礎框架需要對其進行解釋,以便展開成瀏覽器可以理解的東西(HTML元素和腳本),而這個解釋過程,有一個特定的名稱:指令編譯。
可見,要寫的代碼一點也不會少,只是,代碼被一種新的方式重新組織了。
思考有什麼好處
首先在開發過程中,便於分工與代碼複用。
在小的項目中也可以應用AngularJS,這樣你可以得到思維的鍛鍊。但是真正發生威力,是在一個團隊中,可以有專人負責實現指令(比如:ez-clock),其他人只需要利用這些指令編寫模板,這樣的成本更低。
當然,從編寫界面HTML模板的角度看,ez-clock比div更具有語義性,使模板更容易維護,使指令的實現升級不影響模板,這也是不小的好處了。
指令算是新型的API,與我們所熟悉的對象,函數這類接口完全不同,它提供了在靜態化的HTML文件中,指定動態行爲的能力。
總之,在使用AngularJS進行前端開發時,始終應該從構造聲明式模板開始,如果現成的指令不夠用,那麼就定義自己的指令、實現自己的指令,這是一個迭代的過程。
方式二:數據的雙向綁定
數據怎麼顯示
因爲不能像jQuery一樣將DOM操作混在模板裏,聲明式模板的範式很快讓我們變得束手束腳。
一個典型的問題:在聲明式模板裏怎麼顯示數據?
假設我們有某人的基本信息,保存在一個json對象裏:
1 var sb = { 2 name : ‘somebody’, 3 gender : ‘male’, 4 age : 28 5 }
我們定義一個指令ez-namecard,希望它經過編譯後會展開成這樣:
1 <div>
2 <div>name : somebody</div>
3 <div>gender : male</div>
4 <div>age : 28</div>
5 </div>
那麼,怎麼把sb這個json對象指定給ez-namecard這個指令?
一個很容易想到的定義方法,就是給指令ez-namecard指令增加一個屬性,用這個屬性的值指明數據對象,這相當於,用屬性向指令(解釋器)傳遞參數:
1 <ez-namecard data="window.sb"></ez-namecard>
這樣的話,ez-namecard的解釋器只要檢查data屬性,然後執行一個eval()就可以獲得sb值,從而將其內容填充到展開的div片段中。
1 var sb = {
2 name : “somebody”,
3 gender : “male”,
4 age : 28
5 };
6 angular.module(“ezstuff”,[])
7 .directive(“ezNamecard”,function(){
8 return {
9 restrict : “E”,
10 template : “<div></div>”,
11 link : function(scope,element,attrs){
12 var sb = eval(attrs.data);
13 var _html = "<div>gender : " + sb.gender + “</div>"
14 + "<div>name : " + sb.name + “</div>"
15 + "<div>age : " + sb.age + “</div>”;
16 element.append(_html);
17 }
18 }
19 })
作用域(Scope)
從上個實例中看到,前面定義的sb變量,默認是掛在window對象上的,即window.sb。如果所有的數據都掛在window上,說不定哪天就會出現變量的命名衝突,所以,AngularJS引入了一個自用的命名空間,也就是$rootScope對象,這樣sb就可以掛在$rootScope上了,即$rootScope.sb。tips:注意一下下面標紅指令名稱的可命名規範,指令名稱可以這樣寫的原因文章後面講指令的時候會有詳細說明。
1 <body ng-app="ezstuff" ng-init="sb ={name:'somebody',gender:'male',age:28}">
2 <ez-namecard data="sb"></ez-namecard>
3 </body>
1 angular.module("ezstuff",[])
2 .directive("ezNamecard",function($rootScope){
3 return {
4 restrict : "E",
5 template : "<div></div>",
6 link : function(scope,element,attrs){
7 var sb = $rootScope.$eval(attrs.data);
8 var _html = "<div>name : " + sb.name + “</div>"
9 + "<div>gender : " + sb.gender + “</div>"
10 + "<div>age : " + sb.age + “</div>”;
11 element.append(_html);
12 }
13 };
14 });
看一下這個代碼,在模板頁面和控制頁面找一下變化:
HTML:ng-app指令會指示AngularJS基礎框架在啓動引導時創建一個$rootScope對象,ng-init指令用來在作用域上初始化變量,在我們的例子中,這個指令將sb變量建立在$rootScope上。
JavaScript:與之前使用eval函數進行表達式估值不同,這裏我們直接使用$rootScope的$eval方法獲得在$rootScope上sb變量的值。
再仔細看一下,$rootScope是我們在定義ezNamecard這個指令,通過函數參數表的方式注入的,而link函數,也有一個參數scope。而這兩個有什麼區別了?下面我們來理解清楚層級作用域的概念。
層級的作用域
在AngularJS中,ng-app開始的DOM子樹上,每個DOM對象都有一個對應的scope對象。
比如:在上面示例中,body對象對應一個scope對象(因爲body元素有ng-app屬性,所以這個scope就是$rootScope對象),ez-namecard對象也對應一個scope對象…。
當然,這裏必須清楚兩點。
第一:在默認情況下,一個DOM子元素不會創建新的作用域,也就是說,這個子元素所對應的scope對象,其實就是它的最近一級的祖先對象對應的scope對象。
比如:在我們的例子中,我們沒有在ez-namecard創建新的作用域,那麼它對應的scope對象,就是它的父對象即body對象的scope對象,恰好也就是$rootScope對象;而ez-namecard有3個div元素,這三個div對象由於也沒有創建新的作用域,他們對應的scope對象,則向上一直找,最終也是對應到body對象所對應的$rootScope對象。
第二:有些指令會導致創建新的作用域,像ng-controller(控制器),如果在一個DOM對象上創建了新的作用域,那麼這個scope對象的原型是其最近一級的組件對象。
比如:在我們的例子中,如果在ez-namecard上使用ng-controller指令,那麼ez-namecard對應的scope對象就不再是body對應的$roootScope對象,但是由於原型繼承,所以通過這個scope依然可以訪問到sb變量。
在談數據
數據的操作非常重要,可以更深入一點分析一下,也就是說在AngularJS這個框架中是如何玩轉數據的呢?
一、監聽數據的變化
我們已經實現了將數據顯示到界面上,不過這還不夠。由於編譯僅僅在啓動引導時執行一次,這意味着我們的link函數只會被調用一次,那麼,如果數據變化了,在界面上將不會有任何反饋,即界面和數據將變得不同步了。
這就需要持續監聽數據的變化。
好在AngularJS的scope對象直接支持對數據變化的監聽。$watch方法要求傳入兩個參數:
- 要監聽的表達式,比如:sb
- 變化時的回調函數,AngularJS將向這個函數傳入新值和舊值
1 angular.module("ezstuff",[])
2 .directive("ezNamecard",function($rootScope){
3 return {
4 restrict : "E",
5 template : "<div></div>",
6 link : function(scope,element,attrs){
7 element.append("<div>name : <span class='name'></span></div>")
8 .append("<div>gender : <span field='gender'></span></div>")
9 .append("<div>age : <span field='age'></span></div>")
10 scope.$watch(attrs.data,function(nv,ov){
11 var fields = element.find("span");
12 fields[0].innerText = nv.name;
13 fields[1].innerText = nv.gender;
14 fields[2].innerText = nv.age;
15 },true)
16 }
17 };
18 });
經過改進後的代碼,當數據被改變時,界面會自動得到更新。這時,我們稱,建立了從數據到界面的單向綁定。
二、如何修改數據?
一旦在指令解釋器中可以訪問模型,那麼使用聲明模板實現數據修改非常簡單了。
這裏我們定義一個新的指令:ez-namecard-editor,意圖讓其展開成這樣:
先看一下HTML的內容。使用ez-namecard-editor指令代替了ez-namecard,其他都一樣。
1 <body ng-app="ezstuff" ng-init="sb = {name:'somebody',gender:'male',age:28}">
2 <ez-namecard-editor data="sb"></ez-namecard-editor>
3 </body>
再看JavaScript代碼。在ez-namecard-editor的指令實現中,爲了用input中的值自動更新sb值,我們需要在解釋器中給input對象掛接上監聽函數(這裏使用keyup事件),然後在事件中修改sb變量的對應屬性就可以了。
最終的效果是,用戶在界面上進行的操作,自動地同步到了我們的數據。這時,我們稱,已經建立了從界面到數據的單向綁定。
三、數據變化的傳播
然而,我們已經分別實現了兩個方向的綁定:
數據——>界面:我們使用scope對象的$watch方法監聽數據的變化,來更新界面。
界面——>數據:我們在界面的DOM對象上監聽變化事件,來更新數據。
這時,我們就有了一個推測:
如果我們把ez-namecard和ez-namecard-editor都綁定到同一個sb對象上,那麼在ez-namecard-editor上進行編輯,將導致sb對象發生變化,由於ez-namecard監聽了這個變化,所以,ez-namecard的顯示也應該變化。
四、變化傳播的實現原理
scope維護了一個內部的監聽隊列,每次當我們在scope上執行一次$watch方法,就相當於向這個監聽隊列裏塞入一個監聽函數。
爲了捕捉對數據的修改,AngularJS要求開發者使用scope對象的$apply方法對數據進行修改,$apply方法對數據進行修改,$apply方法內部會自動地調用監聽隊列裏的監聽函數:
1 /* 修改scope上的sb對象的name屬性 */
2
3 //方法1:直接修改sb對象. 不會自動觸發監聽函數
4 scope.sb.name = 'Tonny';
5
6 //方法2:使用scope的$apply方法,在數據修改後會自動觸發監聽函數
7 scope.$apply("sb.name = 'Tonny'");
8
9 //方法3:直接修改sb對象,然後調用$apply方法來傳播變化。
10 scope.sb.name = 'Tonny';
11 scope.$apply("");
在有些情況下,AngularJS會自動調用$apply方法,比如在初次編譯的時候。但無論哪種情況,希望瞭解,對數據的變化監聽,總是需要通過$apply方法的調用而被激活,如果AngularJS沒有獲得一個機會來調用$apply,就需要你手工的調用它。
1 <body ng-app="ezstuff" ng-init="sb = {name:'somebody',gender:'male',age:28}">
2 <ez-namecard data="sb"></ez-namecard>
3 <ez-namecard-editor data="sb"></ez-namecard-editor>
4 </body>
1 angular.module("ezstuff",[])
2 .directive("ezNamecard",function($rootScope){
3 return {
4 restrict : "E",
5 template : "<div></div>",
6 link : function(scope,element,attrs){
7 element.append("<div>name:<span class='name'></span></div>")
8 .append("<div>gender:<span field='gender'></span></div>")
9 .append("<div>age:<span field='age'></span></div>")
10 scope.$watch(attrs.data,function(nv,ov){
11 var fields = element.find("span");
12 fields[0].innerText = nv.name;
13 fields[1].innerText = nv.gender;
14 fields[2].innerText = nv.age;
15 },true)
16 }
17 };
18 })
19 .directive("ezNamecardEditor",function($rootScope){
20 return {
21 restrict : "E",
22 template : "<div></div>",
23 link : function(scope,element,attrs){
24 var model = attrs.data;
25 element.append("<div>name:<input type='text' field='name'></div>")
26 .append("<div>gender:<input type='text' field='gender'></div>")
27 .append("<div>age:<input type='text' field='age'></div>");
28 element.find("input").on("keyup",function(ev){
29 var field = ev.target.getAttribute("field");
30 scope[model][field] = ev.target.value;
31 scope.$apply("")
32 })
33 }
34 };
35 });
五、數據和模板解耦
在AngularJS中,界面是運轉起來的聲明式模板,稱爲視圖/View,真實的數據模型/Model通常不完全適用於某個特定應用場景的視圖,所以用於視圖綁定的模型,稱爲視圖模型/ViewModel,這些結合起來,被稱爲M.V.VM,即MVVM模式。
在使用AngularJS進行前端開發,在建立聲明式模式之後,在作用域上定義適應模板的數據模型,然後,指令的解釋器會把數據和模板關聯起來,這被稱爲數據綁定。
數據綁定包含兩個方向:
- 數據的變化會自動更新界面
- 界面的交互會自動更新數據
方式三:用依賴注入解耦
什麼是注入器/injector
AngularJS提供了一些功能的封裝,但是當你試圖通過全局對象angular去訪問這些功能時,卻發現與以往遇到的庫大不相同。
比如,在jQuery中,我們知道它的API通過一個全局對象:$暴露出來,當你需要進行ajax調用時,使用$.ajax()就可以了。API很符合思維的預期。
AngularJS也暴露了一個全局對象:angular,也對ajax調用進行封裝提供了一個$http對象,但是當你試圖沿用舊經驗訪問angular.$http時,發現在不是那麼回事。
仔細地查閱$http的文檔,也找不到一點點的線索,從哪裏可以把這個$http拿到。
事實上,AngularJS把所有的功能組件都以依賴注入的方式組織起來,這導致了你必須通過一箇中介才能獲得某個組件的實例對象:
1 var injector = angular.injector(['ng']);
2 injector.invoke(function($http){
3 // do sth. with $http
4 });
這個中介,就是依賴注入模式中的容器,在AngularJS中,被稱爲注入器。
注入器是AngularJS框架的關鍵,這是一個DI/IoC容器的實現。
DI/IoC:依賴注入和控制反轉,框架都會有這個特性。
AngularJS將功能分成了不同類型的組件分別實現,這些組件有一個統稱:供給者/provider。組件之間不可以互相直接調用,一個組件必須通過注入器纔可以調用另一個組件。這樣的好處是組件之間相互解耦,髒活留給注入器。
注入器實現了兩個重要的功能:
- 集中存儲所有provider的配方(名稱+實例化方法),就是說,它知道整個系統都有哪些功能組件。
- 按需要提供功能組件的實例。
一、註冊服務組件
在AngularJS中,從injector的角度看,組件就是一個功能提供者,因此被稱爲供給者/Provider。
很顯然地,每個組件需要在injector中註冊自己,這需要兩部分信息:
- 標識符:用來區別自己
- 創建方法:用來告訴injector如何實例化自己
標識符通常使用一個字符串標識,比如“$http”代表http調用服務、“$scope”代表作用域對象、“$compile”代表編譯服務...
創建方法通常是一個具有指定接口的構造函數,injector通過調用該函數,就可以實例化組件。
標識符和創建方法的組合信息,被稱爲配方。injector中將維護一些集中的配方庫,用來按需創建不同的組件。
二、獲得注入器對象
要使用AngularJS的功能,必須首先獲取注入器。有兩種方法取得注入器。
- 創建一個新的注入器
可以使用angular.injector()創建一個新的注入器。
- 使用已經創建的注入器
使用angular.element().injector()獲得已經創建的注入器。
三、調用AngularJS的API
使用注入器的invoke()方法,可以直接調用一個用戶自定義的函數體,並通過函數參數注入進來的對象:
1 angular.injector(['ng'])
2 .invoke(function($http){
3 // do sth. with $http
4 });
也可以使用注入器的get()方法,獲得指定名稱的服務實例:
1 var my$http = angular.injector(['ng']).get('$http'); 2 // do sth. with my$http
四、怎麼確定注入什麼?
當採用invoke方式調用組件服務時,AngularJS通過檢查函數的參數名確定需要注入什麼對象,比如:
1 angular.injector(['ng'])
2 .invoke(function($http){
3 // do sth. with $http
4 });
AngularJS執行invoke時,檢查函數的參數表,發現並注入$http對象。
這樣有一個問題,就是當我們對JavaScript代碼進行壓縮處理時,$http可能會被變更成其他名稱,這將導致注入失敗。
五、顯示指定注入項
AngularJS採用依賴項數數組方法解決代碼壓縮混淆注入的問題:
1 angular.injector(['ng'])
2 .invoke(["$http","$compile",function($http,$compile){
3 // do sth. with $http,$compile
4 }]);
這是傳入invoke的是一個數組,數組的最後一項是實際要執行的函數,數組的其他項 則指明需要向該函數注入的對象。invoke將按照數組中的順序,依次向函數注入依賴對象。也就是說上面代碼中綠色部分$http,$compile是可以爲任意名稱的。
實際項目中怎麼使用注入
$injector是基礎,在實際項目中直接使用$injector是很少見的。然而,在angularJS中有三種注入方式:推斷式注入,標註式注入,內聯式注入。
下面分別舉個例子說明一下三種注入方式的使用規則:
推斷式注入:
1 var myModule = angular.module(“MyModule”,[]);
2 var MyCtrl = function($scope){
3 $scope.gameName = “wpzheng”;
4 }
5 myModule.controller(“MyCtrl”,MyCtrl);
這種注入方式無需寫注入對象,有一個不好的地方是函數參數名稱必須和被注入的對象相同,這樣如果發佈後是壓縮代碼的話就可能出現在錯誤。
標註式注入:
1 var myModule = angular.module(“MyModule”,[]);
2 var MyCtrl = function(thisIsMyName){
3 thisIsMyName.gameName = “wpzheng”;
4 }
5 MyCtrl.$inject = [‘$scope’];
6
7 myModule.controller(‘MyCtrl’,MyCtrl);
標註式注入也稱聲明式注入,這種注入方式是需要寫注入對象(如:$scope),而函數參數名稱可以與被注入的對象名稱不同,這樣話就解決了壓縮後代碼所帶來的錯誤。
內聯式注入:
1 var myModule = angular.module(“MyModule”,[]);
2 myModule.controller(‘MyCtrl’,[‘$scope’,
3 function($scope1){
4 $scope1.gameName = “wpzheng”
5 }
6 ])
這種注入方式是我們最常用的方式,因爲這無需單獨使用$inject來注入名稱,哦,對了,不是不需要使用inject,而是controller調用的時候已經自動使用inject幫我們注入了對應的服務。而且函數名稱可以和被注入的對象名稱不同。最主要的是書寫方便,看着也爽。
小結
好了,寫到這個地方,這裏暫且對上面的內容做個小結,這樣更有意於後面的內容能夠連接上。以上總的來說就闡述了一個問題:AngularJS的開發方式。以一個時鐘實現爲測試用例闡述的流程是:先說明jQuery是怎麼實現這個例子中的功能的,通過對比,引出AngularJS的三個編程方式。
方式一:聲明式界面開發:用AngularJS模板方式將時鐘例子從新實現一次,用此詮釋了三個問題,1、AngularJS中是如果利用指令等構造聲明式模板的。2、AngularJS模板中指令的執行過程以及AngularJS基礎框架是如何編譯模板的。3、較jQuery,在代碼量並沒有減少的情況下使用AngularJS有什麼好處。
方式二:數據的雙向綁定:在AngularJS中每一個DOM對象都擁有一個自己的Scope對象,而ng-app啓動指令對應DOM的是對象是$rootScope。但是要注意兩點,1、在默認情況下,一個DOM子元素不會創建新的作用域,這個子元素所對應的scope對象,其實就是它的最近一級的祖先對象對應的scope對象。2、有些指令會導致創建新的作用域,像ng-controller(控制器),如果在一個DOM對象上創建了新的作用域,那麼這個scope對象的原型是其最近一級的組件對象。然後分別以實例的方式講了怎麼實現數據到界面和界面到數據的原理,即雙向數據綁定,這樣就達到了數據和模板解耦的好處。
方式三:用依賴注入解耦:注入器負責從我們創建的服務中創建注入的實例。可以使用注入器的invoke()方法將服務注入到函數中。同樣也可以通過調用get函數來獲得任何一個已經被定義過的服務的實例。同時,AngularJS採用依賴項數數組方法解決代碼壓縮後變更注入名所引起錯誤的問題。其目的是爲了讓組件之間進行一個解耦。最後還列出了AngularJS的三種注入方式。而我們在項目中最常用的方式是內聯注入。
第二部分:AngularJS啓動和編譯
一、AngularJS簡介
AngularJS是Google開源的一款JavaScript MVC框架,彌補了HTML的構建應用方面的不足,其通過使用指令(directives)結構來擴展HTML詞彙,使開發者可以使用HTML來聲明動態內容,從而使得Web開發和測試工作變得更加容易。
和jQuery不同,AngularJS是一個框架。jQuery是一個庫,庫總是被動的,就像工具,應用的開發邏輯是你的,在某一點上需要用一下工具,就用好了。
框架則非常不同,這意味着AngularJS爲應用已經搭起了一個架子,約定了一些組成部分,並且實現了這些部分的拼裝運行。換句話說,應用的開發邏輯是AngularJS的,你得跟着它走。
所以,AngularJS難學一些,因爲它有一個架子在那,你不瞭解這個架子,基本沒法下手。
二、AngularJS啓動方式
明白AngularJS框架的啓動方式更有易於有整體上理解它的運行機制。AngularJS總共有三種啓動方式:
方式一:自動啓動
Angular什麼自動的找到ng-app,將它作爲啓動點,自動啓動。
1 <html ng-app=”myModule”>
2
3 <head>
4 <title>New Page</title>
5 <meta charset="utf-8" />
6 <script type="text/javascript" src="angular/angular.min.js"></script>
7 </head>
8
9 <body>
10 <div ng-controller="MyCtrl">
11 <span>{{Name}}</span>
12 </div>
13 </body>
14 </html>
1 var myModule = angular.module("myModule", []);
2 myModule.controller('MyCtrl', ['$scope',
3 function($scope) {
4 $scope.Name = "Puppet";
5 }
6 ]);
方式二:手動啓動
在沒有ng-app情況下,只需要在JS中添加一段註冊代碼即可
1 <body>
2 <div ng-controller="MyCtrl">
3 <span>{{Name}}</span>
4 </div>
5 </body>
1 var myModule = angular.module("myModule", []);
2 myModule.controller('MyCtrl', ['$scope',
3 function($scope) {
4 $scope.Name = "Puppet";
5 }
6 ]);
1 /**
2 * 這裏要用ready函數等待文檔初始化完成
3 */
4 angular.element(document).ready(function() {
5 angular.bootstrap(document, ['myModule']);
6 });
方式三:多個ng-app
在ng中,Angular的ng-app是無法嵌套使用的,在不嵌套的情況下有多個ng-app,他默認只會啓動第一個ng-app,第二個第三個需要手動啓動(注意:不要手動啓動第一個,雖然可以運行,但會拋異常)
1 <body>
2 <div id="app1" ng-app="myModule1">
3 <div ng-controller="MyCtrl">
4 <span>{{Name}}</span>
5 </div>
6 </div>
7 <div id="app2" ng-app="myModule2">
8 <div ng-controller="MyCtrl">
9 <span>{{Name}}</span>
10 </div>
11 </div>
12 </body>
1 var myModule1 = angular.module("myModule1", []);
2 myModule1.controller('MyCtrl', ['$scope',
3 function($scope) {
4 $scope.Name = "Puppet";
5 }
6 ]);
7
8 var myModule2 = angular.module("myModule2", []);
9 myModule2.controller('MyCtrl', ['$scope',
10 function($scope) {
11 $scope.Name = "Vincent";
12 }
13 ]);
14 angular.element(document).ready(function() {
15 angular.bootstrap(app2, ['myModule2']);
16 });
三、AngularJS指令編譯
指令/directive:籠統地說,指令是DOM元素(例如屬性、元素、CSS類等)上的標識符,用來告訴AngularJS的HTML編譯器($compile服務)將特定的行爲綁定到DOM元素,或者改變DOM元素及其子元素。
指令本質上就是一個函數,當編譯器在DOM中匹配它時,AngularJS將執行這個函數。
指令運行的三個階段:
加載階段:加載angular.js,找到ng-app指令,確定應用的邊界。
編譯階段:遍歷DOM,找到所有的指令緩存到它的內部的一個緩存中,然後在根據指令代碼中的template,replace,transclude轉換DOM結構。
鏈接階段:對每一個指令運行link函數。
小結
通過和jQuery庫進行一個對比,說明要先對AngularJS框架了解怎麼使用後才能應用它。然後列出了Angular的三種啓動,而我們最常用的應該只有自啓動一種方式。最後說明了一下指令在Angular中是怎麼被編譯的。從而也就知道了指令的運行機制。
第三部分:AngularJS開發利器【控制器、服務器、指令、過濾器】
簡介:AngularJS中控制器一旦定義,就意味着一個新的scope對象建立了,如何在這個scope對象上定義屬性和方法以及控制器應該儘量避免做哪些行爲。
一、使用控制器封裝業務邏輯
理解控制器
我們知道,在AngularJS中,實現數據綁定的核心是scope對象。那麼控制器又有什麼用呢?
簡單地說,沒有控制器,我們就沒法定義業務模型。
回憶下ng-init指令,我們可以使用ng-init指令在scope對象上定義數據,比如:
1 <div ng-init="sb={name:'somebody',gender:'male',age:28}">
2 </div>
但是,ng-init的值是一個AngularJS表達式,在這個表達式裏,沒有辦法定義方法。
控制器/controller讓我們有機會在scope上定義我們的業務邏輯,具體說,可以使用控制器:
- 對scope對象上的數據模型進行初始化
- 向scope對象上的數據橫型添加方法
在一HTML元素上使用ng-controller指令,就可以引入一個控制器對象:
1 <div ng-controller=“myController">...</div>
名爲myController控制器實際上就是一個JavaScript的構造函數:
1 // 控制器類定義
2 var myControllerClass = function($scope){
3 // 模型屬性定義
4 $scope.text = "...";
5 // 模型方法定義
6 $scope.do = function(){...};
7 };
8 // 在模塊中註冊控制器
9 angular.module('someModule',[])
10 .controller(“myController",myControllerClass);
控制器構造函數僅在AngularJS對HTML文檔進行編譯時被執行一次,從這個角度看,就更容易理解爲何將控制器稱爲對scope對象的增強:一旦控制器創建完畢,就意味着scope對象上的業務模型構造完畢,以後不在需要控制器了,然而,scope對象接管了一切。
控制器對scope的影響
ng-controller指令將在該DOM對象上創建一個新的scope對象,這個scope對象的原型就是父scope,在下圖中可以看到:
1、ng-app指令導致$rootScope對象的創建,開始時,它是一個空對象。
2、body元素對應的scope對象還是$rootscope,此時,通過ng-init指令,將SB對象掛在了$rootScope上。
3、div元素通過ng-controller指令定義了一個新的scope對象,這個對象的原型是$rootScope,在DO函數中對SB的引用是有效的,在當前SCOPE中沒有SB的定義,因此這個引用指向原型對象的SB,最終$rootScope上的sb得到了修改。
初始化$scope對象
通常在應用啓動時,需要初始化scope對象上的數據模型。我們之前曾使用ng-init指令進行初始化,而使用控制器則是更爲規範的做法,下面定義了一個ezController,利用這個控制器,我們對業務模型進行了初始化賦值。
1 <div ng-controller="ezController">
2 <div>name : {{vm.sb.name}}</div>
3 <div>gender : {{vm.sb.gender}}</div>
4 <div>age : {{vm.sb.age}}</div>
5 <div>career : {{vm.sb.career}}</div>
6 <div><img ng-src="{{vm.sb.photo}}"></div>
7 </div>
1 var ezControllerClass = function($scope){
2 $scope.vm = {
3 sb : {
4 name : "Jason Stantham",
5 gender : "male",
6 age : 48,
7 career : "actor",
8 photo : "http://5c9eae4c45250d739b6003af3b34a.jpg"
9 }
10 };
11 };
12 angular.module("ezstuff",[])
13 .controller("ezController",ezControllerClass);
請注意,控制器僅僅負責在編譯時在scope對象上建立視圖對象vm,視圖對象和模板的綁定則是由scope負責管理的。
向scope對象添加方法
業務模型是動態的,在數據之外,我們需要給業務模型添加動作。
在之前建立的業務模型上,我們增加一個隨機挑選的方法;shuffle,這個方法負責從一個小型的名人庫中隨機的選擇一個名人來更新模型的sb屬性:
通過在button上使用ng-click指令,我們將模型的shuffle方法綁定到了鼠標點擊事件上。然而,從運行結果來看,有同學可以就會想問,爲什麼我點擊後,綁定在$scope上的變量值變化了,界面上顯示的值也就會根着變化?回答這個問題其實很簡單,Angular實現雙向數據綁定,使用的是髒值檢查,你只要弄清楚髒值策略就可以。一個網上搜一下就有一大把。呵呵!!!
1 $scope.vm = {
2 sb : {
3 name : "Jason Stantham",
4 gender : "male",
5 age : 48,
6 career : "actor",
7 photo : "http://af3b34a.jpg"
8 },
9 shuffle : function(){
10 var repo = [
11 {name:,gender:"male",age:48,career:"actor",photo:""},
12 {name:"Jessica Alba",gender:"female",age:32,career:"actress",photo:""},
13 {name:"Nicolas Cage",gender:"male",age:53,career:"actor",photo:""},
14 {name:"ff",gender:"male",age:48,career:"independent",photo:""},
15 {name:"Sheetal Sheth",gender:"female",age:36,career:"actress",photo:""},
16 {name:"Barack Obama",gender:"male",age:58,career:"president",photo:""},
17 {name:"ww",gender:"male",age:63,career:"president",photo:""}
18 ];
19 var idx = Math.floor(Math.random()*repo.length);
20 $scope.vm.sb = repo[idx];
21 }
22 };
一些應避免的行爲
控制器的設計出發點是封裝單個視圖的業務邏輯,因此,不要進行以下操作:
1、DOM操作
應當將DOM操作使用指令directive進行封裝。
2、變換輸出形式
應當使用過濾器/filter對輸出顯示進行轉化。
3、跨控制器共享代碼
對於需要複用的基礎代碼,應當使用服務/service進行封裝。
二、使用服務器封裝可複用的代碼
簡介:詳細總結了AngularJS定義一個服務的三個方法:provider、factory、service。基礎方法是provider。
創建服務組件
AngularJS中創建一個服務組件很簡單,只需要定義一個具有$get方法的構造函數,然後使用模塊的provider方法進行登記。
1 // 定義構造函數
2 var myServiceProvider = function(){
3 this.$get = function(){
4 return ....
5 };
6 };
7 // 在模塊中登記
8 angular.module("myModule",[])
9 .provider("myService",myServiceProvider);
舉個例子:
1 <button onclick="doCalc();">3+4=?</button>
2 <div id="result"></div>
1 function doCalc(){
2 var injector = angular.injector(["ezstuff"]),
3 mycalculator = injector.get("ezCalculator"),
4 ret = mycalculator.add(3,4);
5
6 document.querySelector("#result").innerText = ret;
7 }
8
9 angular.module("ezstuff",[])
10 .provider("ezCalculator",function(){
11 this.$get = function(){
12 return {
13 add : function(a,b){return a+b;},
14 subtract : function(a,b){return a-b;},
15 multiply : function(a,b){return a*b;},
16 divide: function(a,b){return a/b;}
17 }
18 };
19 })
服務的定義方式
使用provider方法定義服務組件,在有些場景下顯示得有些笨重。AngularJS友好地提供了一些簡化的定義方法,這些方法只是對provider方法的封裝,分別適用於不同的場景。
包括factory方法和service方法
1、factory方法
factory方法要求提供一個類工廠,調用該工廠將返回服務實例。
1 var myServiceFactory = function(){
2 return ...
3 };
4 angular.module("myModule",[])
5 .factory("myService",myServiceFactory);
改寫一下上面的例子
1 function doCalc(){
2 var injector = angular.injector(["ezstuff"]),
3 mycalculator = injector.get("ezCalculator"),
4 ret = mycalculator.add(3,4);
5
6 document.querySelector("#result").innerText = ret;
7 }
8
9 angular.module("ezstuff",[])
10 .factory("ezCalculator",function(){
11 return {
12 add : function(a,b){return a+b;},
13 subtract : function(a,b){return a-b;},
14 multiply : function(a,b){return a*b;},
15 divide: function(a,b){return a/b;}
16 }
17 })
2、service方法
service方法要求提供一個構造函數,AngularJS使用這個構造函數創建服務實例。
1 var myServiceClass = function(){
2 this.method1 = function(){...}
3 };
4 angular.module("myModule",[])
5 .service("myService",myServiceClass);
在改寫一下上面的例子
1 function doCalc(){
2 var injector = angular.injector(["ezstuff"]),
3 mycalculator = injector.get("ezCalculator"),
4 ret = mycalculator.add(3,4);
5
6 document.querySelector("#result").innerText = ret;
7 }
8
9 angular.module("ezstuff",[])
10 .service("ezCalculator",function(){
11 this.method1 = {
12 add : function(a,b){return a+b;},
13 subtract : function(a,b){return a-b;},
14 multiply : function(a,b){return a*b;},
15 divide: function(a,b){return a/b;}
16 }
17 this.getMethod = function(){
18 return this.method1;
19 }
20 })
好了,Angular中其實還有兩個也比較常用的服務,value和constant,它們是用來定義一個常量的,也可以說是全局變量。定義模式非常簡單,如果展開總結的話,應該叫angularJS如何定義全局變量。呵呵。關於這個回頭我在單獨寫個總結。
三、使用指令封裝DOM操作
簡介:AngularJS中指令分爲內置指令和自定義指令,下面從三個主題總結:1、如何創建自定義指令(這會詳細說明每個屬性的使用)。2、自定義指令中如何操作DOM的。3、介紹一些常用指令的用法。
如何創建指令
指令也是一種服務,只是這種服務的定義有幾個特殊要求
- 必須使用模塊的directive方法定義
- 必須提供factory方法
- factory方法返回的對象必須返回一個指令定義對象
1 // 定義指令的類工廠
2 var directiveFactory = function(injectables){
3 // 指令定義對象
4 var directiveDefinationObject = {
5 ...
6 };
7 return directiveDefinationObject;
8 };
9 // 在模塊上註冊指令
10 angular.module("someModule",[])
11 .directive("directiveName",directiveFactory);
12 var ezHoverableFactory = function(){
13 return {
14 restrict : "A",
15 replace : true,
16 template :"<p>測試內容</p>",
17 link : function(scope,element,attrs){
18 element.on("mouseover", function(){
19 element.css({outline:"#ff0000 dotted thick"});
20 })
21 element.on("mouseout", function(){
22 element.css({outline:"none"});
23 })
24 }
25 };
26 };
27 angular.module("ezstuff",[])
28 .directive("ezHoverable",ezHoverableFactory);
1 <span ez-hoverable>我是SPAN</span>
2 <button ez-hoverable>我是BUTTON</button>
3 <div ez-hoverable>我是DIV</div>
4 <textarea ez-hoverable>我是TEXTAREA</textarea>
5 <ez-hoverable></ez-hoverable>
每個指令定義的工廠函數,需要返回一個指令定義對象,編譯器/$compile在編譯時就根據這個定義對象對指令進行展開。
指令定義對象的常用屬性如下:
1、link&compile
link函數負責實現DOM和scope的數據綁定,通常在link裏執行DOM事件監聽和數據變化監聽。link函數在template執行後被調用。link是最常用的屬性,一個指令的邏輯通常在link函數裏實現。指令的執行可分爲兩個階段compile和link。然而,compile的作用是什麼,它和link函數有什麼區別等一系列問題,我們通過運行兩個實例來總結結論。
link實例:
1 <hello></hello>
1 return{
2 retrict:’E’,
3 template:’<div>hi everyone!</div>’,
4 link:function(scope,element,attrs){
5 element.on(“mouseenter”,function(){
6 console.log(“測試代碼”);
7 })
8 }
9 }
說明:scope:這個指令所處的作用域。element:這指令所替換的當前元素。attrs:可以理解爲是AngularJS提供的一個獲取當前元素屬性的方法。
compile實例:
1 <div alotofhello=“5”>
2 <p>你好,吃飯,再見!</p>
3 </div>
1 return{
2 restrict:’A’,
3 compile: function(element,attrs,transclude){
4 // 這裏開始 對標籤元素自身進行一些變換
5 console.log(“指令編譯…”);
6 var tpl = element.children().clone();
7 console.log(tpl);
8 for(var i= 0;i<attrs.alotofhello-1;i++){
9 element.append(tpl.clone());
10 }
11 // compile必須指定這麼一個返回的函數 綁定鏈接 作爲link函數
12 return function(scope,element,attrs,controller){
13 console.log(“指令鏈接…”);
14 }
15 }
16
17 // 如果同時提供link函數的定義 結果會怎麼呢? 如果註釋compile在運行
18 link:function(){
19 console.log(“我自己的link函數…”);
20 }
21 }
說明:在compile實例中紅色標的代碼不會被運行,因爲它會被compile函數中return返回的link函數替換運行。當compile函數註釋後,link函數又能正常運行。這行代碼的輸出結果是在頁面中輸出5行p元素。由此可以得出結論:
- compile一般很少用,一般不會和link同時用。
- compile函數的作用是對指令的模板進行轉換。
- link函數的作用是在模型和視圖之間建立關聯,包括在元素上註冊事件監聽。
- 一般情況下我們只要編寫link函數就夠了。
2、restrict
可以是EACM這四個字母的做任意組合,用來限定指令的應用場景。如果不指定這個屬性,默認情況下,指令將僅允許被用作元素和屬性名。
- E - 元素名,例如:<my-directive></my-directive>
- A - 屬性名,例如:<div my-directive="exp"></div>
- C - 類,例如:<div class="my-directive: exp;"></div>
- M - 註釋,例如:<!-- directive:my-directive exp -->
也就是說,如果指令定義中restrict:'E',那麼在模板頁中就只能使用<my-customer></my-customer>纔算合法。上面四行代碼就是是例子,這裏就不舉例了。節省篇幅
3、template
template是一個HTML片段,可以用來:
- 替換指令的內容。這是默認的行爲,可以使用replace屬性更改。
- 替換指令本身(如果replace屬性設爲TRUE的話)。
- 包裹指令的內容(如果transclue屬性設爲TRUE的話)。
1 angular.module("ezstuff",[])
2 .controller("ezCtrl",["$scope", function($scope){
3 $scope.customer = {
4 name: "Naomi",
5 address: "1600 Amphitheatre"
6 };
7 }])
8 .directive("ezCustomer", function(){
9 return {
10 restrict:"AE",
11 template:"<div>Name:{{customer.name}}Address:{{customer.address}}</div>",
12 replace:true
13 }
14 })
1 <body ng-app="ezstuff" ng-controller="ezCtrl">
2 <ez-customer></ez-customer>
3 </body>
同等作用的屬性還有templateUrl、$templateCache。templateUrl好理解一點,其意義就是引入一個頁面片,現在舉個$templateCache的實例,主要目的是說明它是怎麼使用的。
1 angular.module("ezstuff",[])
2 .run(function($templateCache){
3 $templateCache.put('hello.html','<div>HI everynone</div>');
4 })
5 .directive("hello",function(){
6 return{
7 restrict:'E',
8 template:$templateCache.get('hello.html'),
9 replace:true
10 }
11 })
這個例子中綠色的hello.html那個位置其實放的是一個函數$templateCache第二個參數的引用。 run方法只會被運行一次。
4、replace
指明是否使用template替換指令元素
- true - 編譯時,將使用template替換指令元素
- false - 編譯時,將使用template替換指令元素的內容
這個屬性是比較好理解的,這裏就不舉實例了。用個例子運行一下,對照一下結果,區別就顯而易見。呵呵 這裏我得偷一下懶,後面難理解的我就弄個例子說一下。
5、transclude
有些指令需要能夠包含其他未知的元素,比如一個對話框,我們不知道會有什麼元素需要放在對話框裏。如果要設計一個對話框指令,我們需要使用transclude。
1 angular.module('docsTransclusionDirective', [])
2 .controller('Controller', ['$scope', function($scope) {
3 $scope.name = 'Tobias';
4 }])
5 .directive('myDialog', function() {
6 return {
7 restrict: 'E',
8 transclude: true,
9 template: '<div ng-transclude=""></div>'
10 };
11 });
1 <body ng-app="docsTransclusionDirective">
2 <div ng-controller="Controller">
3 <my-dialog>Check out the contents, {{name}}!</my-dialog>
4 </div>
5 </body>
6、scope -建立獨立的作用域
默認情況下,指令沒有自己的作用域,當然,可以使用一個控制器構造獨立的作用域,但更好的方法,是讓指令有自己獨立的作用域。通過獨立的作用域的不同綁定,可以實現更具適應性的自定義標籤,藉由不同的綁定規則屬性,從而定義出符合更多應用場景的標籤。關於獨立作用域搞弄清三個問題:
爲何需要獨立作用域
爲了說清楚這個問題,先列出一個實例:
1 <div>
2 <xingoo></xingoo>
3 <xingoo></xingoo>
4 <xingoo></xingoo>
5 </div>
1 .directive("xingoo",function(){
2 return {
3 restrict:'AE',
4 template:'<div><input type="text" ng-model="username"/>{{username}}</div><br>',
5 repalce:true
6 }
7 })
運行的結果如下:
可以看到,在script中,創建了一個指令,該指令實現了一個自定義的標籤。標籤<xingoo></xingoo>的作用域是替換成一個輸入框和一個數據顯示。
然而,當我們自己創建某個指令時,這個指令肯定不可能只使用一次,是要重複多次使用的,有的在一個頁面或者一個控制器內需要使用多次。類似於上面的這種場景,在任何一個輸入框內改變數據,都會導致其他的標籤內的數據一同發生改變,這顯然不是我們想要的。這個時候就需要獨立作用域了。
如何實現獨立作用域
同樣先給一個例子,然後在來分析它的運行結果。
1 .directive("xingoo",function(){
2 return {
3 restrict:'AE',
4 scope:{},
5 template:'<div><input type="text" ng-model="username"/>{{username}}</div><br>',
6 repalce:true
7 }
8 })
運行結果如下:
由此可見,只需要在定義指令時,添加scope:{}這個屬性,就可以使標籤擁有自己的作用域,僅僅是添加這一行代碼而已,就實現了獨立作用域。在進行輸入時,每個模板使用自己的數據,不會相互干擾。
作用域的數據綁定
自定義標籤或者進行擴展時,會有這樣的需求場景,要在標籤中添加一些屬性,實現一些複雜功能。關於這些屬性,獨立作用域是如何做的呢?舉個例子:
1 <xingoo say="name"></xingoo>
2 <xingoo say="name()"></xingoo>
假設傳入的是上面這種,我們如何區分它傳入的到底是變量呢?還是字符串呢?還是方法呢?
因此AngularJS有了三種自定義的作用域綁定方式:
- 基於字符串的綁定:使用@操作符,雙引號內的內容當做字符串進行綁定。
- 基於變量的綁定:使用=操作符,綁定的內容是個變量。
- 基於方法的綁定:使用&操作符,綁定的內容是個方法。
一、舉個字符串綁定的例子:
1 <xingoo say="test string"></xingoo>
2 <xingoo say="{{str2}}"></xingoo>
3 <xingoo say="test()"></xingoo>
1 .controller('Controller', ['$scope', function($scope) {
2 $scope.str1 = "hello";
3 $scope.str2 = "world";
4 $scope.str3 = "angular";
5 }])
6 .directive("xingoo",function(){
7 return {
8 restrict:'AE',
9 scope:{
10 say:'@'
11 },
12 template:"<div>{{say}}</div><br>",
13 repalce:true
14 }
15 })
運行結果如下:
看一下代碼,在body中使用了三次自定義的標籤,每種標籤的內部有一個say的屬性,這個屬性綁定了一個雙引號的字符串。
在指令的定義中,添加了scope:{say:'@'}這個鍵值對屬性,也就是說,angular會識別say所綁定的東西是一個字符串。
在模板中,使用表達式{{say}}輸出say所表示的內容。
在結果中可以看到,雙引號內的內容都被當做了字符串。當然{{str2}}表達式會被解析成對應的內容,再當做字符串。
二、舉個變量綁定的例子:
1 <div ng-controller="Controller">
2 ctrl:<input type="text" ng-model="testname"><br>
3 directive:<xingoo name="testname"></xingoo>
4 </div>
1 .controller('Controller', ['$scope', function($scope) {
2 $scope.testname="my name is wpzheng";
3 }])
4 .directive("xingoo",function(){
5 return {
6 restrict:'AE',
7 scope:{
8 name:'='
9 },
10 template:'<input type="text" ng-model="name">',
11 repalce:true
12 }
13 })
運行結果如下:
這個例子中,在控制器Controller對應的div中,定義了一個變量ng-model-testname,testname對應的是輸入框中輸入的值,然後把這個變量當做一個參數傳遞給xingoo這個標籤的name屬性,在xingoo標籤中,又把這個name綁定到模板中的一個輸入框內。
最終兩個輸入框的內容被連接起來,無論改變哪一個輸入框內的值,testname與name都會發生改變。
總之,在指令定義中通過scope屬性say綁定規則是變量的綁定方式。最終通過xingoo標籤內的屬性依賴關係把testname與name連接在一起。
三、舉個方法綁定的例子:
1 <xingoo say="sayHello(name)"></xingoo>
2 <xingoo say="sayNo(name)"></xingoo>
3 <xingoo say="sayYes(name)"></xingoo>
1 .controller('Controller', ['$scope', function($scope) {
2 $scope.sayHello = function(name){
3 console.log("hello !"+ name);
4 };
5 $scope.sayNo = function(name){
6 console.log("no !"+ name);
7 };
8 $scope.sayYes = function(name){
9 console.log("yes !"+ name);
10 };
11 }])
12 .directive("xingoo",function(){
13 return {
14 restrict:'AE',
15 scope:{
16 say:'&'
17 },
18 template:'<input type="text" ng-model="username"/><br>'+
19 '<button ng-click="say({name:username})">click</button><br>',
20 repalce:true
21 }
22 })
運行結果如下:
這段代碼中scope中的綁定規則變成了&,也就是方法綁定,在body中,通過自定義標籤傳入了三個方法,分別是sayHello(name),sayNo(name),sayYes(name),這三個方法都需要一個name變量。
在指令的定義中,模板替換成一個輸入框,一個按鈕:
輸入框:用於輸入username,也就是三個方法需要的參數name。
按鈕:點擊觸發函數——通過綁定規則,綁定到相應的方法。
也就是說:通過say在scope中的定義,angular知道了say對應的是個方法,通過{name:username}的關聯,知道了傳入的是username。從而交給對應的方法執行。
如何在指令中操作DOM
如果需要在指令中操作DOM,我們可以在選項中使用link,link需要指定一個函數,AngularJS將在編譯時調用 這個函數,並傳遞scope、element和attrs這幾個參數進去。
- scope是scope對象。
- element是jQuery對象。
- attrs是規範化後的屬性名/值哈希表。
1 <div ng-controller="Controller">
2 Date format: <input ng-model="format"> <hr>
3 Current time is: <span my-current-time="format"></span>
4 </div>
1 .controller('Controller', ['$scope', function($scope) {
2 $scope.format = 'M/d/yy h:mm:ss a';
3 }])
4 .directive('myCurrentTime', ['$interval', 'dateFilter', function($interval, dateFilter) {
5 function link(scope, element, attrs) {
6 var format,
7 timeoutId;
8 function updateTime() {
9 element.text(dateFilter(new Date(), format));
10 }
11 scope.$watch(attrs.myCurrentTime, function(value) {
12 format = value;
13 updateTime();
14 });
15 element.on('$destroy', function() {
16 $interval.cancel(timeoutId);
17 });
18 timeoutId = $interval(function() {
19 updateTime();
20 }, 1000);
21 }
22
23 return {
24 link: link
25 };
26 }]);
常用指令用法
一個項目最常用的指令有以下幾個:
- ng-app 初始化一個AngularJS應用程序。
- ng-init 初始化應用程序數據,其實是將數據綁定在其作用域scope上。
- ng-model 把元素值(比如輸入域的值)綁定到應用程序。
- ng-bind 把應用程序數據綁定到HTML視圖。
- ng-repeat 會重複一個HTML元素。
這裏舉一個例子來說明以上指令的作用和區別:
1 <div ng-app = "">
2 <div ng-init = "firstName='John'">
3 <p>姓名爲 <span ng-bind = "firstName"></span></p>
4 <p>在輸入框中嘗試輸入:</p>
5 <p>姓名: <input type="text" ng-model = "name"></p>
6 <p ng-bind = "name"></p>
7 <p>{{name}}</p>
8 </div>
運行結果如下:
當網頁加載完畢,AngularJS自動開啓。ng-app指令告訴AngularJS,這個div元素所包含的內容都歸我管了。
ng-init指令是初始化數據,這個指令在開發中並不很常見,因爲在AngularJS應用中初始化數據一般會放在控制器裏來完成。
ng-model指令把輸入的值綁定到應用程序變量name。ng-bind指令把應用程序變量name綁定到某個段落的innerHTML。這兩個指令的區別其實就是一個數據綁定方向的問題。
{{}}:作爲AngularJS的取值表達式,它把數據綁定到HTML,這與ng-bind指令有異曲同工之妙。在表達式書寫的位置“輸出”數據。然而{{}}和ng-bind在數據顯示上是有一點需要我們考慮的。
因爲瀏覽器需要首先加載index.html頁面,渲染它,然後AngularJS才能把它解析成你期望看到的內容。所以使用表達式時,頁面渲染時可以會被用戶看到表達式。因此對於index.html頁面中的數據綁定操作,建議採用ng-bind。這樣在數據加載完成之前用戶就不會看到任何內容。
四、使用過濾器轉化輸出
簡介:AngularJS中過濾器分爲內置過濾器和自定義過濾器,下面從兩個主題總結:1、如何使用過濾器。2、如何創建過濾器。
如何使用過濾器
一、在視圖模板中使用過濾器
過濾器也是一種服務,負責對輸入的內容進行處理轉換,以便更好的向用戶顯示。
過濾器可以在模板中的{{}}標記中使用:
1 {{ expression | filter:arg1:arg2}}
1、預置的過濾器
AngularJS的ng模塊實現了一些預置的過濾器,如:currency、number等等,可以直接使用。例如下面的示例將對數字12使用currency過濾器,結果將是“$12.00”;
1 {{12|currency}}
tips:AngularJS內置了9個過濾器,分別是currency, date, filter, json, limitTo, uppercase, lowercase, number, orderBy。在文檔中都要詳細的使用說明。
2、帶參數的過濾器
過濾器也可以有參數,例如下面的示例將數字格式化爲“1,234.00”;
1 {{1234|number:2}}
3、過濾器流水線
過濾器可以應用於另一個過濾器的輸出,可稱之爲鏈式調用,通俗點講就是Filter疊加--前一filter的輸出結果作爲後一filter的輸入數據源。語法如下:
1 {{expression|filter1|filter2|…}}
二、在代碼中使用過濾器
別忘了過濾器也是一種服務,所以你可以將它注入你的代碼中。
和普通的服務不同,過濾器在注入器中註冊時,名稱被加了一個後綴:Filter。例如,number過濾器的服務名稱是:numberFilter,而currency過濾器的服務名稱是:currencyFilter。
通常我們的代碼被封裝在三個地方:控制器、服務器、指令。這些地方都支持服務的直接注入,例如:
1 angular.module('myModule',[])
2 .controller(function($scope,numberFilter){
3 //...
4 })
有時你需要顯式的通過注入器調用過濾器,那麼使用注入器的invoke方法:
1 angular.injector(['ng'])
2 .invoke(function(numberFilter){
3 //...
4 })
總之,記住過濾器是一種服務,除了名字需要追加Filter後綴,和其他服務的調用方法沒什麼區別。
如何創建過濾器
一、如何創建過濾器
過濾器也是一種特殊的服務,與創建一個普通的服務相比較:
1、必須使用模塊的filter()接口定義
2、必須提供factory方法
3、factory方法必須返回一個過濾器函數,其第一個參數爲輸入變量
1 // 定義過濾器類工廠
2 var filterFactory = function(){
3 // 定義過濾器函數
4 var filter = function(input){
5 return output
6 }
7 };
8 // 在模塊上註冊過濾器
9 angular.module("someModule",[])
10 .filter(“filterName",filterFactory);
舉個例子:
1 <p>{{text|ezUC}}</p></body>
1 var ezUCFilterFactory = function(){
2 var filter = function(input){
3 return input.toUpperCase();
4 }
5 return filter;
6 };
7 angular.module("ezstuff",[])
8 .filter("ezUC",ezUCFilterFactory);
二、爲過濾器增加參數
過濾器的行爲可以通過額外的參數來調整。比如,我們希望改進上面的示例,使其可以支持僅大寫每個單詞的首字母。
實現階段:通過在過濾器類工廠返回的過濾器函數中傳入額外的參數,就可以實現這個功能。
1 var filter = function(input,argument1,argument2){…}
調用階段:在使用過濾器時,額外的參數通過前綴:引入,比如
1 {{expression|filter:argument1:argument2}}
改寫例子如下:
1 <body ng-init="text='just a demo!'">
2 <p>{{text|ezUC:true}}</p>
3 </body>
1 var ezUCFilterFactory = function(){
2 return function(input){
3 var output = input.replace(/\b\w+\b/g, function(word) {
4 return word.substring(0,1).toUpperCase( ) +word.substring(1);
5 });
6 return output;
7 }
8 };
9 angular.module("ezstuff",[])
10 .filter("ezUC",ezUCFilterFactory);
小結
這部分包含的是AngularJS的主要內容,控制器、服務器、指令和過濾器。以它們在構建應用中的作用爲主體,分別做了詳細的說明。
1、使用控制器封裝業務邏輯:如何定義一個控制器以及如何在控制器上定義變量和方法。定義了一個控制器就意味着建立了一個新的scope作用域。因爲控制器的設計出發點是封裝單個視圖的業務邏輯,所以有一些行爲不應該用它來完成,比如DOM操作、變換輸出形式、跨控制器共享代碼。
2、使用服務器封裝可複用的代碼:服務器是用來封裝可複用代碼的,可以用provider定義一個服務器,同時AngularJS提供了一另外兩個定義服務的簡單方式,分別爲factory函數和service函數。只它們定義方式稍有區別。
3、使用指令封裝DOM操作:AngularJS如何創建一個指令,逐一介紹了自定義指令的每個屬性的意義和使用,有link、restrict、transclude、replace、template(templateUrl、$templateCache)、compile、還獨立作用域scope等。列舉了指令是如何操作DOM的以及幾個常用指令的用法。
4、使用過濾器轉化輸出:過濾器有內置封裝好的,有用戶自定義的。過濾器的使用又可分爲在視圖模板中使用和在代碼中使用。最後又總結了一下如何自定義指令和如何使用自定義指令。
最後,這篇文章總算寫完了,希望能給正在接觸AngualrJS框架的前端人們帶來參考。也作爲自己後期使用和再次更新的基礎。有講的不對的內容歡迎指正,共同進步。
參考資料
http://www.w3cschool.cc/angularjs/angularjs-intro.html(w3c)
http://docs.angularjs.cn/api(英文文檔)