天天看點

擺脫DOM操作,從TodoMVC看angularJS

取代jQuery?

我很久之前便聽說了angularJS的大名,之前的leader也經常感歎angularJS的設計如何如何精妙,可歎一直沒有機會深入了解,國慶長假因為沒錢出遊,倒是可以對他做一個了解......

根據之前的經驗,就現有的前端項目,如果最初沒有良好的設計,做到一定階段一定會變得難以維護,就算最初有設計,變化無常的PM也會讓你的項目BUG叢生。

一個頁面的複雜程度不斷的增加,依賴子產品也會變得混亂,而其中最為頭疼的就是頁面級随心所欲的DOM操作了!

MVC類的架構可以很好的解決以上問題,而号稱MVVM的angularJS在處理這種情況似乎更有話語權,是以我們今天便來好好研究其一番。

angular适合做具有複雜資料互動的前端應用,他旨在讓我們擺脫繁瑣的DOM操作,而将注意力集中在業務邏輯上,這裡擺脫繁瑣的DOM操作是個非常關鍵的願景,也是很多人不太了解,甚至會将jQuery這種庫與Backbone或者angularJS這種架構做對比的原因。

jQuery是非常優秀的DOM操作工具庫,在DOM操作上,基本沒有庫能超越他了

但Backbone&angularJS這種MVC是架構提供的是完整的解決方案,甚至會依賴jQuery&zepto,他們是兩個東西,不能互相比較,是以完全沒有angularJS要取代jQuery的可能,而當DOM操作過于雜亂一定是你的項目出了問題。

這裡舉個jQuery不依賴MVC骨架的例子,我們的訂單填寫頁,需要在商品數量變化後導緻金額變化,并且沒有選商品時,支付按鈕不可點選:

對于一個有些經驗的菜鳥來說,可能會這樣寫代碼:

$('#reduceNum').click(function() {

      $('#payBar #num').text($('#curNum').html() - 1);

});

對于一些有一定經驗的老鳥來說,可能會這樣寫代碼:

複制代碼

1 events: {

2   'click #reduceNum': reduceNumAction  

3 },

5 reduceNumAction: function() {

6   $('#payBar #num').text($('#curNum').html() - 1);

7 }

第一段代碼可能會導緻你年底加薪無望,并且在團隊中沒有話語權;而第二段代碼積累到一定量後會讓這個項目變得不可維護:

① 支付工具欄初始化狀态如何顯示,如果數字元件按需做異步加載,這個顯示将變得更加負責。

② 哪些操作将導緻支付欄變化,你如何組織這些變化的代碼,是讓他四散到各處,還是集合在一起,集合後導緻函數過大怎麼辦?

③ 新增的導緻工具欄變化的操作會不會對原來的操作造成影響,新增的代碼放在何處?

④ 如果有地方要使用工具欄處的資訊,取的資訊會不會是無效的(取的時候可能正在變化),應該通過DOM取還是記憶體取?

⑤ 如果支付欄DOM結構如果變化,對你的程式影響有多大,如何主流程的影響,比較支付點選後隻需要操作資料,不需要關注DOM?

⑥ ......

這個就是僅僅依賴jQuery要面臨的問題,并且這種問題是無解的,因為這裡的專注點是DOM操作而不是資料,如果将關注點變成了資料,代碼就不是這樣寫的,DOM操作僅僅是過程而不是目的,我們代碼的目的,往往是展示資料、擷取資料,這點一定要清晰。

是以讓我們帶着這些問題:angular的優勢在何處,他如何改善我們的程式設計體驗,進入今天的學習吧。

初探angularJS

Hello World

學習任何一門語言,Hello world是必不可少的,他是我們邁向精通的唯一路徑:

1 <!doctype html>

2 <html ng-app>

3 <head>

4   <script src="angular.js" type="text/javascript"></script>

5 </head>

6 <body>

7   Hello {{'World'}}!

8 </body>

9 </html>

被{{}}包裹的便是angularJS變量,上述程式稍作改變的話:

 1 <!doctype html>

 2 <html ng-app>

 3 <head>

 4   <script src="angular.js" type="text/javascript"></script>

 5 </head>

 6 <body>

 7   <input ng-model="name" type="text" />

 8   Hello {{name}}!

 9 </body>

10 </html>

便會同步顯示文本框輸入内容,這裡通信的基礎是model對應着ng-model,隻要被ng-app包裹就會受angularJS控制,用angularJS自己的話說:HTML标簽增強

作用域

為什麼文本框中的變化會展現在外層,這個涉及到了ng-model的雙向綁定知識,我們暫時不予理睬,但是外層又是從哪裡讀取name這個變量的呢?

在angular中,屬性會存儲在一個@scope(作用域)的對象上,每次我們對文本框的更新皆會通知scope上的name屬性,在angular中,scope上的name屬性,在angular中,scope是連接配接controllers(控制器)與template(視圖)的主要膠合器。

上述代碼完全不涉及js代碼,真實的場景中每個代碼段會對controller做依賴,我們這裡對代碼做一些更改:

 2 <html>

 6 <body ng-app="app" ng-controller="MainCtrl">

 7   <h1 ng-click="click()">

 8     Hello {{name}}!

 9   </h1>

10   <script>

11     var app = angular.module('app', []);

12     app.controller('MainCtrl', function ($scope) {

13       $scope.name = 'World';

14       $scope.click = function () {

15         $scope.name = '霹靂布袋戲';

16       };

17     });

18   </script>

19 </body>

20 </html>

這裡首先定義了一個application子產品,後續會看見,我們每次代碼一定會建立一個application,相當于命名空間的意思,後面還可以做依賴用。

接着,我們建立了一個controller子產品,這裡已經有點MVC的味道了,controller接受$scope屬性,這個時候模闆上所有子标簽對這個控制器中的屬性便有了通路權限,這裡用到了一些angular指令

ng-app:告訴html标簽已經處于angular的控制了,可以使用angular的特性

ng-controller:一個module下面可以包括多個控制器,每一個标簽所屬的控制器由該指令指定

上述代碼是将控制器中的資料讀出來,我們同樣也可以将View中的資料讀入到控制器:

 7   <input type="text" ng-model="message" />

 8   <h1 ng-click="click()">

 9     Hello {{name}}!

10   </h1>

11   <script>

12     var app = angular.module('app', []);

13     app.controller('MainCtrl', function ($scope) {

14       $scope.name = 'World';

15       $scope.click = function () {

16         $scope.name = $scope.message;

17       };

18     });

19   </script>

20 </body>

21 </html>

PS:看到這裡,老夫虎軀為之一振,對該特性的實作産生了興趣,後續值得深入

指令

指令讓我們有能力使用angular規定的方式為HTML标簽增加新特性,angular内置了很多有用的指令,這裡仍然舉一個簡單的例子說明問題:

 6 <body ng-app="app">

 7   <ul ng-controller="MainCtrl">

 8     <li ng-repeat="v in arr">{{v}}</li>

 9   </ul>

13       $scope.arr = ['素還真', '一頁書', '葉小钗']

14     });

15   </script>

16 </body>

17 </html>

我們除了使用angular的内置指令外,還可以自定義指令,比如這裡的讓文本框自動擷取焦點的指令:

 7   <input type="text" focus  ng-model="user.name" />

 8   <button ng-click="greet()">

 9     Click here!</button>

10   <h3>

11     {{ message }}</h3>

12   <script>

13     var app = angular.module('app', []);

14     app.controller('MainCtrl', function ($scope) {

15       $scope.greet = function () {

16         $scope.message = "Hello, " + $scope.user.name;

17       }

19     app.directive('focus', function () {

20       return {

21         link: function (scope, element, attrs) {

22           element[0].focus();

23         }

24       };

25     });

26   </script>

27 </body>

28 </html>

指令的使用可以很複雜,後續我們會更加深入,這裡再舉一個單獨使用的例子:

 7   <hello></hello>

 8   <script>

 9     var app = angular.module('app', []);

10     app.directive('hello', function () {

11       return {

12         restrict: "E",

13         replace: true,

14         template: "<div>顯示固定資料,類似自定義标簽</div>"

15       }

16     });

17   </script>

18 </body>

19 </html>

指令的定義有很多參數,可以指定該指令作為屬性還是作為标簽,這個我們後續再深入了解。

過濾器

感覺過濾器是參考的smarty的文法,一般而言是用作顯示的增強,angular本身也提供了很多内置過濾器,比如:

1 {{ "aaaa" | uppercase }} // AAAA

2 {{ "BBBB" | lowercase }} // bbbb

感覺比較有用的是日期操作過濾器:

{{ 1427345339072 | date:'yyyy' }} // 2015

{{ 1427345339072 |date:'MM' }} // 03

{{ 1427345339072 | date:'d' }} // 26,一月中第多少天

......

數字格式化:

{{12.13534|number:2}} // 12.14 四舍五入保留兩位小數

{{10000000|number}} // 10,000,000

當然,我們可以使用自定義過濾器,比如這裡我想對超出某一區間的數字加...

 8   <h3>

 9     {{ message |myFilter }}</h3>

13       $scope.message = '';

15 

16     app.filter('myFilter', function () {

17       return function (input, param) {

18         return input.length < 5 ? input : input.substring(0, 5) + '...'

19       }

20     });

21   </script>

22 </body>

23 </html>

具備了以上知識,我們嘗試進入To都MVC看看

參考:http://www.cnblogs.com/whitewolf/p/angularjs-start.html

TodoMVC

我們由最新的TodoMVC下載下傳代碼:http://todomvc.com/,首先檢視js引用情況:

1 <script src="node_modules/angular/angular.js"></script>

2 <script src="node_modules/angular-route/angular-route.js"></script>

3 <script src="js/app.js"></script>

4 <script src="js/controllers/todoCtrl.js"></script>

5 <script src="js/services/todoStorage.js"></script>

6 <script src="js/directives/todoFocus.js"></script>

7 <script src="js/directives/todoEscape.js"></script>

除了angular本體檔案外,還多了個angular的擴充,做單頁應用的路由功能的,這個路由代碼量不大,使用和Backbone的路由比較類似;app.js為入口檔案,配置路由的地方;餘下是控制器檔案檔案以及一個localstorage的操作服務,餘下就是指令了。

代碼首先定義了一個子產品作為本次程式的命名空間:

1 angular.module('todomvc', ['ngRoute'])

ngRoute為其依賴項,可以從route的定義看出:

1 var ngRouteModule = angular.module('ngRoute', ['ng']).

2                         provider('$route', $RouteProvider),

3     $routeMinErr = angular.$$minErr('ngRoute');

這裡來看看其router的配置,以及index.html的寫法:

 index.html

 1 var routeConfig = {

 2     controller: 'TodoCtrl',

 3     templateUrl: 'todomvc-index.html',

 4     resolve: {

 5       store: function (todoStorage) {

 6         // Get the correct module (API or localStorage).

 7         return todoStorage.then(function (module) {

 8           module.get(); // Fetch the todo records in the background.

 9           return module;

10         });

11       }

12     }

13 };

14 

15 $routeProvider

16     .when('/', routeConfig)

17     .when('/:status', routeConfig)

18     .otherwise({

19         redirectTo: '/'

這個代碼現在基本看不懂,大概意思應該就是根據路由執行config中的邏輯,将模闆展示在頁面上,其中index.html有一段代碼應該是用于替換模闆的:

<ng-view />

我們先抛開那段看不懂的,直奔主流程,目光聚焦到控制器controller:

 View Code

這段代碼130行不到,讓我體會到了深深的神奇,首先我們在app中傳回了讀取到localstorage的對象:

1 resolve: {

2     store: function (todoStorage) {

3       // Get the correct module (API or localStorage).

4       return todoStorage.then(function (module) {

5         module.get(); // Fetch the todo records in the background.

6         return module;

7       });

8     }

9 }

然後就在controller的依賴項中讀到了被注入的對象:

var todos = $scope.todos = store.todos;

此時,模闆也被插到了頁面上,等待controller的執行:

首先這裡有一個$watch方法,監控着todos的變化,每次變化都會展現到這裡,導緻view的變化:

1 $scope.$watch('todos', function () {

2     $scope.remainingCount = $filter('filter')(todos, { completed: false }).length;

3     $scope.completedCount = todos.length - $scope.remainingCount;

4     $scope.allChecked = !$scope.remainingCount;

5 }, true);

然後我們将關注點放在新增項目上:

 1 $scope.addTodo = function () {

 2     var newTodo = {

 3         title: $scope.newTodo.trim(),

 4         completed: false

 5     };

 6 

 7     if (!newTodo.title) {

 8         return;

 9     }

10 

11     $scope.saving = true;

12     store.insert(newTodo)

13         .then(function success() {

14             $scope.newTodo = '';

15         })

16         .finally(function () {

17             $scope.saving = false;

18         });

19 };

View上的調用點是:

1 <header id="header">

2     <h1>todos</h1>

3     <form id="todo-form" ng-submit="addTodo()">

4         <input id="new-todo" placeholder="What needs to be done?" ng-model="newTodo" ng-disabled="saving" autofocus>

5     </form>

6 </header>

首先這段代碼中有一個autofocus的指令,沒有什麼卵用:

可以看到model直接綁定到了該文本框上,是以addTodo方法可以直接根據$scope擷取文本框的屬性,完了調用單例store提供的靜态方法存儲資料,saving參數可以暫時将文本框變成不可編輯狀态,而後todo資料更新,會自動引發View變化,于是流程結束!!!

我們如果将$scope放到全局上對其資料造成變化:

window.sss = $scope;

//控制台中造成變化

sss.todos.pop()

每次傳回操作視圖時候,該變化會馬上反應到View上,于是我發現了以下不同:

① 因為所有與業務相關的資料全部做了雙向綁定,我根本沒有必要由dom擷取資料了,我自然而然的到$scope中擷取資料,不知道為什麼,這個特性讓我有點愉悅!

② 我要做的事情其實就是約定好資料對象,然後将該對象放到要用到的所有視圖上即可,每次記憶體中資料變化Dom會同步更新

于是通過以上兩點,我似乎得到了一個驚人的結論:

似乎我一旦配置好ng-model後,我要做的事情僅僅是操作$scope上的資料!!!

因為,前端要做的事情隻不過是正确的展示伺服器端的資料,每次DOM事件造成的改變也往往是資料引起的,如果我們能做到資料變化自動更新到DOM變化的話,那麼DOM操作的必要似乎沒有了,而angular幹的事情正是如此!!!

思考

到此為止,TodoMVC的代碼我雖然沒有完全看懂,但是他帶給我的震撼是全方位的,之前使用MVC類架構可以規範資料到DOM的操作,很大程度上解除DOM和JavaScript的耦合關系,而angular似乎完全抛開了業務資料導緻的DOM變化操作!!!

我們現在團隊有一mis背景系統,我在考慮是否要把它接過來,使用angular+bootstrap重構,可能别有一番風味吧!

最後,今天初步調研了一下angularJS,就已經感受到他的魅力了,後面時間需要将之用于實踐,并且對其設計思想作深入研究!!!

本文轉自葉小钗部落格園部落格,原文連結:http://www.cnblogs.com/yexiaochai/p/4855780.html,如需轉載請自行聯系原作者

繼續閱讀