AngularJS自學之路(三)——控制器和運算式,angularjs自學
控制器
控制器在AngularJS中的作用是增強視圖。
AngularJS中的控制器是一個函數,用來向視圖的範圍中添加額外的功能。我們用它來給範圍對象設定初始狀態,並添加自訂行為。
當我們在頁面上建立一個新的控制器時,AngularJS會產生並傳遞一個新的$scope給這個控制器。可以在這個控制器裡初始化scope。由於AngularJS會負責處理控制器的執行個體化過程,我們只需編寫建構函式即可。下面的例子展示了控制器初始化:
function FirstController($scope) {$scope.message = "hello";}
細心的讀者會發現,我們是在全域範圍中建立的這個函數。這樣做並不合適,因為會汙染全域命名空間。更合理的方式是建立一個模組,然後在模組中建立控制器,如下所示:
var app = angular.module('app', []);app.controller('FirstController', function($scope) {$scope.message = "hello";});
只需建立控制器範圍中的函數,就能建立可以在視圖中使用的自訂動作。很幸運,AngularJS允許我們在視圖中像調用普通資料一樣調用$scope上的函數。
用內建指令ng-click可以將按鈕、連結等其他任何DOM元素同點擊事件進行綁定。ng-click指令將瀏覽器中的mouseup事件,同設定在DOM元素上的事件處理常式綁定在一起(例如,當瀏覽器在某個DOM元素上觸發了點擊事件,函數就會被調用)。和前面的例子類似,綁定看起來是這樣的:
<div ng-controller="FirstController"><h4>The simplest adding machine ever</h4><button ng-click="add(1)" class="button">Add</button><a ng-click="subtract(1)" class="button alert">Subtract</a><h4>Current count: {{ counter }}</h4></div>
按鈕和連結都被綁定在了內部$scope的一個操作上,當點擊任何一個元素時AngularJS都會調用相應的方法。注意,當設定調用哪個函數時,會同時用括弧傳遞一個參數(add(1))。
下面給FirstController添加一個操作:
app.controller('FirstController', function($scope) {$scope.counter = 0;$scope.add = function(amount) { $scope.counter += amount; };$scope.subtract = function(amount) { $scope.counter -= amount; };});
控制器可以將與一個獨立視圖相關的商務邏輯封裝在一個獨立的容器中。儘可能地精簡控制器是很好的做法。作為AngularJS開發人員,使用依賴注入來訪問服務可以實現這個目的。
AngularJS同其他JavaScript架構最主要的一個區別就是,控制器並不適合用來執行DOM操作、格式化或資料操作,以及除儲存資料模型之外的狀態維護操作。它只是視圖和$scope之間的橋樑。
控制器嵌套(範圍包含範圍)
預設情況下,AngularJS在當前範圍中無法找到某個屬性時,便會在父級範圍中進行尋找。如果AngularJS找不到對應的屬性,會順著父級範圍一直向上尋找,直到抵達$rootScope為止。如果在rootScope中也找不到,程式會繼續運行,但視圖無法更新。
通過例子來看一下這個行為。建立一個ParentController,其中包含一個user對象,再建立一個ChildController來引用這個對象:
app.controller('ParentController', function($scope) {$scope.person = {greeted: false};});app.controller('ChildController', function($scope) {$scope.sayHello = function() {$scope.person.name = 'Ari Lerner';};});
如果我們將ChildController置於ParentController內部,那ChildController的$scope對象的父級範圍就是ParentController的scope對象。根據原型繼承的機制,我們可以在子範圍中訪問ParentController的scope對象。
例如,我們可以在ChildController的DOM元素中訪問定義在ParentController中的person對象
<div ng-controller="ParentController"><div ng-controller="ChildController"><a ng-click="sayHello()">Say hello</a></div>{{ person }}</div>
我們看到,點擊按鈕時,可以在ChildController中訪問ParentController中$scope.person的值,就好像person對象定義在ChildController的scope中一樣。
控制器應該儘可能保持短小精悍,而在控制器中進行DOM操作和資料操作則是一個不好的實踐。
設計良好的應用會將複雜的邏輯放到指令和服務中。通過使用指令和服務,我們可以將控制器重構成一個輕量且更易維護的形式
簡潔的控制器:
angular.module('myApp', []).controller('MyController', function($scope,UserSrv) {// 內容可以被指令控制$scope.onLogin = function(user) {UserSrv.runLogin(user);};});
運算式
前面已經見過使用運算式的樣本。用{{ }}符號將一個變數綁定到$scope上的寫法本質上就是一個運算式:{{ expression }}。當用watch進行監聽時,AngularJS會對錶達式或函數進行運算。
運算式和eval(javascript)非常相似,但是由於運算式由AngularJS來處理,它們有以下顯著不同的特性:
對錶達式進行的任何操作,都會在其所屬的範圍內部執行,因此可以在運算式內部調用那些限制在此範圍內的變數,並進行迴圈、函數調用、將變數應用到數學運算式中等操作。
解析AngularJS 運算式
儘管AngularJS會在運行$digest迴圈的過程中自動解析運算式,但有時手動解析運算式也是非常有用的。
AngularJS通過$parse這個內部服務來進行運算式的運算,這個服務能夠訪問當前所處的範圍。
將$parse服務注入到控制器中,然後調用它就可以實現手動解析運算式。舉例來說,如果頁面上有一個輸入框綁定到了expr變數上,如下所示:
<div ng-controller="MyController"><input ng-model="expr" type="text" placeholder="Enter an expression" /><h2>{{ parseValue }}</h2></div>
我們可以在MyController中給expr這個運算式設定一個$watch並解析它:
angular.module("myApp", []).controller('MyController',function($scope,$parse) {$scope.$watch('expr', function(newVal, oldVal, scope) {if (newVal !== oldVal) {// 用該運算式設定parseFunvar parseFun = $parse(newVal);// 擷取經過解析後運算式的值$scope.parsedValue = parseFun(scope);}});});
插值字串
在AngularJS中,我們的確有手動運行模板編譯的能力。例如,插值允許基於範圍上的某個條件即時更新文本字串。
要在字串模板中做插值操作,需要在你的對象中注入$interpolate服務。在下面的例子中,我們將會將它注入到一個控制器中:
angular.module('myApp', []).controller('MyController',function($scope, $interpolate) {// 我們同時擁有訪問$scope和$interpolate服務的許可權});
$interpolate服務是一個可以接受三個參數的函數,其中第一個參數是必需的。
$interpolate服務返回一個函數,用來在特定的上下文中運算運算式。
設定好這些參數後,就可以在控制器中進行字元插值的操作了。例如,假設我們希望可以在電子郵件的本文中進行即時編輯,當文本發生變化時進行字元插值操作並將結果展示出來。
<div ng-controller="MyController"><input ng-model="to"type="email"placeholder="Recipient" /><textarea ng-model="emailBody"></textarea><pre>{{ previewText }}</pre></div>
由於控制器內部設定了一個需要每次變化都重新進行字元插值的自訂輸入欄位,因此需要設定一個$watch來監聽資料的變化。
簡而言之,$watch函數會監視scope上的某個屬性。只要屬性發生變化就會調用對應的函數。可以使用watch函數在scope上某個屬性發生變化時直接運行一個自訂函數。
在控制器中,我們設定了$watch來監視郵件內文的變化,並將emailBody屬性的值進行字元插值後的結果賦值給previewText屬性。
angular.module('myApp', []).controller('MyController', function($scope, $interpolate) {// 設定監聽$scope.$watch('emailBody', function(body) {if (body) {var template = $interpolate(body);$scope.previewText =template({to: $scope.to});}};});在
現在,在{{ previewText }}內部的文本中可以將{{ to }}當做一個變數來使用,並對文本的變化進行即時更新。
用startSymbol()方法可以修改標識開始的符號。這個方法接受一個參數。
用endSymbol()方法可以修改標識結束的符號。這個方法也接受一個參數。
如果要修改這兩個符號的設定,需要在建立新模組時將$interpolateProvider注入進去。
angular.module('emailParser', []).config(['$interpolateProvider', function($interpolateProvider) {$interpolateProvider.startSymbol('__');$interpolateProvider.endSymbol('__');}]).factory('EmailParser', ['$interpolate', function($interpolate) {// 處理解析的服務return {parse: function(text, context) {var template = $interpolate(text);return template(context);}};}]);
現在,我們已經建立了一個模組,可以將它注入到應用中,並在郵件內文的文本中運行這個郵件解析器:
angular.module('myApp', ['emailParser']).controller('MyController', ['$scope', 'EmailParser',function($scope, EmailParser) {// 設定監聽$scope.$watch('emailBody', function(body) {if (body) {$scope.previewText = EmailParser.parse(body, {to: $scope.to});}});}]);
現在用自訂的 __ 符號取代預設文法中的 {{ }} 符號來請求插值文本。
由於我們將運算式開始和結束的符號都設定成了__,因此需要將HTML修改成用這個符號取代{{ }}的版本,
<div id="emailEditor"><input ng-model="to"type="email"placeholder="Recipient" /><textarea ng-model="emailBody"></textarea></div><div id="emailPreview"><pre>__ previewText __</pre></div>