取代jQuery?
我很久之前便聽說了angularJS的大名,之前的leader也經常感嘆angularJS的設計如何如何精妙,可嘆一直沒有機會深入瞭解,國慶長假因為沒錢出遊,倒是可以對他做一個瞭解……
根據之前的經驗,就現有的前端專案,如果最初沒有良好的設計,做到一定階段一定會變得難以維護,就算最初有設計,變化無常的PM也會讓你的專案BUG叢生。
一個頁面的複雜程度不斷的增加,依賴模組也會變得混亂,而其中最為頭疼的就是頁面級隨心所欲的DOM操作了!
MVC類的框架可以很好的解決以上問題,而號稱MVVM的angularJS在處理這種情況似乎更有話語權,所以我們今天便來好好研究其一番。
angular適合做具有複雜資料互動的前端應用,他旨在讓我們擺脫繁瑣的DOM操作,而將注意力集中在業務邏輯上,這裡擺脫繁瑣的DOM操作是個非常關鍵的願景,也是很多人不太理解,甚至會將jQuery這種庫與Backbone或者angularJS這種框架做對比的原因。
1 2 |
jQuery是非常優秀的DOM操作工具庫,在DOM操作上,基本沒有庫能超越他了 但Backbone&angularJS這種MVC是框架提供的是完整的解決方案,甚至會依賴jQuery&zepto,他們是兩個東西,不能互相比較,所以完全沒有angularJS要取代jQuery的可能,而當DOM操作過於雜亂一定是你的專案出了問題。 |
這裡舉個jQuery不依賴MVC骨架的例子,我們的訂單填寫頁,需要在商品數量變化後導致金額變化,並且沒有選商品時,支付按鈕不可點選:
對於一個有些經驗的菜鳥來說,可能會這樣寫程式碼:
1 2 3 |
$('#reduceNum').click(function() { $('#payBar #num').text($('#curNum').html() - 1); }); |
對於一些有一定經驗的老鳥來說,可能會這樣寫程式碼:
1 2 3 4 5 6 7 |
events: { 'click #reduceNum': reduceNumAction }, reduceNumAction: function() { $('#payBar #num').text($('#curNum').html() - 1); } |
第一段程式碼可能會導致你年底加薪無望,並且在團隊中沒有話語權;而第二段程式碼積累到一定量後會讓這個專案變得不可維護:
① 支付工具欄初始化狀態如何顯示,如果數字元件按需做非同步載入,這個顯示將變得更加負責。
② 哪些操作將導致支付欄變化,你如何組織這些變化的程式碼,是讓他四散到各處,還是集合在一起,集合後導致函式過大怎麼辦?
③ 新增的導致工具欄變化的操作會不會對原來的操作造成影響,新增的程式碼放在何處?
④ 如果有地方要使用工具欄處的資訊,取的資訊會不會是無效的(取的時候可能正在變化),應該通過DOM取還是記憶體取?
⑤ 如果支付欄DOM結構如果變化,對你的程式影響有多大,如何主流程的影響,比較支付點選後只需要運算元據,不需要關注DOM?
⑥ ……
這個就是僅僅依賴jQuery要面臨的問題,並且這種問題是無解的,因為這裡的專注點是DOM操作而不是資料,如果將關注點變成了資料,程式碼就不是這樣寫的,DOM操作僅僅是過程而不是目的,我們程式碼的目的,往往是展示資料、獲取資料,這點一定要清晰。
所以讓我們帶著這些問題:angular的優勢在何處,他如何改善我們的程式設計體驗,進入今天的學習吧。
初探angularJS
Hello World
學習任何一門語言,Hello world是必不可少的,他是我們邁向精通的唯一路徑:
1 2 3 4 5 6 7 8 9 |
<!doctype html> <html ng-app> <head> <script src="angular.js" type="text/javascript"></script> </head> <body> Hello {{'World'}}! </body> </html> |
被{{}}包裹的便是angularJS變數,上述程式稍作改變的話:
1 2 3 4 5 6 7 8 9 10 |
<!doctype html> <html ng-app> <head> <script src="angular.js" type="text/javascript"></script> </head> <body> <input ng-model="name" type="text" /> Hello {{name}}! </body> </html> |
便會同步顯示文字框輸入內容,這裡通訊的基礎是model對應著ng-model,只要被ng-app包裹就會受angularJS控制,用angularJS自己的話說:HTML標籤增強
作用域
為什麼文字框中的變化會體現在外層,這個涉及到了ng-model的雙向繫結知識,我們暫時不予理睬,但是外層又是從哪裡讀取name這個變數的呢?
在angular中,屬性會儲存在一個@scope(作用域)的物件上,每次我們對文字框的更新皆會通知scope上的name屬性,在angular中,scope是連線controllers(控制器)與template(檢視)的主要膠合器。
上述程式碼完全不涉及js程式碼,真實的場景中每個程式碼段會對controller做依賴,我們這裡對程式碼做一些更改:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
<!doctype html> <html> <head> <script src="angular.js" type="text/javascript"></script> </head> <body ng-app="app" ng-controller="MainCtrl"> <h1 ng-click="click()"> Hello {{name}}! </h1> <script> var app = angular.module('app', []); app.controller('MainCtrl', function ($scope) { $scope.name = 'World'; $scope.click = function () { $scope.name = '霹靂布袋戲'; }; }); </script> </body> </html> |
這裡首先定義了一個application模組,後續會看見,我們每次程式碼一定會新建一個application,相當於名稱空間的意思,後面還可以做依賴用。
接著,我們建立了一個controller模組,這裡已經有點MVC的味道了,controller接受$scope屬性,這個時候模板上所有子標籤對這個控制器中的屬性便有了訪問許可權,這裡用到了一些angular指令
ng-app:告訴html標籤已經處於angular的控制了,可以使用angular的特性
ng-controller:一個module下面可以包括多個控制器,每一個標籤所屬的控制器由該指令指定
上述程式碼是將控制器中的資料讀出來,我們同樣也可以將View中的資料讀入到控制器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
<!doctype html> <html> <head> <script src="angular.js" type="text/javascript"></script> </head> <body ng-app="app" ng-controller="MainCtrl"> <input type="text" ng-model="message" /> <h1 ng-click="click()"> Hello {{name}}! </h1> <script> var app = angular.module('app', []); app.controller('MainCtrl', function ($scope) { $scope.name = 'World'; $scope.click = function () { $scope.name = $scope.message; }; }); </script> </body> </html> |
PS:看到這裡,老夫虎軀為之一振,對該特性的實現產生了興趣,後續值得深入
指令
指令讓我們有能力使用angular規定的方式為HTML標籤增加新特性,angular內建了很多有用的指令,這裡仍然舉一個簡單的例子說明問題:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
<!doctype html> <html> <head> <script src="angular.js" type="text/javascript"></script> </head> <body ng-app="app"> <ul ng-controller="MainCtrl"> <li ng-repeat="v in arr">{{v}}</li> </ul> <script> var app = angular.module('app', []); app.controller('MainCtrl', function ($scope) { $scope.arr = ['素還真', '一頁書', '葉小釵'] }); </script> </body> </html> |
我們除了使用angular的內建指令外,還可以自定義指令,比如這裡的讓文字框自動獲取焦點的指令:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
<!doctype html> <html> <head> <script src="angular.js" type="text/javascript"></script> </head> <body ng-app="app" ng-controller="MainCtrl"> <input type="text" focus ng-model="user.name" /> <button ng-click="greet()"> Click here!</button> <h3> {{ message }}</h3> <script> var app = angular.module('app', []); app.controller('MainCtrl', function ($scope) { $scope.greet = function () { $scope.message = "Hello, " + $scope.user.name; } }); app.directive('focus', function () { return { link: function (scope, element, attrs) { element[0].focus(); } }; }); </script> </body> </html> |
指令的使用可以很複雜,後續我們會更加深入,這裡再舉一個單獨使用的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
<!doctype html> <html> <head> <script src="angular.js" type="text/javascript"></script> </head> <body ng-app="app"> <hello></hello> <script> var app = angular.module('app', []); app.directive('hello', function () { return { restrict: "E", replace: true, template: "<div>顯示固定資料,類似自定義標籤</div>" } }); </script> </body> </html> |
指令的定義有很多引數,可以指定該指令作為屬性還是作為標籤,這個我們後續再深入瞭解。
過濾器
感覺過濾器是參考的smarty的語法,一般而言是用作顯示的增強,angular本身也提供了很多內建過濾器,比如:
1 2 |
{{ "aaaa" | uppercase }} // AAAA {{ "BBBB" | lowercase }} // bbbb |
感覺比較有用的是日期操作過濾器:
1 2 3 4 |
{{ 1427345339072 | date:'yyyy' }} // 2015 {{ 1427345339072 |date:'MM' }} // 03 {{ 1427345339072 | date:'d' }} // 26,一月中第多少天 ...... |
數字格式化:
1 2 |
{{12.13534|number:2}} // 12.14 四捨五入保留兩位小數 {{10000000|number}} // 10,000,000 |
當然,我們可以使用自定義過濾器,比如這裡我想對超出某一區間的數字加…
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
<!doctype html> <html> <head> <script src="angular.js" type="text/javascript"></script> </head> <body ng-app="app" ng-controller="MainCtrl"> <input type="text" ng-model="message" /> <h3> {{ message |myFilter }}</h3> <script> var app = angular.module('app', []); app.controller('MainCtrl', function ($scope) { $scope.message = ''; }); app.filter('myFilter', function () { return function (input, param) { return input.length < 5 ? input : input.substring(0, 5) + '...' } }); </script> </body> </html> |
具備了以上知識,我們嘗試進入To都MVC看看
參考:http://www.cnblogs.com/whitewolf/p/angularjs-start.html
TodoMVC
我們由最新的TodoMVC下載程式碼:http://todomvc.com/,首先檢視js引用情況:
1 2 3 4 5 6 7 |
<script src="node_modules/angular/angular.js"></script> <script src="node_modules/angular-route/angular-route.js"></script> <script src="js/app.js"></script> <script src="js/controllers/todoCtrl.js"></script> <script src="js/services/todoStorage.js"></script> <script src="js/directives/todoFocus.js"></script> <script src="js/directives/todoEscape.js"></script> |
除了angular本體檔案外,還多了個angular的擴充套件,做單頁應用的路由功能的,這個路由程式碼量不大,使用和Backbone的路由比較類似;app.js為入口檔案,配置路由的地方;餘下是控制器檔案檔案以及一個localstorage的操作服務,餘下就是指令了。
程式碼首先定義了一個模組作為本次程式的名稱空間:
1 |
angular.module('todomvc', ['ngRoute']) |
ngRoute為其依賴項,可以從route的定義看出:
1 2 3 |
var ngRouteModule = angular.module('ngRoute', ['ng']). provider('$route', $RouteProvider), $routeMinErr = angular.$$minErr('ngRoute'); |
這裡來看看其router的配置,以及index.html的寫法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 |
<!doctype html> <html lang="en" data-framework="angularjs"> <head> <meta charset="utf-8"> <title>AngularJS • TodoMVC</title> <link rel="stylesheet" href="node_modules/todomvc-common/base.css"> <link rel="stylesheet" href="node_modules/todomvc-app-css/index.css"> <style>[ng-cloak] { display: none; }</style> </head> <body ng-app="todomvc"> <ng-view /> <script type="text/ng-template" id="todomvc-index.html"> <section id="todoapp"> <header id="header"> <h1>todos</h1> <form id="todo-form" ng-submit="addTodo()"> <input id="new-todo" placeholder="What needs to be done?" ng-model="newTodo" ng-disabled="saving" autofocus> </form> </header> <section id="main" ng-show="todos.length" ng-cloak> <input id="toggle-all" type="checkbox" ng-model="allChecked" ng-click="markAll(allChecked)"> <label for="toggle-all">Mark all as complete</label> <ul id="todo-list"> <li ng-repeat="todo in todos | filter:statusFilter track by $index" ng-class="{completed: todo.completed, editing: todo == editedTodo}"> <div class="view"> <input class="toggle" type="checkbox" ng-model="todo.completed" ng-change="toggleCompleted(todo)"> <label ng-dblclick="editTodo(todo)">{{todo.title}}</label> <button class="destroy" ng-click="removeTodo(todo)"></button> </div> <form ng-submit="saveEdits(todo, 'submit')"> <input class="edit" ng-trim="false" ng-model="todo.title" todo-escape="revertEdits(todo)" ng-blur="saveEdits(todo, 'blur')" todo-focus="todo == editedTodo"> </form> </li> </ul> </section> <footer id="footer" ng-show="todos.length" ng-cloak> <span id="todo-count"><strong>{{remainingCount}}</strong> <ng-pluralize count="remainingCount" when="{ one: 'item left', other: 'items left' }"></ng-pluralize> </span> <ul id="filters"> <li> <a ng-class="{selected: status == ''} " href="#/">All</a> </li> <li> <a ng-class="{selected: status == 'active'}" href="#/active">Active</a> </li> <li> <a ng-class="{selected: status == 'completed'}" href="#/completed">Completed</a> </li> </ul> <button id="clear-completed" ng-click="clearCompletedTodos()" ng-show="completedCount">Clear completed</button> </footer> </section> <footer id="info"> <p>Double-click to edit a todo</p> <p>Credits: <a href="http://twitter.com/cburgdorf">Christoph Burgdorf</a>, <a href="http://ericbidelman.com">Eric Bidelman</a>, <a href="http://jacobmumm.com">Jacob Mumm</a> and <a href="http://igorminar.com">Igor Minar</a> </p> <p>Part of <a href="http://todomvc.com">TodoMVC</a></p> </footer> </script> <script src="node_modules/angular/angular.js"></script> <script src="node_modules/angular-route/angular-route.js"></script> <script src="js/app.js"></script> <script src="js/controllers/todoCtrl.js"></script> <script src="js/services/todoStorage.js"></script> <script src="js/directives/todoFocus.js"></script> <script src="js/directives/todoEscape.js"></script> </body> </html> index.html |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
var routeConfig = { controller: 'TodoCtrl', templateUrl: 'todomvc-index.html', resolve: { store: function (todoStorage) { // Get the correct module (API or localStorage). return todoStorage.then(function (module) { module.get(); // Fetch the todo records in the background. return module; }); } } }; $routeProvider .when('/', routeConfig) .when('/:status', routeConfig) .otherwise({ redirectTo: '/' }); |
這個程式碼現在基本看不懂,大概意思應該就是根據路由執行config中的邏輯,將模板展示在頁面上,其中index.html有一段程式碼應該是用於替換模板的:
1 |
<ng-view /> |
我們先拋開那段看不懂的,直奔主流程,目光聚焦到控制器controller:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 |
angular.module('todomvc') .controller('TodoCtrl', function TodoCtrl($scope, $routeParams, $filter, store) { 'use strict'; var todos = $scope.todos = store.todos; $scope.newTodo = ''; $scope.editedTodo = null; $scope.$watch('todos', function () { $scope.remainingCount = $filter('filter')(todos, { completed: false }).length; $scope.completedCount = todos.length - $scope.remainingCount; $scope.allChecked = !$scope.remainingCount; }, true); // Monitor the current route for changes and adjust the filter accordingly. $scope.$on('$routeChangeSuccess', function () { var status = $scope.status = $routeParams.status || ''; $scope.statusFilter = (status === 'active') ? { completed: false } : (status === 'completed') ? { completed: true } : {}; }); $scope.addTodo = function () { var newTodo = { title: $scope.newTodo.trim(), completed: false }; if (!newTodo.title) { return; } $scope.saving = true; store.insert(newTodo) .then(function success() { $scope.newTodo = ''; }) .finally(function () { $scope.saving = false; }); }; $scope.editTodo = function (todo) { $scope.editedTodo = todo; // Clone the original todo to restore it on demand. $scope.originalTodo = angular.extend({}, todo); }; $scope.saveEdits = function (todo, event) { // Blur events are automatically triggered after the form submit event. // This does some unfortunate logic handling to prevent saving twice. if (event === 'blur' && $scope.saveEvent === 'submit') { $scope.saveEvent = null; return; } $scope.saveEvent = event; if ($scope.reverted) { // Todo edits were reverted-- don't save. $scope.reverted = null; return; } todo.title = todo.title.trim(); if (todo.title === $scope.originalTodo.title) { $scope.editedTodo = null; return; } store[todo.title ? 'put' : 'delete'](todo) .then(function success() {}, function error() { todo.title = $scope.originalTodo.title; }) .finally(function () { $scope.editedTodo = null; }); }; $scope.revertEdits = function (todo) { todos[todos.indexOf(todo)] = $scope.originalTodo; $scope.editedTodo = null; $scope.originalTodo = null; $scope.reverted = true; }; $scope.removeTodo = function (todo) { store.delete(todo); }; $scope.saveTodo = function (todo) { store.put(todo); }; $scope.toggleCompleted = function (todo, completed) { if (angular.isDefined(completed)) { todo.completed = completed; } store.put(todo, todos.indexOf(todo)) .then(function success() {}, function error() { todo.completed = !todo.completed; }); }; $scope.clearCompletedTodos = function () { store.clearCompleted(); }; $scope.markAll = function (completed) { todos.forEach(function (todo) { if (todo.completed !== completed) { $scope.toggleCompleted(todo, completed); } }); }; }); |
這段程式碼130行不到,讓我體會到了深深的神奇,首先我們在app中返回了讀取到localstorage的物件:
1 2 3 4 5 6 7 8 9 |
resolve: { store: function (todoStorage) { // Get the correct module (API or localStorage). return todoStorage.then(function (module) { module.get(); // Fetch the todo records in the background. return module; }); } } |
然後就在controller的依賴項中讀到了被注入的物件:
1 |
var todos = $scope.todos = store.todos; |
此時,模板也被插到了頁面上,等待controller的執行:
首先這裡有一個$watch方法,監控著todos的變化,每次變化都會體現到這裡,導致view的變化:
1 2 3 4 5 |
$scope.$watch('todos', function () { $scope.remainingCount = $filter('filter')(todos, { completed: false }).length; $scope.completedCount = todos.length - $scope.remainingCount; $scope.allChecked = !$scope.remainingCount; }, true); |
然後我們將關注點放在新增專案上:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
$scope.addTodo = function () { var newTodo = { title: $scope.newTodo.trim(), completed: false }; if (!newTodo.title) { return; } $scope.saving = true; store.insert(newTodo) .then(function success() { $scope.newTodo = ''; }) .finally(function () { $scope.saving = false; }); }; |
View上的呼叫點是:
1 2 3 4 5 6 |
<header id="header"> <h1>todos</h1> <form id="todo-form" ng-submit="addTodo()"> <input id="new-todo" placeholder="What needs to be done?" ng-model="newTodo" ng-disabled="saving" autofocus> </form> </header> |
首先這段程式碼中有一個autofocus的指令,沒有什麼卵用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
angular.module('todomvc') .directive('todoFocus', function todoFocus($timeout) { 'use strict'; return function (scope, elem, attrs) { scope.$watch(attrs.todoFocus, function (newVal) { if (newVal) { $timeout(function () { elem[0].focus(); }, 0, false); } }); }; }); |
可以看到model直接繫結到了該文字框上,所以addTodo方法可以直接根據$scope獲取文字框的屬性,完了呼叫單例store提供的靜態方法儲存資料,saving引數可以暫時將文字框變成不可編輯狀態,而後todo資料更新,會自動引發View變化,於是流程結束!!!
我們如果將$scope放到全域性上對其資料造成變化:
1 2 3 |
window.sss = $scope; //控制檯中造成變化 sss.todos.pop() |
每次返回操作檢視時候,該變化會馬上反應到View上,於是我發現了以下不同:
① 因為所有與業務相關的資料全部做了雙向繫結,我根本沒有必要由dom獲取資料了,我自然而然的到$scope中獲取資料,不知道為什麼,這個特性讓我有點愉悅!
② 我要做的事情其實就是約定好資料物件,然後將該物件放到要用到的所有檢視上即可,每次記憶體中資料變化Dom會同步更新
於是通過以上兩點,我似乎得到了一個驚人的結論:
1 |
似乎我一旦配置好ng-model後,我要做的事情僅僅是操作$scope上的資料!!! |
因為,前端要做的事情只不過是正確的展示伺服器端的資料,每次DOM事件造成的改變也往往是資料引起的,如果我們能做到資料變化自動更新到DOM變化的話,那麼DOM操作的必要似乎沒有了,而angular乾的事情正是如此!!!
思考
到此為止,TodoMVC的程式碼我雖然沒有完全看懂,但是他帶給我的震撼是全方位的,之前使用MVC類框架可以規範資料到DOM的操作,很大程度上解除DOM和JavaScript的耦合關係,而angular似乎完全拋開了業務資料導致的DOM變化操作!!!
我們現在團隊有一mis後臺系統,我在考慮是否要把它接過來,使用angular+bootstrap重構,可能別有一番風味吧!
最後,今天初步調研了一下angularJS,就已經感受到他的魅力了,後面時間需要將之用於實踐,並且對其設計思想作深入研究!!!