MVVM是前端Framework的重要功能,AngularJS當然也有強大好用的資料繫結及模板(Template)機制,才能橫掃江湖。我們就從最簡單的"計算型屬性"開始,順便介紹Angular程式的基本架構。
(註: AngularJS採取的應該是MVC設計模式,但依我的理解,資料繫結、模板、屬性邏輯寫在Scope等特性,與MVVM設計模式沒有分別,因此使用Angular處理網頁呈現時,我仍然會用MVVM的思維看待它。)
拿KO範例1 - 計算型屬性為示範: ViewModel有firstName及lastName兩個屬性,第三個屬性fullName等於fisrtName與lastName相加。這是再平常也不過的應用,對NG來說是小菜一碟: Live Demo
<!DOCTYPEhtml>
<htmlng-app="sampleApp">
<head>
<metacharset="utf-8">
<title>Lab 1 - 計算型屬性</title>
</head>
<body>
<divng-controller="mainCtrl">
<inputtype="text"ng-model="firstName"/>
<inputtype="text"ng-model="lastName"/>
<br/>
<spanng-bind="fullName()"></span>
<br/>
<span>{{ fullName() }}</span>
</div>
<scriptsrc="http://ajax.googleapis.com/ajax/libs/angularjs/1.2.14/angular.js"></script>
<script>
angular.module("sampleApp", [])
.controller("mainCtrl", function($scope) {
$scope.firstName="Jeffrey";
$scope.lastName="Lee";
$scope.fullName = function() {
return $scope.firstName + " " + $scope.lastName;
};
});
</script>
</body>
</html>
程式解析:
- <html>加上ng-app,指明網頁要使用sampleApp這個Module。(依慣例用App做為Module名稱字尾)
- <div>加上ng-controller,指明由mainCtrl這個Controller主控<div>內的MVVM作業。(依慣例取Ctrl字尾)
- <input>加上ng-model="屬性",標明對firstName及lastName屬性做雙向繫結。
- 單向繫結有兩種寫法: 用ng-bind或是直接寫{{ propName }},後者簡潔直覺,可寫成"如果{{fact}}已成事實,{{action}}就是義務"一般文字與繫結內容交雜的字串,但缺點是繫結完成前要防止使用者看到{{、}}等裸碼。
- 網頁要載入angular.js。
NG內建jqLite,摸擬並借用基本的jQuery功能以處理DOM操作,但功能很陽春。如果想要用完整的jQuery功能,可在載入angular前先載入jQuery,jqLite就會換成jQuery。我個人沒有jQuery就混不下去,專案中一定會加,此處不用僅示範NG不依賴jQuery也能運作。 - 程式用angular.module("sampleApp", [])建立並註冊一個新的Module。注意後方的[]空集合表示不需要參照其他Moudle,但不能省略,若寫成.module("sampleApp"),NG會假設sampleApp已存在試圖找尋建好的Instance而出現錯誤。
- .module()方法傳回Module物件,像jQuery一樣,可接著呼叫.controller(名稱, 建構式)註冊Controller。(還可呼叫.service()、.directive()、filter()…等註冊其他項目)
- Controller建構式參數透過DI(依賴注入)方式傳遞,故把用到的項目列上去即可,順序不重要。我們一定會用到的是$scope,它相當於KO的ViewModel角色,MVVM沒有VM玩個屁呢?
註: function($scope) { ... }的寫法在JavaScript壓縮(Minification)過程可能被修改,$scope被換成a, b之類的隨機變數名稱,導致DI機制失效。因此NG提供另一種寫法,.controller("mainCtrl", ["$scope", function($scope) { ... }],如果要進行JS壓縮,記得要用這種寫法。(關於Controller的宣告方式,保哥有篇文章詳解) - $scope.firstName宣告ViewModel有個firstName屬性,事實上不加也成,NG在ng-model看到未宣告屬性,便會自動在$scope新增。(不必宣告,遇缺新增的缺點是打錯字不會報錯而是冒出新屬性,Debug起來比較刺激)
- 在KO要用ko.observable()屬性才可雙向繫結,需用ko.computed()建立計算型屬性。在NG不必這麼麻煩,直接寫成一般JavaScript物件跟屬性處理就可以,所以fullName寫成function() { return this.fisrtName + " " + this.lastName; },在firstName或lastName變動時就會自動更新。(註: 這麼做有其方便之處,但複雜情境不易判斷某項更動會不會影響繫結,與KO的做法也算各有優劣)
就這樣,第一個範例完成了。
實務上,我習慣把真正的ViewModel獨立出來,變成事先定義好的function,如同在KO裡"先宣告function myViewModel() { var self = this; … },再ko.applyBindings(new myViewModel())"一樣。
這麼做有三個目的:
- 將ViewModel類別自$scope抽離,以便用程式產生器依文件或C#類別產生對應的ViewModel,在大型專案中有其必要性
- 中大型專案我傾向用TypeScript開發JavaScript,ViewModel獨立成function或類別後,方便用TypeScript寫成強型別類別或介面。
- $scope.propA在NG中常會因Scope繼承出現非預期結果,寫成$scope.model = { propA: "A" }較不易受影響。(Scope繼承議題挺複雜,先不多提,記得"資料放在$scope.model較不易被繼承給陰了"就好,如果對Scope繼承的雷想多了解一點,可以聽被陰過的索爾同學現身說法...)
因此,我心中理想寫法如下: Live Demo
<divng-controller="mainCtrl">
<inputtype="text"ng-model="model.firstName"/>
<inputtype="text"ng-model="model.lastName"/>
<br/>
<spanng-bind="model.fullName()"></span>
<br/>
<span>{{ model.fullName() }}</span>
</div>
<scriptsrc="http://ajax.googleapis.com/ajax/libs/angularjs/1.2.14/angular.js"></script>
<script>
angular.module("sampleApp", [])
.controller("mainCtrl", function($scope) {
function myViewModel() {
var self = this;
self.firstName="Jeffrey";
self.lastName="Lee";
self.fullName = function() {
return self.firstName + " " + self.lastName;
};
}
$scope.model = new myViewModel();
});
</script>
像KO裡的做法一樣,我先宣告myViewModel(),再$scope.model = new myViewModel(),而ng-model則要改用model.firstName, model.lastName, model.fullName()。在更正式的應用裡,function() myViewModel會放在獨立JS檔維護管理。
[NG系列]
http://www.darkthread.net/kolab/labs/default.aspx?m=post&t=angularjs