一、前言
前端技術的發展是如此之快,各種優秀技術、優秀框架的出現簡直讓人目不暇接,作為一名業界新秀,緊跟時代潮流,學習掌握新知識自然是不敢怠慢。當聽到AngularJs這個名字並知道是google在維護它時,便一直在關注,看到其在國外已經十分火熱,可是國內的使用情況卻有不小的差距,參考文獻/網絡文章也很匱乏。朝思暮想良久,決定深入學習angular,並寫系列博客,一方面作為自己學習路程上的記錄,另一方面也給有興趣的同學一些參考。
首先我自己是一名學習者,會以學習者的角度來整理我的行文思路,故該系列博客也不能叫做教程,只能是些探索,有理解或是技術上的錯誤還請大家指出。其次我特別喜歡編寫小例子來把一件事情說明白,故在文中會盡可能多的用示例加代碼講解,我相信這會是一鍾比較好的方式。最后,我深知在現有條件下對於angular的學習會困難重重,不過我更相信堅持的力量,所以謹以此文作為日后學習的動力,讓我們一起來走進angular的世界吧~
二、AngularJs是什么
這個定義一定要定准了,AngularJs(后面就簡稱ng了)是一個用於設計動態web應用的結構框架。首先,它是一個框架,不是類庫,是像backbone一樣提供一整套方案用於設計web應用。它不僅僅是一個javascript框架,因為它的核心其實是對HTML標簽的增強,有圖有真相,請看官網描述:
何為HTML標簽增強?其實就是使你能夠用標簽完成一部分頁面邏輯,具體方式就是通過自定義標簽、自定義屬性等,這些HTML原生沒有的標簽/屬性在ng中有一個名字:指令(directive)。后面會詳細介紹。那么,什么又是動態web應用呢?與傳統web系統相區別,web應用能為用戶提供豐富的操作,能夠隨用戶操作不斷更新視圖而不進行url跳轉。ng官方也聲明它更適用於開發CRUD應用,即數據操作比較多的應用,而非是游戲或圖像處理類應用。
為了實現這些,ng引入了一些非常棒的特性,包括模板機制、數據綁定、模塊、指令、依賴注入、路由。通過數據與模板的綁定,能夠讓我們擺脫繁瑣的DOM操作,而將注意力集中在業務邏輯上。這些我將在以后的學習中一一研究。
另外一個疑問,ng是MVC框架嗎?還是MVVM框架?官網有提到ng的設計采用了MVC的基本思想,而又不完全是MVC,因為在書寫代碼時我們確實是在用ng-controller這個指令(起碼從名字上看,是MVC吧),但這個controller處理的業務基本上都是與view進行交互,這么看來又很接近MVVM。讓我們把目光移到官網那個非醒目的title上:“AngularJS — Superheroic JavaScript MVW Framework”。
好吧,MVW。W—whatever。隨便是MV什么好了,所以也有人寫為了MV*。其實糾結這個也真沒必要,等今后對整個框架熟悉了,其中結構自然了然於心。
三、開始運行angular
有了一個大概的朦朧的了解就夠了,我相信很多概念在使用的過程中會慢慢清晰。我迫不及待的想要讓angular運行起來了。動手~
首先從官網http://angularjs.org/下載angular.js,引入你的頁面中,然后我們使用最簡單的手工啟動方式,直接調用bootstrap方法。所有的代碼如下:
<!DOCTYPE html> <html > <head> <meta charset="utf-8" /> <title>運行ng</title> <script type="text/javascript" src="../angular.js"></script> </head> <body> <div> 1+1={{1+1}} </div> <script> angular.bootstrap(document,[]); </script> </body> </html>
只有一行代碼。調用bootstrap方法傳入作用域和初始化的模塊數組(此處為空)。是不是很簡單。你很快會看到一處比較特別的,就是這里:
<div> 1+1={{1+1}} </div>
如果你使用過其他模板庫,應該對這種寫法不陌生了,{{}}雙大括號,這是ng的模板中用於書寫表達式的標記,ng成功運行起來后,{{}}內的表達式會生效,即頁面會顯示如下:
為了不讓你把ng看的這么簡單,我必須告訴你一般是不這么啟動的,來看稍微修改以后的代碼:
<!DOCTYPE html> <html ng-app="MyApp"> <head> <meta charset="utf-8" /> <title>運行ng</title> <script type="text/javascript" src="../angular.js"></script> </head> <body> <div> 1+1={{1+1}} </div> <script type="text/javascript" charset="utf-8"> var app = angular.module('MyApp', [], function(){console.log('started')}); </script> </body> </html>
在<html>標簽上多了一個屬性ng-app=”MyApp”,它的作用就是用來指定ng的作用域是在<html>標簽以內部分。在js中,我們調用angular對象的module方法來聲明一個模塊,模塊的名字和ng-app的值對應。關於如何聲明、使用模塊我們在后面會講。現在我們只要明白用這種方式可以優雅的讓ng運行起來就可以了。
四、模板與數據的綁定
首先需要明確一下模板的概念。在我還不知道有模板這個東西的時候,曾經用js拼接出很長的HTML字符串,然后append到頁面中,這種方式想想真是又土又笨。后來又看到可以把HTML代碼包裹在一個<script>標簽中當作模板,然后按需要取來使用。在ng中,模板十分簡單,它就是我們頁面上的HTML代碼,不需要附加任何額外的東西。在模板中可以使用各種指令來增強它的功能,這些指令可以讓你把模板和數據巧妙的綁定起來。
綁定這個東西可是ng中的大功臣了。在我們使用jQuery的時候,代碼中會大量充斥類似這樣的語句:var v = $(‘#id’).val();$(‘#id’).html(str);即頻繁的DOM操作(讀取和寫入),其實我們的最終目的並不是要操作DOM,而是要實現業務邏輯。ng的綁定將讓你擺脫DOM操作,只要模板與數據通過聲明進行了綁定,兩者將隨時保持同步,最新的數據會實時顯示在頁面中,頁面中用戶修改的數據也會實時被記錄在數據模型中。
我構思了一個小例子,本篇文章的示例將圍繞這個小例子來進行。我猜你已經厭倦了登錄模塊或者是購物車示例,我們來點新穎的。我已化身為一名教師,我要用一個web應用來為學生出一份在線試題。首先從一道題開始吧~
<!DOCTYPE html> <html ng-app="MyApp"> <head> <meta charset="utf-8" /> <title>模板數據綁定</title> <script type="text/javascript" src="../angular.js"></script> </head> <body> <div ng-controller="testC"> <h1>{{newtitle}}</h1> 題目:<input type="text" ng-model="name" /><br /> 分數:<input type="text" ng-model="fraction" /><br /> <hr> <h1>{{previewtitle}}</h1> <b>{{name}}</b>({{fraction}}分) </div> <script type="text/javascript" charset="utf-8"> var app = angular.module('MyApp', [], function(){console.log('started')}); var testC = function($scope){ $scope.newtitle = '新建試題'; $scope.previewtitle = '預覽試題'; $scope.name = $scope.fraction = ''; } </script> </body> </html>
頁面上有分別表示題目和分數的兩個輸入框,下面有一個實時預覽區域,用來展示題目的最終輸出效果。ng-controller=”textC”用來聲明一個需要和數據進行綁定的模板區域,它的作用域就是這個div之內的東西,並起名為textC,js代碼中定義了一個名為textC的函數與它對應,我們將在這個函數內完成綁定。函數傳入一個參數$scope,表示這個作用范圍。我們分別為作用范圍內的newtitle、previewtitle、name、fraction四個變量賦值。此時<h1>標簽內的{{}}便能得到相應的值了。
通過{{}}只能完成數據向模板的單向綁定。要想進行雙向綁定,我們需要用到ng-modle這個指令,我們使用它分別為題目和分數進行了雙向綁定,這樣當輸入框內的值發生變化時,函數中的變量也會跟隨變化,它的變化會實時反饋在下方的預覽區域中,因為預覽區域中也有一個name和fraction的綁定。
試試在下面的輸入框中進行編輯吧~
我們並未進行任何DOM操作,框架自動完成了DOM的取值和賦值。在后面我將繼續完善這個例子,並引入更多的新概念。
五、模板中的一些控制方式
我們在使用其他模板庫時,一般都會有模板的循環輸出、分支輸出、邏輯判斷等類似的控制。ng模板中都可以進行哪些控制呢?來一塊探索之。
1.循環輸出
繼續上面的例子。試題光有題目和分數還不夠,我想要出一道選擇題,可以自己添加若干選項並編輯選項內容,代碼該怎么寫呢?先上代碼:
HTML部分:
<div ng-controller="testC"> <h1>{{question.newtitle}}</h1> 題目:<input type="text" ng-model="question.name" /><br /> 分數:<input type="text" number ng-model="question.fraction" /><br /> 選項:<button ng-click="addOption()">增加選項</button><br /> <ul> <li ng-repeat="o in question.options"> <b>{{$index+1}}.</b> <input type="text" ng-model="o.content" value="o.content" /> <a href="javascript:void(0);" ng-click="delOption($index)">刪除</a> </li> </ul> <hr> <div > <h1>{{question.previewtitle}}</h1> <b>{{question.name}}</b>({{question.fraction}}分) <ul> <li ng-repeat="o in question.options"> <b>{{$index+1}}.</b> <input type="radio" name="optcheck" /> {{o.content}} </li> </ul> </div> </div>
js部分:
var app = angular.module('MyApp', [], function(){console.log('started')}); var questionModel = { newtitle : '新建試題', previewtitle : '預覽試題', name : '', fraction : '', options : [] }; app.controller('testC',function($scope){ $scope.question = questionModel; $scope.addOption = function(){ var o = {content:''}; $scope.question.options.push(o); }; $scope.delOption = function(index){ $scope.question.options.splice(index,1); }; });
請注意我的js代碼有了一點變化,在數據綁定時沒有直接寫成字符串,而是將所有的數據寫成了一個questionModel對象,這只是一個普遍的js對象,這樣能夠使我們的數據看起來像是“model”的樣子,畢竟我們是MV*嘛。那么在模板中,我們使用name的地方也改成question.name,其他的值也同理。另外在controller部分,我沒有用var textC = function(){}這樣的寫法,而是改成了app.controller(‘textC’,function(){}),這樣明確指定了這個controller屬於MyApp這個模塊,我們的MV*代碼看起來更專業、更一體了。
在HTML部分我分別在新建區域和預覽區域添加了一個列表,用來放置各選項。並添加了一個“增加選項”按鈕。在<li>標簽上使用了ng-repeat指令來進行循環輸出,<li>標簽將會根據$scopt中的options數組長度被復制多份。在循環范圍內,可以使用$index獲得當前循環的索引,可以認為這是一個公共變量,直接使用。同時,循環輸出的每個<input>元素也使用ng-model與選項的內容進行了雙向綁定。另外還輸出一個刪除鏈接,點擊的時候調用$scope中的delOption方法。
這樣綁定好之后,模版中的列表與數據模型中的options數組便保持了同步,我們在頁面上點擊增加選項,數組中便會增加一個元素,數組中的每個元素也會實時反饋在模板中。所以在js代碼中,我們只須操作options數組就夠了,壓根不需要在頁面上append或removed節點。
你可以在下面點擊增加選項和刪除試試~
2.單個節點的控制
在上面的例子中,你是不是發現了,我在處理按鈕的點擊時,使用了叫做ng-click的指令,為什么不直接用onclick呢?是因為ng根據自己的需要進行了封裝。我們把addOption這個函數定義在了controller范圍之內,用我們常規的onclick已經無法訪問到。換言之,我們頁面上的作用域,ng已經幫我們都規划好了,我們只需按照它提供的方式來使用就夠了。
通過onclick我們可以聯想到,HTML節點還有好多其他屬性,如class、style、href、checked、disabled等等,ng對這些都一一進行了封裝,更厲害的是,除此之外ng還額外提供了許多更加詳細的控制節點的指令。這些指令我以后會詳細研究,在這里,我們先拿個其中一個應用到我們的例子中,看看效果先。
我馬上變回老師身份,嘩~
我新建試題的時候,不要局限於單選題,我想要在單選題和多選題之間能夠切換,同時下方的預覽區域內,選擇框也進行單選框和多選框的切換。上代碼:
HTML部分:
<div ng-controller="testC"> <h1>{{question.newtitle}}</h1> 題目:<input type="text" ng-model="question.name" /><br /> 分數:<input type="text" ng-model="question.fraction" /><br /> 類型:<select ng-model="question.type"><option value="1" selected>單選</option><option value="2">多選</option></select><br /> 選項:<button ng-click="addOption()">增加選項</button><br /> <ul> <li ng-repeat="o in question.options"> <b>{{$index+1}}.</b> <input type="text" ng-model="o.content" value="o.content" /> <a href="javascript:void(0);" ng-click="delOption($index)">刪除</a> </li> </ul> <hr> <div preview-panel> <h1>{{question.previewtitle}}</h1> <b>{{question.name}}</b>({{question.fraction}}分) <ul> <li ng-repeat="o in question.options"> <b>{{$index+1}}.</b> <input type="radio" name="optcheck" ng-show="question.type==1" /> <input type="checkbox" ng-show="question.type==2" /> {{o.content}} </li> </ul> </div> </div>
Js代碼中,我只是在questionModel中新增了一項type:1,表示題的類型,1為單選,2為多選。並給默認值為1.
var questionModel = { newtitle : '新建試題', previewtitle : '預覽試題', name : '', fraction : '', type : '1', options : [] };
在HTML中,我新增了一個下拉框,並與question.type建立雙向綁定。需要關注的是下面的預覽區域。我又添加了一個checkbox控件來為多選題提供選擇框。顯然,radio與checkbox不能同時存在,所以我用ng-show這個指令來控制它們的顯隱,ng-show接收boolean類型的值以及計算結果為boolean類型的表達式。請注意,ng-show以及其他所有指令的值不是簡單的字符串(盡管看上去是那樣),而是字符串表達式,擁有計算能力,我現在的理解是它將來會在框架的某個地方放進eval()或是類似的函數執行。{{}}里的內容也是一樣。
所以在這里我給radio的ng-show賦值為question.type==1,checkbox的ng-show賦值為question.type==2。當試題是單選題時,radio的ng-show便能得到值true,從而顯示出來。在下面看一下效果:
其他的節點控制指令也可以類似這樣使用,思想就是根據你的業務邏輯,賦予它們一定的表達式。其他的控制還有事件綁定、表單控件等等,篇幅的限制在這里就不講了,以后專門開一篇介紹。
3.過濾器(filter)
所謂過濾器是指對輸出的內容進行格式化,如格式化為美元、日期等。框架自己提供一些過濾器,如排序、字符串內容篩選。我們也可以自定義過濾器。
過濾器在{{}}中使用,表達式后用|隔開使用。拿日期過濾器舉例,方式如下:
$scope.nowTime = new Date().valueOf(); {{nowTime | date : 'yyyy-MM-dd HH:mm:ss'}}
便會輸出格式化的日期。是不是很方便呢。
接下來實戰一下,嘩~
接上一步的例子,我想要在預覽區域中的題目前面顯示題型,像[單選題]這樣。我們的questionModel中,type的值是1和2,所以我們要做的就是通過過濾器,把1和2顯示為單選題和多選題。Go~
在js中定義一個名為typeFilter的過濾器:
app.filter('typeFilter',function(){ var f = function(input){ return input == '1' ? '單選題' : '多選題'; } return f; });
filter函數如何使用以及執行細節不是本篇的討論內容,所以現在只要明白這樣可以定義一個過濾器就可以了。結構姑且認為是固定寫法,代碼不難看懂。
定義后這個filter后我們便可以在模板中使用了:
<b>[{{question.type | typeFilter}}]{{question.name}}</b>({{question.fraction}}分)
在題目的前面顯示題型,並使用typeFilter,效果如下:
單調的“1”已經華麗轉身變為了“單選題”。這只是一個簡單的過濾器。你可以發揮想象力編寫更強大的過濾器。
六、指令(directive)
前面已經提到很多次指令了,現在來正式介紹一下它。指令是ng為HTML補充的語法擴展,用於增強HTML的表現力。像我們之前使用的ng-controller、ng-model等都屬於指令。你也可以自定義指令。ng內部包含了一個強大的DOM解析引擎,所以這些新的標簽或是標簽屬性可以像使用原生HTML那樣很好的工作。
聽起來很牛的樣子,那我們來試試自己定義一個指令吧。注意,我要變形了~
我是教師,在新建試題輸入分數的時候應該只能輸入數字才對,輸入其他內容是不合法的,而且我希望這個分數是1~10之間的數字。能否只在輸入框上加一個屬性就完成這個驗證呢?就像使用HTML5新增的required一樣。
我們定義一個叫做fractionNum的指令如下:
app.directive('fractionNum',function(){ return { link : function(scope, elements, attrs, controller){ elements[0].onkeyup = function(){ if(isNaN(this.value) || this.value<1 || this.value>10){ this.style.borderColor = 'red'; } else{ this.style.borderColor = ''; } }; } }; });
哇,代碼好多層級呀,不要慌張,穩住陣腳!其實最后就是返回了帶有link字段的對象,link的值是一個函數,用來定義指令的行為。從傳入的參數中可以獲取到當前元素,我們便可以拿當前元素開刀了。我在此處監聽當前元素的keyup事件,獲取元素的值,如果不是1~10之間的數字,則把輸入框的邊框顏色變為紅色。這下這個指令就可以工作了。
至於傳入的四個參數到底都有什么玄機,我暫時還未研究,也不是本篇的重點,本篇只是做一個概覽,先把ng拉出來溜溜的意思。現在姑且可以認為,一個指令的固定寫法大概就是這個結構。
定義好的指令就可以在模板中使用了,使用方法如下:
分數:<input type="text" ng-model="question.fraction" fraction-num /><br />
把它加在了分數輸入框上,此處要特別小心一個寫法,我定義的時候名字是fractionNum,用在模板中需要寫成fraction-num,就是因為名字中含有大寫字母的原因,感覺上跟使用css屬性名稱有點像。如果定義的時候沒有大寫字母,就不必擔心這一點了。
看效果,現在你可以去下面蹂躪那個分數輸入框去了~
七、依賴注入
通過依賴注入,ng想要推崇一種聲明式的開發方式,即當我們需要使用某一模塊或服務時,不需要關心此模塊內部如何實現,只需聲明一下就可以使用了。在多處使用只需進行多次聲明,大大提高可復用性。
比如我們的controller,在定義的時候用到一個$scope參數。
app.controller('testC',function($scope){});
如果我們在此處還需操作其他的東西,比如與瀏覽器地址欄進行交互。我們只需再多添一個參數$location進去:
app.controller('testC',function($scope,$location){});
這樣便可以通過$location來與地址欄進行交互了,我們僅僅是聲明了一下,所需的其他代碼,框架已經幫我們注入了。我們很明顯的感覺到了這個函數已經不是常規意義上的javascript函數了,在常規的函數中,把形參換一個名字照樣可以運行,但在此處若是把$scope換成別的名字,程序便不能運行了。因為這是已經定義好的服務名稱。
這便是依賴注入機制。順理成章的推斷,我們可以自己定義模塊和服務,然后在需要的地方進行聲明,由框架來替我們注入。
對,我的小例子呢?我現在要想點需求來把依賴注入試驗一下。我覺得試題全是文字太單調了,我希望題目中能含有圖片/音視頻,或者選項中可以含有圖片/音視頻。並且,我要更靈活一點,我要為試題提供若干套模板來選擇,選擇不同的模板可以新建不同樣式的題,比如模板一為純文字試題、模板二為題目中帶圖片/音視頻的試題、模板三為選項中帶圖片/音視頻的試題,等等。注意此處所說的模板跟ng的模板不是一個概念,是我自己試題的模板,不要混淆。這些模板應該是與試題相分離的,以后可以為其他題型例如填空題啊簡答題啊同樣使用。所以試題模板這個東西就需要做成服務了。
不知道我表達清楚了沒有,確實有點繞。來看下我們如何定義一個服務:
app.factory('tpls',function(){ return ['tpl1','tpl2','tpl3','tpl4']; });
看上去相當簡單,是因為我在這里僅僅是直接返回一個數組。在實際應用中,這里應該是需要向服務器發起一個請求,來獲取到這些模板們。服務的定義方式有好幾種,包括使用provider方法、使用factory方法,使用service方法。它們之間的區別暫且不關心。我們現在只要能創建一個服務出來就可以了。我使用了factory方法。一個需要注意的地方是,框架提供的服務名字都是由$開頭的,所以我們自己定義的最好不要用$開頭,防止發生命名沖突。
定義好一個服務后,我們就可以在控制器中聲明使用了,如下:
app.controller('testC',function($scope,tpls){ $scope.question = questionModel; $scope.nowTime = new Date().valueOf(); $scope.templates = tpls; //賦值到$scope中 $scope.addOption = function(){ var o = {content:''}; $scope.question.options.push(o); }; $scope.delOption = function(index){ $scope.question.options.splice(index,1); }; });
此時,若在模板中書寫如下代碼,我們便可以獲取到服務tpls所提供的數據了:
模板:<a href="javascript:void(0);" ng-repeat="t in templates">{{t}} </a><br />
隨着知識點的一點點增加,我的這個小例子也越來越豐滿了,來看看完整版吧:
查看完整代碼請移步到runjs:http://runjs.cn/code/95wlwsfh
八、總結一下
乎~可以松一口氣了。文章寫到這里終於接近尾聲了,不知我上面的陳述能否被大家理解。ng所包含的內容還是挺多的,以上的每個概念都可以再拆出幾篇文章來講解。所以我在這里只能是每一個都點到為止,不揪細節。這篇文章的思想也就是“先了解概念,例子能跑起來就行了”。在以后的文章中會隨着我學習的深入進行探討。