以todomvc为例分析knockout、backbone和angularjs – GongQi

一、整体结构

项目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中出现的enterKeyescapeKey和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 ngsubmit=addTodo()>
<input id=new-todo placeholder=What needs to be done? ngmodel=newTodo ngdisabled=saving autofocus>
</form>
</header>
<section id=main ngshow=todos.length ngcloak>
<input id=toggle-all type=checkbox ngmodel=allChecked ngclick=markAll(allChecked)>
<label for=toggle-all>Mark all as complete</label>
<ul id=todo-list>
<li ngrepeat=todo in todos | filter:statusFilter track by $index ngclass={completed: todo.completed, editing: todo == editedTodo}>
<div class=view>
<input class=toggle type=checkbox ngmodel=todo.completed ngchange=toggleCompleted(todo)>
<label ngdblclick=editTodo(todo)>{{todo.title}}</label>
<button class=destroy ngclick=removeTodo(todo)></button>
</div>
<form ngsubmit=saveEdits(todo, ‘submit’)>
<input class=edit ngtrim=false ngmodel=todo.title todoescape=revertEdits(todo) ngblur=saveEdits(todo, ‘blur’) todofocus=todo == editedTodo>
</form>
</li>
</ul>
</section>
<footer id=footer ngshow=todos.length ngcloak>
<span id=todo-count><strong>{{remainingCount}}</strong>
<ngpluralize count=remainingCount when={ one: ‘item left’, other: ‘items left’ }></ng-pluralize>
</span>
<ul id=filters>
<li>
<a ngclass={selected: status == ”} href=#/>All</a>
</li>
<li>
<a ngclass={selected: status == ‘active’} href=#/active>Active</a>
</li>
<li>
<a ngclass={selected: status == ‘completed’} href=#/completed>Completed</a>
</li>
</ul>
<button id=clear-completed ngclick=clearCompletedTodos() ngshow=completedCount>Clear completed ({{completedCount}})</button>
</footer>
</section>
<footer id=info>
<p>Doubleclick 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服务的介绍可以参考

卡通方式趣解 AngularJS 中的 promise

五、总结

单以此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,转载请注明。



You must enable javascript to see captcha here!

Copyright © All Rights Reserved · Green Hope Theme by Sivan & schiy · Proudly powered by WordPress

无觅相关文章插件,快速提升流量