AngularJs-todoMVC 源码解释
github上的todoMVC仓库是一个帮助你选择前端MVC框架的项目
项目中包含了绝大多数前端MVC框架实现Todo application的范例,让你能比较不同的框架实现同一个应用的差异。进而让你做出最佳选择。
Todo application的具体效果,可以看这个http://todomvc.com/examples/angularjs/#/
对于新手来说,是个很不错的学习范例。
本文选取的是其中的angularJs范例,对其做了简单分析。
分析源码已经上传至github,https://github.com/BryanAdamss/SourceSave/tree/master/TodoMVC/angularjs
源码下载后,请在服务器中打开
目录结构
主要根据功能不同,放在了不同文件夹中
- angularjs/
- js/
- controllers/
- todoCtrl.js->最主要的一个控制器
- directives/
- todoEscape.js->实现按下esc键,恢复到原先编辑状态的指令
- todoFocus.js->再编辑input显示,聚焦的指令
- services/
- todoStorage.js->实现本地localStorge
- app.js->入口文件,包含了路由配置
- controllers/
- node_modules/
- angular/
- angular-resource/
- angular-route/
- todomvc-app-css/->页面主要样式文件
- todomvc-common/->一些通用的css样式和js helper
- index.html
- js/
index.html
相关说明全部写在注释里了
1 |
|
app.js
app.js是入口文件,主要是创建了模块,并配置了路由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/*global angular */
/**
* The main TodoMVC app module
*
* @type {angular.Module}
*/
angular.module('todomvc', ['ngRoute', 'ngResource'])
.config(['$routeProvider', function($routeProvider) {
;
var routeConfig = {
controller: 'TodoCtrl',
templateUrl: 'todomvc-index.html', // 指定模板
resolve: {
store: function(todoStorage) { // 在跳转路由之前载入正确的module
// 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: '/'
});
}]);
todoStorage.js
这个文件是一个服务,主要实现了数据在localStorge中的存储和读写
其实这一块没怎么看懂,主要是不太理解ngResource模块的作用,不过大概知道是存储和读取数据用的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
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165/*global angular */
/**
* Services that persists and retrieves todos from localStorage or a backend API
* if available.
*
* They both follow the same API, returning promises for all changes to the
* model.
*/
// 这一块是懵逼的...大概就是将数据存储在localStorage中
angular.module('todomvc')
.factory('todoStorage', function($http, $injector) {
;
// Detect if an API backend is present. If so, return the API module, else
// hand off the localStorage adapter
return $http.get('/api')
.then(function() {
return $injector.get('api');
}, function() {
return $injector.get('localStorage');
});
})
.factory('api', function($resource) {
;
var store = {
todos: [],
api: $resource('/api/todos/:id', null, {
update: { method: 'PUT' }
}),
clearCompleted: function() {
var originalTodos = store.todos.slice(0);
var incompleteTodos = store.todos.filter(function(todo) {
return !todo.completed;
});
angular.copy(incompleteTodos, store.todos);
return store.api.delete(function() {}, function error() {
angular.copy(originalTodos, store.todos);
});
},
delete: function(todo) {
var originalTodos = store.todos.slice(0);
store.todos.splice(store.todos.indexOf(todo), 1);
return store.api.delete({ id: todo.id },
function() {},
function error() {
angular.copy(originalTodos, store.todos);
});
},
get: function() {
return store.api.query(function(resp) {
angular.copy(resp, store.todos);
});
},
insert: function(todo) {
var originalTodos = store.todos.slice(0);
return store.api.save(todo,
function success(resp) {
todo.id = resp.id;
store.todos.push(todo);
},
function error() {
angular.copy(originalTodos, store.todos);
})
.$promise;
},
put: function(todo) {
return store.api.update({ id: todo.id }, todo)
.$promise;
}
};
return store;
})
.factory('localStorage', function($q) {
;
var STORAGE_ID = 'todos-angularjs';
var store = {
todos: [],
_getFromLocalStorage: function() {
return JSON.parse(localStorage.getItem(STORAGE_ID) || '[]');
},
_saveToLocalStorage: function(todos) {
localStorage.setItem(STORAGE_ID, JSON.stringify(todos));
},
clearCompleted: function() {
var deferred = $q.defer();
var incompleteTodos = store.todos.filter(function(todo) {
return !todo.completed;
});
angular.copy(incompleteTodos, store.todos);
store._saveToLocalStorage(store.todos);
deferred.resolve(store.todos);
return deferred.promise;
},
delete: function(todo) {
var deferred = $q.defer();
store.todos.splice(store.todos.indexOf(todo), 1);
store._saveToLocalStorage(store.todos);
deferred.resolve(store.todos);
return deferred.promise;
},
get: function() {
var deferred = $q.defer();
angular.copy(store._getFromLocalStorage(), store.todos);
deferred.resolve(store.todos);
return deferred.promise;
},
insert: function(todo) {
var deferred = $q.defer();
store.todos.push(todo);
store._saveToLocalStorage(store.todos);
deferred.resolve(store.todos);
return deferred.promise;
},
put: function(todo, index) {
var deferred = $q.defer();
store.todos[index] = todo;
store._saveToLocalStorage(store.todos);
deferred.resolve(store.todos);
return deferred.promise;
}
};
return store;
});
todoEscape.js
这是一个指令,主要完成按下esc键,恢复再编辑input到原先状态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/*global angular */
/**
* Directive that executes an expression when the element it is applied to gets
* an `escape` keydown event.
*/
// esc键绑定事件
// 当按下Escape键时,执行attrs.todoEscape的表达式。
angular.module('todomvc')
.directive('todoEscape', function() {
;
var ESCAPE_KEY = 27;
return function(scope, elem, attrs) { // 直接返回一个函数,实际上就是link函数;在link函数中绑定事件
elem.bind('keydown', function(event) {
if (event.keyCode === ESCAPE_KEY) { // 按下esc,触发attrs.todoEscape对应的事件
scope.$apply(attrs.todoEscape);
}
});
scope.$on('$destroy', function() { // 销毁时,解除绑定
elem.unbind('keydown');
});
};
});
todoFocus.js
这个指令主要完成再编辑input的显示和聚焦1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20/*global angular */
/**
* Directive that places focus on the element it is applied to when the
* expression it binds to evaluates to true
*/
angular.module('todomvc')
.directive('todoFocus', function todoFocus($timeout) {
;
return function(scope, elem, attrs) { // 在二次编辑的input上绑定事件
scope.$watch(attrs.todoFocus, function(newVal, oldVal) {
// 当双击时,newVal为true
if (newVal) {
$timeout(function() {
elem[0].focus();
}, 0, false);
}
});
};
});
todoCtrl.js
这是重头性,关键性逻辑全写在这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
119
120
121
122
123
124
125
126/*global angular */
/**
* 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
*/
angular.module('todomvc')
.controller('TodoCtrl', function TodoCtrl($scope, $routeParams, $filter, store) {
;
var todos = $scope.todos = store.todos; // 从localStorge中取出所有todo
$scope.newTodo = ''; // 用来保存新创建的todo
$scope.editedTodo = null; // 用来保存编辑过的todo
$scope.$watch('todos', function() { // 深度观察todos的值
$scope.remainingCount = $filter('filter')(todos, { completed: false }).length; // 更新未完成的todo数量
$scope.completedCount = todos.length - $scope.remainingCount; // 更新完成的todo数量
$scope.allChecked = !$scope.remainingCount; // 是否全部完成
}, true);
// Monitor the current route for changes and adjust the filter accordingly.
$scope.$on('$routeChangeSuccess', function() { // 观察路由跳转,并更新用来过滤的statusFilter
var status = $scope.status = $routeParams.status || '';
$scope.statusFilter = (status === 'active') ? { completed: false } : (status === 'completed') ? { completed: true } : {};
});
$scope.addTodo = function() { // 输入框提交时触发
var newTodo = { // 创建新todo
title: $scope.newTodo.trim(), //newTodo是绑定在input输入框上
completed: false
};
if (!newTodo.title) { // 空值,则不提交
return;
}
$scope.saving = true; // saving用来标识input的禁用状态,为true则禁用
store.insert(newTodo) // 插入新todo
.then(function success() { // 成功则重置newTodo
$scope.newTodo = '';
})
.finally(function() {
$scope.saving = false; // 最后取消input的禁用状态
});
};
$scope.editTodo = function(todo) { // 已添加的todo上双击时触发,会将双击的todo传入
$scope.editedTodo = todo; // 保存正在编辑的todo
// Clone the original todo to restore it on demand.
$scope.originalTodo = angular.extend({}, todo); // 保留原先的todo,以备不时之需
};
$scope.saveEdits = function(todo, event) { // 再编辑input提交或者blur时触发
// 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') { // 提交时,会自动触发一次blur,所以手动阻止
$scope.saveEvent = null;
return;
}
$scope.saveEvent = event; // 保存事件类型(blur或submit)
if ($scope.reverted) { // 如果编辑后按esc,取消了编辑,则不保存
// Todo edits were reverted-- don't save.
$scope.reverted = null;
return;
}
todo.title = todo.title.trim(); // 保存新编辑title
if (todo.title === $scope.originalTodo.title) { // title未发生改变,则不保存
$scope.editedTodo = null;
return;
}
store[todo.title ? 'put' : 'delete'](todo)
.then(function success() {}, function error() { // 保存出错,则恢复title
todo.title = $scope.originalTodo.title;
})
.finally(function() { // 最后,重置editedTodo
$scope.editedTodo = null;
});
};
$scope.revertEdits = function(todo) { // todoEscape时触发,将再编辑input恢复到编辑前的状态,会传入需要恢复的todo
todos[todos.indexOf(todo)] = $scope.originalTodo;
$scope.editedTodo = null;
$scope.originalTodo = null;
$scope.reverted = true;
};
$scope.removeTodo = function(todo) { // 删除todo
store.delete(todo);
};
$scope.saveTodo = function(todo) { // 保存todo
store.put(todo);
};
$scope.toggleCompleted = function(todo, completed) { // 切换完成状态
if (angular.isDefined(completed)) { // 如果completed曾经定义过,则直接使用
todo.completed = completed;
}
// 更新localStorge上的todo的complete
store.put(todo, todos.indexOf(todo))
.then(function success() {}, function error() { // 保存出错,则恢复
todo.completed = !todo.completed;
});
};
$scope.clearCompletedTodos = function() { // 清除所有已经完成的todo
store.clearCompleted();
};
$scope.markAll = function(completed) { // 将所有todo置为已完成
todos.forEach(function(todo) {
if (todo.completed !== completed) {
$scope.toggleCompleted(todo, completed);
}
});
};
});