最近一段時間一直在看AngularJS,趁着一點時間總結一下。
先推薦幾個教程
1. AngularJS入門教程 比較基礎,是官方Tutorial的翻譯。
2. 七步從AngularJS菜鳥到專家 也比較基礎,制作了一個在線音樂播放網站。
3. AngularJS開發指南 這個教程比較全面,但我感覺翻譯的有些晦澀難懂。
看過這些教程后,覺得AngularJS也懂一點了,就想用它干點事,就分析一下AngularJS寫的todomvc吧。
Todomvc官網地址:http://todomvc.com/
項目的目錄如下:

bower_components里放了兩個文件夾,其中angular文件夾是用來一如angular.js文件的,todomvc-common文件夾里的放入了所有todo項目統一的css\js(只是用來生成左側內容的,與項目無關)和圖片。
js文件夾是大頭,里面放了相應的controller(控制器)\directive(指令)\service(服務)和app.js。
test文件夾里放的是測試用的代碼,不分析。
index.html是項目的view頁面。
先來看一下app.js
/*global angular */ /*jshint unused:false */ 'use strict'; /** * The main TodoMVC app module * * @type {angular.Module} */ var todomvc = angular.module('todomvc', []);
就是定義了一個模塊todomvc
再看一下services下的todoStorage.js
/*global todomvc */ 'use strict'; /** * Services that persists and retrieves TODOs from localStorage */ todomvc.factory('todoStorage', function () { // todos JSON字符串存儲的唯一標識 var STORAGE_ID = 'todos-angularjs'; return { // 從localStorage中取出todos,並解析成JSON對象 get: function () { return JSON.parse(localStorage.getItem(STORAGE_ID) || '[]'); }, // 將todos對象轉化成JSON字符串,並存入localStorage put: function (todos) { localStorage.setItem(STORAGE_ID, JSON.stringify(todos)); } }; });
使用factory方法創建了todoStorage的service方法,這個service方法的本質就是返回了兩個方法get和put,兩者都是用了JSON2和HTML5的特性。get將todos的內容從localStorage中取出,並解析成JSON,put將todos轉化成JSON字符串,並存儲到localStorage中。
再看一下directives下面的兩個指令文件。
todoFocus.js
/*global todomvc */ 'use strict'; /** * Directive that places focus on the element it is applied to when the expression it binds to evaluates to true */ todomvc.directive('todoFocus', function todoFocus($timeout) { return function (scope, elem, attrs) { // 為todoFocus屬性的值添加監聽 scope.$watch(attrs.todoFocus, function (newVal) { if (newVal) { $timeout(function () { elem[0].focus(); }, 0, false); } }); }; });
返回function的參數中,elem就是包含該指令的元素的數組,attrs是元素的所有屬性、屬性名等組成的對象。
其中用到了兩個AngularJS的方法
$watch(watchExpression, listener, objectEquality) 注冊一個偵聽器回調,每當watchExpression變化時,監聽回調將被執行。
$timeout(fn[, delay][, invokeApply]) 當timeout的值達到時,執行fn函數。
todoFocus.js創建了todoFocus指令。當一個元素擁有todoFocus屬性時,該指令會為該元素的todoFocus屬性的值添加監聽,如果todoFocus屬性的值改變成true,就會執行$timeout(function () {elem[0].focus();}, 0, false);其中的延遲時間為0秒,所以會立即執行elem[0].focus()。
todoEscape.js
/*global todomvc */ 'use strict'; /** * Directive that executes an expression when the element it is applied to gets * an `escape` keydown event. */ todomvc.directive('todoEscape', function () { var ESCAPE_KEY = 27; return function (scope, elem, attrs) { elem.bind('keydown', function (event) { if (event.keyCode === ESCAPE_KEY) { scope.$apply(attrs.todoEscape); } }); }; });
todoEscape.js創建了todoEscape指令。當按下Escape鍵時,執行attrs.todoEscape的表達式。
看一下大頭,controllers文件夾中的todoCtrl.js,這個文件略長,我就直接寫注釋了。
/*global todomvc, angular */ 'use strict'; /** * The main controller for the app. The controller: * - retrieves and persists the model via the todoStorage service * - exposes the model to the template and provides event handlers */ todomvc.controller('TodoCtrl', function TodoCtrl($scope, $location, todoStorage, filterFilter) { // 從localStorage中獲取todos var todos = $scope.todos = todoStorage.get(); // 記錄新的todo $scope.newTodo = ''; // 記錄編輯過的todo $scope.editedTodo = null; // 當todos的值改變時執行其中的方法 $scope.$watch('todos', function (newValue, oldValue) { // 獲取未完成的todos的數目 $scope.remainingCount = filterFilter(todos, { completed: false }).length; // 獲取已完成的todos的數目 $scope.completedCount = todos.length - $scope.remainingCount; // 當且僅當$scope.remainingCount為0時,$scope.allChecked為true $scope.allChecked = !$scope.remainingCount; // 當todos的新值和舊值不相等時,向localStorage中存入todos if (newValue !== oldValue) { // This prevents unneeded calls to the local storage todoStorage.put(todos); } }, true); if ($location.path() === '') { // 如果$location.path()為空,就設置為/ $location.path('/'); } $scope.location = $location; // 當location.path()的值改變時執行其中的方法 $scope.$watch('location.path()', function (path) { // 獲取狀態的過濾器 // 如果path為'/active',過濾器為{ completed: false } // 如果path為'/completed',過濾器為{ completed: true } // 否則,過濾器為null $scope.statusFilter = (path === '/active') ? { completed: false } : (path === '/completed') ? { completed: true } : null; }); // 添加一個新的todo $scope.addTodo = function () { var newTodo = $scope.newTodo.trim(); if (!newTodo.length) { return; } // 向todos里添加一個todo,completed屬性默認為false todos.push({ title: newTodo, completed: false }); // 置空 $scope.newTodo = ''; }; // 編輯一個todo $scope.editTodo = function (todo) { $scope.editedTodo = todo; // Clone the original todo to restore it on demand. // 保存編輯前的todo,為恢復編輯前做准備 $scope.originalTodo = angular.extend({}, todo); }; // 編輯todo完成 $scope.doneEditing = function (todo) { // 置空 $scope.editedTodo = null; todo.title = todo.title.trim(); if (!todo.title) { // 如果todo的title為空,則移除該todo $scope.removeTodo(todo); } }; // 恢復編輯前的todo $scope.revertEditing = function (todo) { todos[todos.indexOf(todo)] = $scope.originalTodo; $scope.doneEditing($scope.originalTodo); }; // 移除todo $scope.removeTodo = function (todo) { todos.splice(todos.indexOf(todo), 1); }; // 清除已完成的todos $scope.clearCompletedTodos = function () { $scope.todos = todos = todos.filter(function (val) { return !val.completed; }); }; // 標記所有的todo的狀態(true或false) $scope.markAll = function (completed) { todos.forEach(function (todo) { todo.completed = completed; }); }; });
最后看一下index.html,這個文件我們一段一段的分析。
<!doctype html> <html lang="en" ng-app="todomvc" data-framework="angularjs"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title>AngularJS • TodoMVC</title> <link rel="stylesheet" href="bower_components/todomvc-common/base.css"> <style>[ng-cloak] { display: none; }</style> </head> <body> <section id="todoapp" ng-controller="TodoCtrl"> <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" 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"> <label ng-dblclick="editTodo(todo)">{{todo.title}}</label> <button class="destroy" ng-click="removeTodo(todo)"></button> </div> <form ng-submit="doneEditing(todo)"> <input class="edit" ng-trim="false" ng-model="todo.title" todo-escape="revertEditing(todo)" ng-blur="doneEditing(todo)" 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: location.path() == '/'} " href="#/">All</a> </li> <li> <a ng-class="{selected: location.path() == '/active'}" href="#/active">Active</a> </li> <li> <a ng-class="{selected: location.path() == '/completed'}" href="#/completed">Completed</a> </li> </ul> <button id="clear-completed" ng-click="clearCompletedTodos()" ng-show="completedCount">Clear completed ({{completedCount}})</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 src="bower_components/todomvc-common/base.js"></script> <script src="bower_components/angular/angular.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>
首先是在最下面,引入相應的JS,這個就不多說了。
<script src="bower_components/todomvc-common/base.js"></script> <script src="bower_components/angular/angular.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>
定義style[ng-cloak],含有ng-cloak屬性則不可見。
<style>[ng-cloak] { display: none; }</style>
來看添加todo的html,綁定的model為newTodo,submit的方法是todoCtrl.js中的addTodo(),會添加一條todo,點擊Enter,默認觸發提交事件,就觸發了addTodo()方法,添加了一條todo到todos中。
<form id="todo-form" ng-submit="addTodo()"> <input id="new-todo" placeholder="What needs to be done?" ng-model="newTodo" autofocus> </form>
再看展示todos的html
<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"> <label ng-dblclick="editTodo(todo)">{{todo.title}}</label> <button class="destroy" ng-click="removeTodo(todo)"></button> </div> <form ng-submit="doneEditing(todo)"> <input class="edit" ng-trim="false" ng-model="todo.title" todo-escape="revertEditing(todo)" ng-blur="doneEditing(todo)" todo-focus="todo == editedTodo"> </form> </li> </ul> </section>
section使用ngShow方法根據todos的長度判斷是否顯示,加上ng-cloak屬性是為了在剛開始時不要顯示出AngularJS未處理的頁面。可以去掉刷新試一試。
其中id為toggle-all的checkbox綁定到allChecked model上,點擊觸發markAll(allChecked),將allChecked的值傳入,標記所有的todos。
使用ngRepeat循環產生li標簽,todo in todos | filter:statusFilter track by $index,循環todos,用statusFilter過濾,用$index追蹤。ngClass綁定了兩個class,{completed: todo.completed, editing: todo == editedTodo},如果todo.completed為true,添加completed class,如果todo==editedTodo,則添加editing class。class為toggle的checkbox綁定到todo.completed。todo標題展示的label綁定了雙擊事件,雙擊觸發editTodo(todo),editTodo會將todo賦給editedTodo,然后會觸發下面form中的todoFocus指令,這時候form中的input可見。按Esc就觸發revertEditing(todo),恢復到編輯前,按Enter或者失去焦點就觸發doneEditing(todo) ,保存編輯后的todo。class為destroy的button綁定了click事件,點擊觸發removeTodo(todo),刪除掉該條todo。
最后看todos的統計信息展示的html
<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: location.path() == '/'} " href="#/">All</a> </li> <li> <a ng-class="{selected: location.path() == '/active'}" href="#/active">Active</a> </li> <li> <a ng-class="{selected: location.path() == '/completed'}" href="#/completed">Completed</a> </li> </ul> <button id="clear-completed" ng-click="clearCompletedTodos()" ng-show="completedCount">Clear completed ({{completedCount}})</button> </footer>
ng-pluralize標簽實現了當remainingCount個數為1時,顯示 item left,否則顯示 items left。
id為filters的ul標簽中根據location.path()的內容不同,標記不同的a標簽被選中。
id為clear-completed的button添加了點擊事件,觸發clearCompletedTodos(),清除掉所有已完成的todo。
分析到此結束,如有錯誤,或者有不明白的地方,請留言~~
