无框架高治理 - 第二部分





5.00/5 (5投票s)
使用高级开发人员治理来实现一个无框架的客户端应用程序。
引言
本三部分系列文章《无框架高治理》的第二部分,通过在 第一部分 中描述的高治理,展示了如何将 TodoMVC 实现为一个无框架的应用程序。
背景
第一部分 本三部分系列文章探讨了开发无框架应用程序的背景、动机和架构方法。 第二部分 展示了一个无框架应用程序的实现。 第三部分 反驳了反对无框架应用程序开发的论点。
TodoMVC 实现
实现类建立在上一节介绍的基础类之上。Todos
Presenter继承自基类Controller
,该类提供了一个极其简单的路由机制。Todos
封装了一个TodoView
对象来管理视图,以及一个TodoModel
对象来管理数据。它还包含一个TodoConfig
对象,用于检索设置和其他配置值。

Todos
类实现封装了其内部依赖项,而不是像VanillaJS示例那样将依赖项注入到类中。封装遵循面向对象原则,即隐藏数据免受客户端影响并本地化更改。
此外,Todos
实例的创建与其初始化是分开的。这是因为创建是分配表示实例的内存的过程,而初始化是获取资源的行为。创建始终是同步的。另一方面,初始化根据资源获取的方式,同步或异步发生。
/*jshint strict: true, undef: true, eqeqeq: true */
/* globals console, Promise, Subscriber, Controller, TodoConfig, TodoModel, TodoView */
/**
* The todos controller.
*
* @class
*/
function Todos() {
"use strict";
this.inherit(Todos, Controller);
var self = this,
settings = new TodoConfig(),
view = new TodoView(),
model = new TodoModel();
/**
* Initialize instance.
*/
function initialize() {
view.on(subscribers);
view.render(view.commands.initContent, settings);
self.$base.init.call(self, router);
}
/**
* Display the remaining number of todo items.
*/
function showStats() {
model.getStats(function(stats) {
view.render(view.commands.showStats, stats);
view.render(view.commands.toggleAll, (stats.completed === stats.total));
});
}
/**
* Initialize the todos controller.
*
* @returns {Promise} Resource acquisition promise.
*/
this.init = function() {
// Chained initialization: settings->model->view->initialize.
return settings.init()
.then(function() {
return model.init();
}).then(function() {
return view.init();
}).then(initialize);
// Parallelize initialization: (settings||model||view)->initialize.
// Comment the chained implementation then uncomment the section below.
// return Promise.all([settings.init(),
// model.init(),
// view.init()])
// .then(initialize);
};
/**
* Set the application state.
*
* @param {string} '' | 'active' | 'completed'
*/
this.stateChanged = function() {
this.executeState();
view.render(view.commands.setFilter, this.getHyperlink());
showStats();
};
/**
* Router is the receiver of events that changes the application state.
*/
var router = {
/**
* Renders all todo list items.
*/
default: function () {
model.find(function (results) {
view.render(view.commands.showEntries, results);
});
},
/**
* Renders all active tasks
*/
active: function () {
model.find({ completed: false }, function (results) {
view.render(view.commands.showEntries, results);
});
},
/**
* Renders all completed tasks
*/
completed: function () {
model.find({ completed: true }, function (results) {
view.render(view.commands.showEntries, results);
});
}
};
/**
* Subscriber of view events.
*/
var subscribers = {
/**
* Adds a new todo item to the todo list.
*/
todoAdd: new Subscriber(this, function (title) {
// Add item.
if (title.trim() === '') {
return;
}
model.add(title, new Subscriber(this, function () {
view.render(view.commands.clearNewTodo);
this.stateChanged();
}));
}),
/*
* Starts the todo item editing mode.
*/
todoEdit: new Subscriber(this, function (id) {
model.find(id, function (results) {
view.render(view.commands.editItem, id, results[0].title);
});
}),
/*
* Saves edited changes to the todo item.
*/
todoEditSave: new Subscriber(this, function (id, title) {
if (title.length !== 0) {
model.save({id: id, title: title}, function (item) {
view.render(view.commands.editItemDone, item.id, item.title);
});
} else {
subscribers.todoRemove(id);
}
}),
/*
* Cancels the todo item editing mode and restore previous value.
*/
todoEditCancel: new Subscriber(this, function (id) {
model.find(id, function (results) {
view.render(view.commands.editItemDone, id, results[0].title);
});
}),
/**
* Removes the todo item.
*/
todoRemove: new Subscriber(this, function (id, silent) {
model.remove(id, function () {
view.render(view.commands.removeItem, id);
});
if (!silent)
showStats();
}),
/**
* Removes all completed items todo items.
*/
todoRemoveCompleted: new Subscriber(this, function () {
model.find({ completed: true }, function (results) {
results.forEach(function (item) {
subscribers.todoRemove(item.id, true);
});
});
showStats();
}),
/**
* Toggles the completion of a todo item.
*/
todoToggle: new Subscriber(this, function (viewdata, silent) {
model.save(viewdata, function (item) {
view.render(view.commands.completedItem, item.id, item.completed);
});
if (!silent)
showStats();
}),
/**
* Toggles completion of all todo items.
*/
todoToggleAll: new Subscriber(this, function (completed) {
model.find({ completed: !completed }, function (results) {
results.forEach(function (item) {
subscribers.todoToggle({id: item.id,
title: item.title,
completed: completed},
true);
});
});
showStats();
})
};
}
Todos
类定义了一个router
对象,其属性代表default
、active
和completed
这三种演示模式。default
状态显示活动和已完成待办事项列表;active
状态显示活动待办事项列表;completed
状态显示已完成待办事项列表。subscribers
对象将事件消息定义为属性,这些属性具有由视图触发的相应事件处理程序。
Todos
类封装命令和事件。router
类的状态被定义为命令。init
方法注册Todos
类的命令。subscribers
对象定义了Todos
类的事件消息和事件处理程序。view.on
方法将Todos
类订阅者附加到触发事件的TodoView
类。
TodoMVC 模型
在MVP架构中,Todos
Presenter控制模型状态,因为Todos
会启动模型的所有操作。因此,TodoMVC应用程序不需要模型触发事件,尽管基类Model
支持这一点。TodoModel
类继承自Model类,并使用Storage
类来执行从localStorage
的数据访问的繁重工作。

待办事项列表会保存在浏览器本地存储中。TodoMVC使用的存储策略不是为每个待办事项存储一个键,而是将待办事项列表集合作为序列化的JSON数组进行持久化。这会将持久化减少到本地存储中的一个集合。

TodoMVC使用的查询仅限于按ID查找特定待办事项,以及查询活动或已完成待办事项列表。如果需要,可以增强Storage
类来处理更复杂的查询,类似于MongoDB的风格。
/*jshint strict:true, undef:true, eqeqeq:true, laxbreak:true */
/* globals $, console, document, Model, Subscriber, Storage, TodoItem */
/**
* The todos storage model.
*
* @class
*/
function TodoModel() {
"use strict";
this.inherit(TodoModel, Model);
var self = this,
DBNAME = 'todos';
/**
* Initialize the model.
*
* @returns {Promise} Resource acquisition promise.
*/
this.init = function() {
return self.$base.init.call(self, DBNAME);
};
/**
* Create new todo item
*
* @param {string} [title] The title of the task
* @param {function} [callback] The callback to fire after the model is created
*/
this.add = function(title, callback) {
title = title || '';
var todo = new TodoItem(title);
self.$base.add.call(self, todo, callback);
};
/**
* Returns a count of all todos
*/
this.getStats = function(callback) {
var results = self.$base.getItems.call(self);
var stats = { active: 0, completed: 0, total: results.length};
results.forEach(function(item) {
if (item.value.completed) {
stats.completed++;
} else {
stats.active++;
}
});
callback(stats);
};
/**
* Updates a model by giving it an ID, data to update, and a callback to fire when
* the update is completed.
*
* @param {object} entity The properties to update and their new value
* @param {function} callback The callback to fire when the update is complete.
*/
this.save = function(entity, callback) {
var todo = new TodoItem(entity.id, entity.title, entity.completed);
self.$base.save.call(self, todo, callback);
};
}
TodoMVC 演示
演示系统协调Todos
Presenter与TodoView
和TodoTemplate
类。Todos
创建TodoView
实例,该实例使用Todos
事件订阅者进行初始化。TodoView
接收用户事件并向用户显示信息。TodoView
创建TodoTemplate
,后者从模板化内容构建元素。

模板化
模板是动态创建并渲染到HTML中的内容片段,而不是在视图上静态渲染。模板引擎将模板转换为HTML内容。基类Template
类使用Handlebars模板引擎将模板转换为HTML内容。
/*jshint strict: true, undef: true, laxbreak:true */
/* globals $, console, document, window, HTMLElement, Promise, Handlebars */
/**
* The base template class.
*
* @class
*/
function Template() {
"use strict";
this.inherit(Template);
var noop = function() {},
templateCache = Object.create(null);
/**
* Converts relative url path to absolute url path.
*
* @param {string} url relative url path.
*
* @returns {string} Absolute url.
*/
function getAbsoluteUrl(relativeUrl) {
var prefixIndex = window.location.href.lastIndexOf('/'),
prefix = window.location.href.slice(0, prefixIndex+1);
return prefix + relativeUrl;
}
/**
* Load the template cache from the source properties.
*
* @param {object} source Template source object.
*
* @returns {Promise} Promise object.
*/
function loadTemplateFromObject(source) {
return new Promise(function(resolve, reject) {
try {
Object.getOwnPropertyNames(source).forEach(function(name) {
templateCache[name] = Handlebars.compile(source[name]);
});
if (Object.getOwnPropertyNames(templateCache).length > 0) {
resolve();
} else {
reject({message: 'Cannot find template object'});
}
}
catch(e) {
reject(e);
}
});
}
/**
* Load the template cache from the DOM.
*
* @param {jquery} source DOM element containing templates.
*
* @returns {Promise} Promise object.
*/
function loadTemplateFromElement(source) {
return new Promise(function(resolve, reject) {
try {
source.children().each(function(index, element) {
var name = element.getAttribute('id').replace('template-', '');
templateCache[name] = Handlebars.compile(element.innerHTML);
});
if (Object.getOwnPropertyNames(templateCache).length > 0) {
resolve();
} else {
reject({message: 'Cannot find template source: (' + source.selector + ')'});
}
}
catch(e) {
reject(e);
}
});
}
/**
* Retrieve templates from url.
*
* @param {string} source The url of the tmeplates.
*
* @returns {Promise} Promise object.
*/
function loadTemplateFromUrl(source) {
var lastSeparator = source.lastIndexOf('.'),
name = source.substr(0, lastSeparator),
ext = source.substr(lastSeparator) || '.html';
return new Promise(function(resolve, reject) {
try {
// load template file.
$.ajax({
url: name + ext,
dataType: 'text'
})
.done(function(data) {
// find the template section.
var templateSection = $('#template-section');
if (!templateSection.length) {
templateSection = $(document.createElement('section'));
templateSection.attr('id', 'template-section');
}
templateSection.append($.parseHTML(data));
templateSection.children().each(function(index, element) {
var name = element.getAttribute('id').replace('template-', '');
templateCache[name] = Handlebars.compile(element.innerHTML);
});
templateSection.empty();
resolve();
})
.fail(function(xhr, textStatus, errorThrown) {
reject({xhr: xhr,
message: 'Cannot load template source: (' + getAbsoluteUrl(name + ext) + ')',
status: textStatus});
});
}
catch(e) {
reject(e);
}
});
}
/**
* Retrieve templates from url.
*
* @param {$|HTMLElement|object|string} source Template source.
* @param {function} callback Loader callback.
*
* @returns {Promise} Promise object.
*/
function loadTemplate(source) {
if (source instanceof $) {
return loadTemplateFromElement(source);
} else if (source instanceof HTMLElement) {
return loadTemplateFromElement($(source));
} else if (typeof source === "string") {
return loadTemplateFromUrl(source);
} else {
return loadTemplateFromObject(source);
}
}
/**
* Retrieves the template by name.
*
* @param {string} name template name.
*/
function getTemplate(name) {
return templateCache[name];
}
/**
* Initialize the template
*
* @param {$|HTMLElement|object|string} source Template source.
*/
this.init = function(source) {
var self = this;
return loadTemplate(source)
.then(
function (data) {
Object.getOwnPropertyNames(templateCache).forEach(function(name) {
Object.defineProperty(self, name, {
get: function() { return name; },
enumerable: true,
configurable: false
});
});
});
};
/**
* Create text using the named template.
*
* @param {string} name Template name.
* @param {object} data Template data.
*
* @returns {string} text.
*/
this.createTextFor = function(name, data) {
if (!name) return;
var template = getTemplate(name);
return template(data);
};
/**
* Create element using the named template.
*
* @param {string} name Template name.
* @param {object} data Template data.
*
* @returns {$} jQuery element.
*/
this.createElementFor = function(name, data) {
var html = this.createTextFor(name, data);
var d = document.createElement("div");
d.innerHTML = html;
return $(d.children);
};
}
TodoTemplate
支持三种模板策略
-
在对象中定义的模板。
-
在HTML中定义的模板
-
在服务器上定义的模板。
TodoTemplate
的init
方法中使用的source
参数决定了用于检索模板的策略。如果源是元素,则从HTML中获取模板。如果源是对象,则从对象的属性中检索模板。如果源是字符串,则假定它是URL路径,并从服务器检索模板。
在对象中定义的模板。
使用此策略,模板被定义为一个对象。每个单独的模板被标识为对象的属性。
/*jshint strict:true, undef:true, eqeqeq:true, laxbreak:true */
/*global $, console, Template */
function TodoTemplate() {
'use strict';
this.inherit(TodoTemplate, Template);
var self = this;
/**
* Initialize instance.
*/
function initialize(source) {
return self.$base.init.call(self, source)
.catch(
function (reason) {
console.log('Template cache load failure: ' + reason.message);
});
}
/**
* Template object.
*/
var templates = {
content: ''
+ '<header class="header">'
+ '<h1>todos</h1>'
+ '<input class="new-todo" placeholder="{{placeholder}}" autofocus>'
+ '</header>'
+ '<section class="workspace">'
+ '<section class="main">'
+ '<input class="toggle-all" type="checkbox">'
+ '<label for="toggle-all">{{markall}}</label>'
+ '<ul class="todo-list"></ul>'
+ '</section>'
+ '<section class="menu">'
+ '<span class="todo-count"></span>'
+ '<ul class="filters">'
+ '<li>'
+ '<a href="#/" class="selected">{{default}}</a>'
+ '</li>'
+ '<li>'
+ '<a href="#/active">{{active}}</a>'
+ '</li>'
+ '<li>'
+ '<a href="#/completed">{{completed}}</a>'
+ '</li>'
+ '</ul>'
+ '<button class="clear-completed">{{clear}}</button>'
+ '</section>'
+ '</section>',
listitem: ''
+ '<li data-id="{{id}}" class="{{completed}}">'
+ '<div class="view">'
+ '<input class="toggle" type="checkbox" {{checked}}>'
+ '<label>{{title}}</label>'
+ '<button class="destroy"></button>'
+ '</div>'
+ '</li>',
summary: '<span><strong>{{count}}</strong> item{{plural}} left</span>'
};
/**
* init()
*
* initialize templates from source.
*
* @returns {Promise} Promise used to acquire templates.
*/
this.init = function() {
return initialize(templates);
//return initialize($('#templates'));
//return initialize('template/templates.html');
};
}
嵌入HTML中的模板。
当前浏览器版本支持<template>
元素,作为将模板直接嵌入HTML的方式。 对于旧版浏览器,开发人员可以使用<script>
元素作为模板内容的替代品。 TodoMVC旨在在支持<template>
元素的浏览器版本上运行。 为保持唯一标识符,模板名称前会加上template-
。
<!doctype html> <html lang="en" data-framework="javascript"> <head> <meta charset="utf-8"> <title>TodoMVC</title> <link rel="stylesheet" href="css/base.css"> <link rel="stylesheet" href="css/index.css"> </head> <body> <section class="todoapp"> </section> <footer class="info"> <p>Double-click to edit a todo</p> <p>Created by <a href="http://twitter.com/oscargodson">Oscar Godson</a></p> <p>Refactored by <a href="https://github.com/cburgmer">Christoph Burgmer</a></p> <p>Part of <a href="http://todomvc.com">TodoMVC</a></p> </footer> <!-- ----------------------------------------------------------------------------- --> <!-- Content templates --> <!-- ----------------------------------------------------------------------------- --> <section id="templates"> <!-- Content template --> <template id="template-content"> <header class="header"> <h1>todos</h1> <input class="new-todo" placeholder="{{placeholder}}" autofocus> </header> <section class="workspace"> <section class="main"> <input class="toggle-all" type="checkbox"> <label for="toggle-all">{{markall}}</label> <ul class="todo-list"></ul> </section> <section class="menu"> <span class="todo-count"></span> <ul class="filters"> <li> <a href="#/" class="selected">{{default}}</a> </li> <li> <a href="#/active">{{active}}</a> </li> <li> <a href="#/completed">{{completed}}</a> </li> </ul> <button class="clear-completed">{{clear}}</button> </section> </section> </template> <!-- Todo list item template --> <template id="template-listitem"> <li data-id="{{id}}" class="{{completed}}"> <div class="view"> <input class="toggle" type="checkbox" {{checked}}> <label>{{title}}</label> <button class="destroy"></button> </div> </li> </template> <!-- Todos summary template --> <template id="template-summary"> <strong>{{count}}</strong> item{{plural}} left </template> </section> <!—- scripts definitions removed for brevity (see Index.html) --> </body> </html>
服务器上托管的模板。
最后一个关键策略是模板从服务器请求。有几种方法可以在服务器上组织模板并将其下载到客户端。本版TodoMVC中的策略是将模板组织成<template>
元素内的源片段,位于服务器位置template/templates.html
的单个文件中。使用ajax调用,包含模板片段的文件会被下载到客户端,然后这些片段会被转换为可用的<template>
DOM元素。
<!-- Content template --> <template id="template-content"> <header class="header"> <h1>todos</h1> <input class="new-todo" placeholder="{{placeholder}}" autofocus> </header> <section class="workspace"> <section class="main"> <input class="toggle-all" type="checkbox"> <label for="toggle-all">{{markall}}</label> <ul class="todo-list"></ul> </section> <section class="menu"> <span class="todo-count"></span> <ul class="filters"> <li> <a href="#/" class="selected">{{default}}</a> </li> <li> <a href="#/active">{{active}}</a> </li> <li> <a href="#/completed">{{completed}}</a> </li> </ul> <button class="clear-completed">{{clear}}</button> </section> </section> </template> <!-- Todo list item template --> <template id="template-listitem"> <li data-id="{{id}}" class="{{completed}}"> <div class="view"> <input class="toggle" type="checkbox" {{checked}}> <label>{{title}}</label> <button class="destroy"></button> </div> </li> </template> <!-- Todos summary template --> <template id="template-summary"> <span><strong>{{count}}</strong> item{{plural}} left</span> </template>
TodoTemplate
在运行时组装HTML模板,并将HTML演示与视图解耦。通过这种分离,TodoView
的职责仅集中于视觉渲染和事件处理。
TodoView
TodoView
管理应用程序的用户界面和外观。它将信息渲染到显示屏,并将用户事件转化为订阅者消息。TodoView
订阅者由其对Todos
Presenter类的所有权提供。这建立了Todos
和TodoView
之间的操作模式,其中Todos
向TodoView
发出命令,TodoView
也向Todos
发布消息。
下面的UML图显示了TodoView
类的类层次结构和组成。

作为Dispatcher
类的后代,TodoView
继承了传入命令和传出事件的实现。Todos
Presenter通过render
方法向TodoView
发出命令,该方法使用TodoView
DOM元素向用户显示内容。
当用户发出事件时,附加的DOM事件处理程序会处理用户事件。之后,TodoView
会将用户事件转化为事件消息,并发布给订阅者。
使用此事件模型消除了耦合,因为事件消息的发布与其订阅者之间存在完全的自主性。另一方面,命令具有更紧密的耦合,因为客户端了解命令提供者。命令和事件的组合允许Todos
Presenter向其依赖的TodoView
发出命令。同时,TodoView
通过订阅者以独立的方式将事件发布给Todos
。
下面的源代码显示了TodoView
如何将所有内容整合在一起。在构造时,TodoView
创建以下内容:
-
一个空对象作为DOM元素的占位符(
dom
) -
TodoTemplate
的实例 -
用于视图命令的容器对象(
viewCommands
) -
最后,它将
viewCommands
注册到其基类Dispatcher
。
Todos
发出的initContent
命令初始化TodoView
。在initContent
中,DOM元素被初始化,attachHandlers
将事件处理程序连接到DOM元素。这些处理程序处理DOM事件,然后将其转化为转发给视图订阅者的消息。Todos
Presenter定义了视图订阅者。
/*jshint strict: true, undef: true, eqeqeq: true */
/*global $, document, View, TodoTemplate */
/**
* The todo view.
*
* @class
*/
function TodoView() {
'use strict';
this.inherit(TodoView, View);
var self = this,
view = {},
todoapp = $('.todoapp'),
template = new TodoTemplate(),
viewCommands = {
initContent: function(settings) {
var element = template.createElementFor(template.content, settings.glossary);
todoapp.append(element);
view.todoList = $('.todo-list');
view.todoItemCount = $('.todo-count');
view.clearCompleted = $('.clear-completed');
view.workspace = $('.workspace');
view.main = $('.main');
view.menu = $('.menu');
view.toggleAll = $('.toggle-all');
view.newTodo = $('.new-todo');
attachHandlers();
},
showEntries: function (todos) {
view.todoList.empty();
todos.forEach(function(todo) {
var viewdata = Object.create(null);
viewdata.id = todo.id;
viewdata.title = todo.title;
if (todo.completed) {
viewdata.completed = 'completed';
viewdata.checked = 'checked';
}
var element = template.createElementFor(template.listitem, viewdata);
view.todoList.append(element);
});
},
showStats: function (stats) {
var viewdata = Object.create(null);
viewdata.count = stats.active;
viewdata.plural = (stats.active > 1) ? 's' : '';
var text = template.createTextFor(template.summary, viewdata);
view.todoItemCount.html(text);
view.workspace.css('display', (stats.total > 0) ? 'block' : 'none');
view.clearCompleted.css('display', (stats.completed > 0) ? 'block' : 'none');
},
toggleAll: function (isCompleted) {
view.toggleAll.prop('checked', isCompleted);
},
setFilter: function (href) {
view.menu.find('.filters .selected').removeClass('selected');
view.menu.find('.filters [href="' + href + '"]').addClass('selected');
},
/**
* Clears the new todo field.
*/
clearNewTodo: function () {
view.newTodo.val('');
},
/**
* Change the completion state of the todo item.
*
* @param {number} id The todo identifier.
* @param {string} title The title of the todo.
*/
completedItem: function (id, completed) {
var listItem = view.todoList.find('[data-id="' + id + '"]');
var btnCompleted = listItem.find('.toggle');
listItem[(completed) ? 'addClass' : 'removeClass']('completed');
btnCompleted.prop('checked', completed);
},
/**
* Edit todo by creating an input field used for editing.
*
* @param {number} id The todo identifier.
* @param {string} title The title of the todo.
*/
editItem: function (id, title) {
var listItem = view.todoList.find('[data-id="' + id + '"]'),
input = $(document.createElement('input'));
listItem.addClass('editing');
input.addClass('edit');
listItem.append(input);
input.val(title);
input.focus();
},
/**
* Edit of todo is completed.
*
* @param {number} id The todo identifier.
* @param {string} title The title of the todo.
*/
editItemDone: function (id, title) {
var listItem = view.todoList.find('[data-id="' + id + '"]');
listItem.find('input.edit').remove();
listItem.removeClass('editing');
listItem.removeData('canceled');
listItem.find('label').text(title);
},
/**
* Remove the todo item.
*
* @param {number} id The todo identitifier.
*/
removeItem: function (id) {
var item = view.todoList.find('[data-id="' + id + '"]');
item.remove();
}
};
/**
* Initialize instance.
*/
function initialize() {
self.$base.init.call(self, viewCommands);
}
/**
* Attaches the UI event handler to the view selectors.
*/
function attachHandlers() {
view.newTodo.on('change', function() {
self.trigger(self.messages.todoAdd, this.value);
});
view.clearCompleted.on('click', function() {
self.trigger(self.messages.todoRemoveCompleted, this, view.clearCompleted.checked);
});
view.toggleAll.on('click', function(event) {
self.trigger(self.messages.todoToggleAll, view.toggleAll.prop('checked'));
});
/**
* Initiate edit of todo item.
*
* @param {event} event Event object.
*/
view.todoList.on('dblclick', 'li label', function(event) {
var id = $(event.target).parents('li').data('id');
self.trigger(self.messages.todoEdit, id);
});
/**
* Process the toggling of the completed todo item.
*
* @param {event} event Event object.
*/
view.todoList.on('click', 'li .toggle', function(event) {
var btnCompleted = $(event.target);
var todoItem = btnCompleted.parents('li');
var label = todoItem.find('label');
self.trigger(self.messages.todoToggle, {id: todoItem.data('id'), title: label.text(), completed: btnCompleted.prop('checked')});
});
/**
* Accept and complete todo item editing.
*
* @param {event} event Event object.
*/
view.todoList.on('keypress', 'li .edit', function(event) {
if (event.keyCode === self.ENTER_KEY) {
$(event.target).blur();
}
});
/*
* Cancel todo item editing.
*/
view.todoList.on('keyup', 'li .edit', function(event) {
if (event.keyCode === self.ESCAPE_KEY) {
var editor = $(event.target);
var todoItem = editor.parents('li');
var id = todoItem.data('id');
todoItem.data('canceled', true);
editor.blur();
self.trigger(self.messages.todoEditCancel, id);
}
});
/*
* Complete todo item editing when focus is loss.
*/
view.todoList.on('blur', 'li .edit', function(event) {
var editor = $(event.target);
var todoItem = editor.parents('li');
if (!todoItem.data('canceled')) {
var id = todoItem.data('id');
self.trigger(self.messages.todoEditSave, id, editor.val());
}
});
// Remove todo item.
view.todoList.on('click', '.destroy', function(event) {
var id = $(event.target).parents('li').data('id');
self.trigger(self.messages.todoRemove, id);
});
}
/**
* Initialize the view.
*
* @returns {Promise} Resource acquisition promise.
*/
this.init = function() {
return template.init()
.then(initialize);
};
}
初始化模式
对象构造过程是面向对象编程的基础。在对象构造期间,分配对象的内存表示,然后执行对象的构造函数方法。面向对象语言通过new
运算符支持对象构造。这种内在支持掩盖了对象构造的两个不同的任务——对象创建和对象初始化。
对象创建是分配对象内存表示的过程,此过程同步执行。对象初始化是为对象分配其状态和获取要使用的资源的过程。对象初始化的执行是同步的或异步的。new
运算符使用同步初始化,这会倾向于将对象构造过程视为完全同步的。
当初始化数据易于获得且分配时间可忽略时,同步初始化最适合。一个常见的例子是从默认值分配对象状态。同步初始化在使用上变得不切实际,特别是对于在初始化过程中获取资源的对象的。资源获取的时间跨度并非总是可以忽略的,并且在同步运行时会阻塞CPU。从服务器获取的资源使用异步请求。发出异步资源请求的同步初始化需要一个等待机制,以防止应用程序在请求完成之前继续运行。没有等待机制,应用程序将返回一个状态未定义的未初始化对象。为了能够正确处理资源获取,需要分离对象初始化职责,从而导致异步初始化模式。
TodoMVC实现了遵循以下规则的替代初始化模式:
-
类构造函数仅限于对象创建和同步状态分配。
-
类通过名为
init
的方法执行对象初始化。 -
init
方法返回一个异步对象。 -
嵌套初始化程序可确保内部类的初始化先于外部类。
-
异步对象的链接可保证初始化序列顺序。
下图表示了初始化模式的模型。

如图所示,初始化顺序为OuterClass.Class1
、InnerClass
、OuterClass.Class2
、OuterClass.Class3
和OuterClass
。OuterClass
的客户端通过调用init
方法开始初始化。OuterClass
的init
方法以异步链的形式调用其所有内部类的init
方法。链接可确保内部兄弟类别的初始化调用顺序。
JavaScript Promise
JavaScript使用Promise
类来创建一个返回未来结果的代理的异步对象。对Promises的详细讨论超出了本文的范围,但有初步了解有助于解释TodoMVC中使用的初始化模式。要获得详细解释,请访问ExploringJS网站。
Promise
构造函数有一个名为executor的函数参数。executor是一个包含异步操作的函数,该异步操作由Promise对象执行。executor函数有两个回调函数作为参数:resolve
和reject
,Promise对象分别通过它们返回成功或错误的结果。
function asyncFunc() {
return new Promise (
function(resolve, reject) { // Promise executor.
resolve(value); // returns success value of the Promise executor.
reject(error); // returns error value in case of failure.
});
}
下面显示的.js代码演示了异步初始化模式的实现。它展示了初始化序列如何由Promise对象的链式调用来管理。链式调用通过Promise
的then
方法建立操作的延续序列。延续是一个在Promise
executor成功完成后执行的回调。then
方法本身返回一个Promise
对象,这使得能够将延续链接成一个序列。
/*
* Asynchronous initialization pattern – InnerClass.
*
* @class
*/
function InnerClass() {
"use strict";
this.inherit(InnerClass);
var self = this,
class1 = new Class1();
/**
* Initialize InnerClass internal state.
*/
function initialize() {
...
}
/**
* Initialize InnerClass.
*
* @returns {Promise} Initialize contained objects.
*/
this.init = function() {
return Class1.init()
.then(initialize);
};
}
/*
* Asynchronous initialization pattern – OuterClass.
*
* @class
*/
function OuterClass() {
"use strict";
this.inherit(OuterClass);
var self = this,
innerClass = new InnerClass(),
class2 = new Class2(),
class3 = new Class3();
/**
* Initialize OuterClass internal state.
*/
function initialize() {
...
}
/**
* Initialize OuterClass.
*
* @returns {Promise} Initialization of contained objects via chaining.
*/
this.init = function() {
return InnerClass.init()
.then(function() {
return class2.init();
}).then(function() {
return class3.init();
}).then(initialize);
};
}
使用TodoMVC中使用的对象的这种实现模式可确保应用程序中对象初始化的Consistency。在初始化过程中,TodoView.init
调用TodoTemplate
的init
方法,该方法返回一个Promise
。由TodoTemplate
返回的Promise
的then
方法包含一个延续函数(initialize
),该函数在TodoTemplate
初始化完成后被调用。TodoView
的initialize方法调用基类View.init
方法来完成视图的初始化。
function TodoView() {
'use strict';
this.inherit(TodoView, View);
var self = this,
view = {},
todoapp = $('.todoapp'),
template = new TodoTemplate(),
viewCommands = {
...
};
/**
* Initialize instance.
*/
function initialize() {
self.$base.init.call(self, viewCommands);
}
/**
* Initialize the view.
*
* @returns {Promise} Resource acquisition promise.
*/
this.init = function() {
return template.init()
.then(initialize);
};
}
作为TodoMVC中最外层的容器对象,Todos
控制器的初始化通过其init
方法触发所有包含对象(controller)的初始化。Todos
控制器使用链式调用来顺序初始化其包含的对象。但是,如果包含对象的初始化不依赖于顺序,则并行化提供了一种替代链式调用的方法。Promise.all
命令接受一个Promise数组,并行执行它们,并在所有Promise完成之前继续执行。并行化对象初始化提高了初始化过程的性能,并应与顺序初始化进行测试,以确定最适合应用程序的方法。
/**
* The todos controller.
*
* @class
*/
function Todos() {
"use strict";
this.inherit(Todos, Controller);
var self = this,
settings = new TodoConfig(),
view = new TodoView(),
model = new TodoModel();
/**
* Initialize instance.
*/
function initialize() {
view.on(subscribers);
view.render(view.commands.initContent, settings);
self.$base.init.call(self, router);
}
/**
* Initialize the todos controller.
*
* @returns {Promise} Resource acquisition promise.
*/
this.init = function() {
// Chained initialization: settings->model->view->initialize.
return settings.init()
.then(function() {
return model.init();
}).then(function() {
return view.init();
}).then(initialize);
// Parallelize initialization: (settings||model||view)->initialize.
// Comment the chained implementation then uncomment the section below.
// return Promise.all([settings.init(),
// model.init(),
// view.init()])
// .then(initialize);
};
}
使用代码
将zip文件下载到您的本地计算机并解压todomvc
文件夹。要运行该应用程序,请在浏览器中打开index.html
。使用浏览器的调试器分析该应用程序。
文件server.py
设置了一个快速简陋的服务器环境。要从服务器运行该应用程序,请执行以下操作:
-
在您的机器上安装python 3.0或更高版本。
-
打开命令窗口。
-
导航到
todomvc
文件夹。 -
在命令行中键入
python server.py
以启动服务器 -
在浏览器的地址栏中使用
https://:8000
来启动应用程序。
关注点
在工作实现到位后,在第三部分中,我们可以回顾并反驳反对无框架的论点。
历史
2016年6月11日 无框架实现初次演示。
2016年6月19日 修复了图20中JavaScript代码的可显示HTML。