一、整体结构

项目github地址https://github.com/tastejs/todomvc/

排除通用的css样式文件和引用的js库文件,仅看html和js

1.1 knockoutjs版todo app文件结构

knockoutjs
--index.html
--js
----app.js 

1.2 backbonejs版todo app文件结构

backbonejs
--index.html
--js
----collections
------todos.js
----models
------todo.js
----routers
------router.js
----views
------app-view.js
------todo-view.js
----app.js 

1.3 angularjs版todo app文件结构

angularjs
--index.html
--js
----controllers
------todoCtrl.js
----directives
------todoEscape.js
----services
------todoStorage.js
----app.js

二、knockout版todo主要内容

knockout版todo app实现细节,之前有文讲过,详情见《用KnockoutJS实现ToDoMVC代码分析》

从上文的文件结构可知,其业务代码只有app.js,html view只有index.html

2.1 视图代码index.html

knockout在html原有属性基础上,新增了data-bind属性

data-bind属性作为knockout与html交互的入口,内置了如下若干指令

  • visible binding
  • text binding
  • html binding
  • css binding
  • style binding
  • attr binding

除了上述内置指令,knockout也可以添加自定义指令,如html中出现的enterKey、escapeKey和selectAndFocus指令

<section id="todoapp">
<header id="header">
<h1>todos</h1>
<input id="new-todo" data-bind="value: current, valueUpdate: 'afterkeydown', enterKey: add" placeholder="What needs to be done?" autofocus>
</header>
<section id="main" data-bind="visible: todos().length">
<input id="toggle-all" data-bind="checked: allCompleted" type="checkbox">
<label for="toggle-all">Mark all as complete</label>
<ul id="todo-list" data-bind="foreach: filteredTodos">
<li data-bind="css: { completed: completed, editing: editing }">
<div class="view">
<input class="toggle" data-bind="checked: completed" type="checkbox">
<label data-bind="text: title, event: { dblclick: $root.editItem }"></label>
<button class="destroy" data-bind="click: $root.remove"></button>
</div>
<input class="edit" data-bind="value: title, valueUpdate: 'afterkeydown', enterKey: $root.saveEditing, escapeKey: $root.cancelEditing, selectAndFocus: editing, event: { blur: $root.stopEditing }">
</li>
</ul>
</section>
<footer id="footer" data-bind="visible: completedCount() || remainingCount()">
<span id="todo-count">
<strong data-bind="text: remainingCount">0</strong>
<span data-bind="text: getLabel(remainingCount)"></span> left
</span>
<ul id="filters">
<li>
<a data-bind="css: { selected: showMode() == 'all' }" href="#/all">All</a>
</li>
<li>
<a data-bind="css: { selected: showMode() == 'active' }" href="#/active">Active</a>
</li>
<li>
<a data-bind="css: { selected: showMode() == 'completed' }" href="#/completed">Completed</a>
</li>
</ul>
<button id="clear-completed" data-bind="visible: completedCount, click: removeCompleted">Clear completed</button>
</footer>
</section>

2.2 业务代码app.js

app.js中,首先对html view中自定义的指令enterKey、escapeKey和selectAndFocus做了定义

var ENTER_KEY = 13;
var ESCAPE_KEY = 27; // A factory function we can use to create binding handlers for specific
// keycodes.
function keyhandlerBindingFactory(keyCode) {
return {
init: function (element, valueAccessor, allBindingsAccessor, data, bindingContext) {
var wrappedHandler, newValueAccessor; // wrap the handler with a check for the enter key
wrappedHandler = function (data, event) {
if (event.keyCode === keyCode) {
valueAccessor().call(this, data, event);
}
}; // create a valueAccessor with the options that we would want to pass to the event binding
newValueAccessor = function () {
return {
keyup: wrappedHandler
};
}; // call the real event binding's init function
ko.bindingHandlers.event.init(element, newValueAccessor, allBindingsAccessor, data, bindingContext);
}
};
} // a custom binding to handle the enter key
ko.bindingHandlers.enterKey = keyhandlerBindingFactory(ENTER_KEY); // another custom binding, this time to handle the escape key
ko.bindingHandlers.escapeKey = keyhandlerBindingFactory(ESCAPE_KEY); // wrapper to hasFocus that also selects text and applies focus async
ko.bindingHandlers.selectAndFocus = {
init: function (element, valueAccessor, allBindingsAccessor, bindingContext) {
ko.bindingHandlers.hasFocus.init(element, valueAccessor, allBindingsAccessor, bindingContext);
ko.utils.registerEventHandler(element, 'focus', function () {
element.focus();
});
},
update: function (element, valueAccessor) {
ko.utils.unwrapObservable(valueAccessor()); // for dependency
// ensure that element is visible before trying to focus
setTimeout(function () {
ko.bindingHandlers.hasFocus.update(element, valueAccessor);
}, 0);
}
};

然后定义了todo model

// represent a single todo item
var Todo = function (title, completed) {
this.title = ko.observable(title);
this.completed = ko.observable(completed);
this.editing = ko.observable(false);
};

ViewModel中定义了html view中的业务方法和属性

// our main view model
var ViewModel = function (todos) {
// map array of passed in todos to an observableArray of Todo objects
this.todos = ko.observableArray(todos.map(function (todo) {
return new Todo(todo.title, todo.completed);
})); // store the new todo value being entered
this.current = ko.observable(); this.showMode = ko.observable('all'); this.filteredTodos = ko.computed(function () {
switch (this.showMode()) {
case 'active':
return this.todos().filter(function (todo) {
return !todo.completed();
});
case 'completed':
return this.todos().filter(function (todo) {
return todo.completed();
});
default:
return this.todos();
}
}.bind(this)); // add a new todo, when enter key is pressed
this.add = function () {
var current = this.current().trim();
if (current) {
this.todos.push(new Todo(current));
this.current('');
}
}.bind(this); // remove a single todo
this.remove = function (todo) {
this.todos.remove(todo);
}.bind(this); // remove all completed todos
this.removeCompleted = function () {
this.todos.remove(function (todo) {
return todo.completed();
});
}.bind(this); // edit an item
this.editItem = function (item) {
item.editing(true);
item.previousTitle = item.title();
}.bind(this); // stop editing an item. Remove the item, if it is now empty
this.saveEditing = function (item) {
item.editing(false); var title = item.title();
var trimmedTitle = title.trim(); // Observable value changes are not triggered if they're consisting of whitespaces only
// Therefore we've to compare untrimmed version with a trimmed one to chech whether anything changed
// And if yes, we've to set the new value manually
if (title !== trimmedTitle) {
item.title(trimmedTitle);
} if (!trimmedTitle) {
this.remove(item);
}
}.bind(this); // cancel editing an item and revert to the previous content
this.cancelEditing = function (item) {
item.editing(false);
item.title(item.previousTitle);
}.bind(this); // count of all completed todos
this.completedCount = ko.computed(function () {
return this.todos().filter(function (todo) {
return todo.completed();
}).length;
}.bind(this)); // count of todos that are not complete
this.remainingCount = ko.computed(function () {
return this.todos().length - this.completedCount();
}.bind(this)); // writeable computed observable to handle marking all complete/incomplete
this.allCompleted = ko.computed({
//always return true/false based on the done flag of all todos
read: function () {
return !this.remainingCount();
}.bind(this),
// set all todos to the written value (true/false)
write: function (newValue) {
this.todos().forEach(function (todo) {
// set even if value is the same, as subscribers are not notified in that case
todo.completed(newValue);
});
}.bind(this)
}); // helper function to keep expressions out of markup
this.getLabel = function (count) {
return ko.utils.unwrapObservable(count) === 1 ? 'item' : 'items';
}.bind(this); // internal computed observable that fires whenever anything changes in our todos
ko.computed(function () {
// store a clean copy to local storage, which also creates a dependency on the observableArray and all observables in each item
localStorage.setItem('todos-knockoutjs', ko.toJSON(this.todos));
alert(1);
}.bind(this)).extend({
rateLimit: { timeout: 500, method: 'notifyWhenChangesStop' }
}); // save at most twice per second
};

定义完成后,通过下述代码,将ViewModel和view绑定起来

// bind a new instance of our view model to the page
var viewModel = new ViewModel(todos || []);
ko.applyBindings(viewModel);

存储使用的是localStorage

// check local storage for todos
var todos = ko.utils.parseJson(localStorage.getItem('todos-knockoutjs'));

页面链接路由用到了第三方插件

// set up filter routing
/*jshint newcap:false */
Router({ '/:filter': viewModel.showMode }).init();

三、backbone版todo主要内容

从前文的文件结构中可以发现,backbone版todo app包含index.html和collection部分、model部分、router部分、view部分这些子模块js以及主模块app.js

3.1 html文件index.html

<section id="todoapp">
<header id="header">
<h1>todos</h1>
<input id="new-todo" placeholder="What needs to be done?" autofocus>
</header>
<section id="main">
<input id="toggle-all" type="checkbox">
<label for="toggle-all">Mark all as complete</label>
<ul id="todo-list"></ul>
</section>
<footer id="footer"></footer>
</section>
<footer id="info">
<p>Double-click to edit a todo</p>
<p>Written by <a href="https://github.com/addyosmani">Addy Osmani</a></p>
<p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
</footer>
<script type="text/template" id="item-template">
<div class="view">
<input class="toggle" type="checkbox" <%= completed ? 'checked' : '' %>>
<label><%- title %></label>
<button class="destroy"></button>
</div>
<input class="edit" value="<%- title %>">
</script>
<script type="text/template" id="stats-template">
<span id="todo-count"><strong><%= remaining %></strong> <%= remaining === 1 ? 'item' : 'items' %> left</span>
<ul id="filters">
<li>
<a class="selected" href="#/">All</a>
</li>
<li>
<a href="#/active">Active</a>
</li>
<li>
<a href="#/completed">Completed</a>
</li>
</ul>
<% if (completed) { %>
<button id="clear-completed">Clear completed (<%= completed %>)</button>
<% } %>
</script>

index.html中主要部分内容很简洁,上述片段中还包含了两个模板的定义,如果只看html部分,内容更少

<section id="todoapp">
<header id="header">
<h1>todos</h1>
<input id="new-todo" placeholder="What needs to be done?" autofocus>
</header>
<section id="main">
<input id="toggle-all" type="checkbox">
<label for="toggle-all">Mark all as complete</label>
<ul id="todo-list"></ul>
</section>
<footer id="footer"></footer>
</section>

上述html中,只有最基本的html元素和属性

backbone没有对html添加扩展属性,对html是没有侵入的

todo对象的列表,也页面底部的状态过滤链接,是通过view template插入到html中的

3.2 各个js文件分析

app.js作为backbone 业务代码主模块,内容很简单,在页面加载完之后,对AppView进行了实例化

/*global $ */
/*jshint unused:false */
var app = app || {};
var ENTER_KEY = 13;
var ESC_KEY = 27; $(function () {
'use strict'; // kick things off by creating the `App`
new app.AppView();
});

app-view.js是应用顶层的view,处理的对象是todo model的集合

在app-view.js代码中,首先指定了视图的作用对象和模板对象

然后在events对象中,为dom元素特定事件绑定事件处理函数

在initialize对象中,为todos集合绑定特定事件的事件处理函数

在render函数中,用模板对象渲染指定dom元素

随后依次定义事件处理函数

和app-view.js不同,todo-view.js是负责处理todo list中单个todo对象的dom处理

todo-view.js中代码过程与app-view.js中大致相似

更多view内容可参考What is a view?

todo.js定义了todo对象模型,而todos.js中定义了todo对象模型的集合

前文knockout版本todo app中,也有相应的todo对象和todos对象集合

相比knockout版本中的对象和集合,backbone版本中独立出model和collection模块的意义是什么呢

答案是backbone中model和collection功能比knockout中丰富的多

model是js应用的核心,包括基础的数据以及围绕着这些数据的逻辑:数据转换、验证、属性计算和访问控制

collection是model对象的集合,为model对象提供便捷的操作。在我看来,collection不是必须的,他属于语法糖类型的东西。

更多model和collection内容可以参考

Backbone入门指南(四):Model(数据模型)
Backbone入门指南(五):Collection (数据模型集合)

router.js是根据backbone内置的路由模块实现的路由处理,根据All、Active、Completed三个不同链接,进行不同操作

router使用可以参考认识 Backbone(三) : 什么是 Router

四、angular版todo主要内容

angular版本todo app包含index.html view文件和controller部分、director部分、service部分和主入口app.js

4.1 index.html分析

<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 ({{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>

查看index.html发现,body元素下,第一行元素为

<ng-view />

随后,在脚本<script type="text/ng-template" id="todomvc-index.html"></script>中,定义了app的html

html属性中,看到很多ng开头的属性,如ng-app,ng-submit,ng-model等

这些属性,都是angular对html的扩展,而上述属性中大部分是angular内置的指令

todo-escape,todo-focus这两个不是以ng开头的指令,是app自定义的指令

对ng-view指令的用法,更多内容可参考AngularJS Views

4.2 js业务代码分析

angular程序的启动开始于ng-app指令,他的位置也决定了脚本的作用域范围

<body ng-app="todomvc">

这里注册的todomvc模块,与app.js中定义的模块是一致的

angular.module('todomvc', ['ngRoute'])
.config(function ($routeProvider) {
'use strict'; 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: '/'
});
});

应用程序入口,app.js中,定义了todomvc模块,引入了ngRoute模块

程序中,采用$routeProvider服务对页面路由进行了配置,指定链接对应的配置中,控制器是 TodoCtrl,模板地址是todomvc-index.html,定义了resolve对象,根据todoStorage服务,获取todos集合,填充store对象

关于这里的路由配置中,配置对象和resolve用法,可以参考Promise/Q和AngularJS中的resolve

todoCtrl.js是应用的控制器部分,控制器是和应用的与对象scope交互的地方,可以将一个独立视图的业务逻辑封装在一个独立的容器中。

index.html的模板中,涉及的属性和方法,都是在todoCtrl.js中定义的

todoFocus.js和todoEscape.js是两个自定义指令,对应todo-focus和todo-escape

这里的自定义指令,实际上可以对应到knockout的custom binding,均是对内置指令的扩展

对指令的使用可参考《AngularJS》5个实例详解Directive(指令)机制

todoStorage.js是应用的服务部分,服务部分提供了http服务和localStorage两种方式,并且提供的是promise的异步处理方式

对promise的介绍和对angular中$q服务的介绍可以参考

五、总结

单以此todo app来看knockout、backbone和angular

文件结构上

knockout最简洁,angular其次,backbone最复杂

对html侵入上

backbone对html无侵入,knockout增加了data-bind属性,angular增加了一套属性并提供自定义属性方法

对第三方插件依赖上

knockout不提供dom操作,不提供路由操作,提供简单的模板支持

backbone不提供dom操作,提供了路由模块,依赖underscore函数库

angular提供内置的jqlite,提供路由服务,异步处理服务,依赖服务

代码分离角度

个人认为backbone和angular都比较清晰,knockout一般

以todomvc为例分析knockout、backbone和angularjs的更多相关文章

  1. Psp个人软件开发软件需求分析和用例分析

    Psp个人软件开发软件需求分析和用例分析 一.需求分析 1.业务需求 1.1 应用背景 开发项目进度计划总是那么不明确,延期经常出现,甚至无法给出一个相对比较明确的延迟时间.这样给市场的推广会带来很大 ...

  2. K米APP----案例分析

    K米APP----案例分析 第一部分 调研,评测 第一次上手体验 软件的美工做得不错,功能排版很清楚,用户很容易上手,不至于用户不知道怎么用这个APP点歌 软件最主要的功能是KTV的点歌功能,这个功能 ...

  3. Psp个人软件开发软件需求分析及用例分析

    一.需求分析 1.  业务需求 1.1 应用背景 开发项目进度计划总是那么不明确,延期经常出现,甚至无法给出一个相对比较明确的延迟时间.这样给市场的推广会带来很大的影响,不确定因素使得应对十分困难. ...

  4. [转][LoadRunner]LR性能测试结果样例分析

    LR性能测试结果样例分析 测试结果分析 LoadRunner性能测试结果分析是个复杂的过程,通常可以从结果摘要.并发数.平均事务响应时间.每秒点击数.业务成功率.系统资源.网页细分图.Web服务器资源 ...

  5. 需求用例分析之五:业务用例之Rational系

    版权声明:作者:张克强.未经作者允许不得转载. https://blog.csdn.net/zhangmike/article/details/28134897 作者:张克强    作者微博:张克强- ...

  6. [LoadRunner]LR性能测试结果样例分析

    R性能测试结果样例分析 测试结果分析 LoadRunner性能测试结果分析是个复杂的过程,通常可以从结果摘要.并发数.平均事务响应时间.每秒点击数.业务成功率.系统资源.网页细分图.Web服务器资源. ...

  7. LoadRunner性能测试样例分析

    LR性能测试结果样例分析 测试结果分析 LoadRunner性能测试结果分析是个复杂的过程,通常可以从结果摘要.并发数.平均事务响应时间.每秒点击数.业务成功率.系统资源.网页细分图.Web服务器资源 ...

  8. Makefile 实际用例分析(二) ------- 比较通用的一种架构

    之前已经讲了这一篇文章:Makefile实际用例分析(一)-----比较通用的一种架构 现在这篇其实和那个差的不是很多,只是在布局上有些差别(这个makefile也是论坛上一起讨论过的,囧,忘了哪个论 ...

  9. Backbone vs AngularJS

    首先 Backbone 没有 AngularJS 那么容易上手. 而且作者并没有想让Backbone草根化的意思. Backbone 比喻成战斗机. 看上去更像是真正的MVC框架, model-vie ...

随机推荐

  1. K. Perpetuum Mobile

    The world famous scientist Innokentiy almost finished the creation of perpetuum mobile. Its main par ...

  2. ADS1.2安装

    一.ADS1.2的安装 1. 解压 2. 双击打开ads1.2 3.我们选择当中的SETUP.EXE文件,进行安装 4.点击Next: 5.这是许可文件,假设允许的话选择Yes: 6.选择安装文件夹, ...

  3. 百度贴吧客户端(Android)网络通信行为分析

    百度贴吧安卓客户端网络通信行为分析 本文由CSDN-蚍蜉撼青松[主页:http://blog.csdn.net/howeverpf]原创,转载请注明出处! 一.实验环境与结果概述 1.1 实验环境   ...

  4. Windows 编程之 对话框总结

    关于对话框 1 对话框种类 对话框也是一种资源,Windows中对话框分为模态的和非模态的,以及Windows系统中已经定义好的那些通用对话框,比方打开文件,目录,调色板,字符串查找等. 2 模态对话 ...

  5. SetBkMode可设置文字背景色:TRANSPARENT或OPAQUE

    感受一下区别: procedure TForm1.Timer2Timer(Sender: TObject); var cvs: TCanvas; Rect: TRect; Str: string; b ...

  6. 一劳永逸解决UAC问题(修改QMAKE_LFLAGS_EXE的设置)

    如果你的程序跑在一个开启了UAC保护的系统中,而你的程序又没有"盾牌"的话,程序总是会受到各种阻挠的,比如读写文件,写注册表等. 有了"盾牌"的话就不会出现一些 ...

  7. perl的一些基本用法

    ReadLine support available (try 'install Bundle::CPAN')cpan>进入cpan的shell,好了,我为了安装spamassassin,需要安 ...

  8. android中Logcat的深层理解

    Android的开发也能够归类为嵌入式设备的开发.即便不是嵌入式开发,依旧要注意对内存和处理的使用.养成一个好的习惯对自己的帮助是非常大的. 在Log的源代码中能够看到这种凝视: The order ...

  9. HTML属性

  10. Hibernate(六)——多对多关联映射

    前面几篇文章已经较讲解了三大种关联映射,多对多映射就非常简单了,不过出于对关联映射完整性的考虑,本文还是会简要介绍下多对多关联映射. 1.单向多对多关联映射 情景:一个用户可以有多个角色,比如数据录入 ...