TodoMVC中的Backbone+MarionetteJS+RequireJS例子源码分析之三 Views

时间:2022-03-25 12:16:23

这个版本的TodoMVC中的视图组织划分比较细,更加易于理解,这也得益于Marionette为我们带来了丰富的视图选择,原生的backbone只有views,而Marionette则有itemview, collectionview, compositeview 和layoutview.

js/templates.js

/*global define */
define(function (require) {//这里用了简写,因为require.js是CommonJS 模块,相当于 define(["require"],function(require){
'use strict'; return {
todoItemView: require('tpl!templates/todoItemView.tmpl'),//tpl是requireJS的text.js插件的扩展,!符号把templates/todoItemView.tmpl文件的url传给tpl处理返回string内容,详细可以查看js/lib/tpl.js定义,tpl.js是个模版引擎,可以对模板文件进行去空格,去注释,去xml的meta信息等等
todosCompositeView: require('tpl!templates/todoListCompositeView.tmpl'),//同上
footer: require('tpl!templates/footer.tmpl'),//同上
header: require('tpl!templates/header.tmpl')//同上
};
});

js/templates/head.tmpl

<h1>todos</h1>
<input id="new-todo" placeholder="What needs to be done?" autofocus>

js/views/head.js  这个view是用来实现 输入创建新的todo的view视图

/*global define */
define([
'marionette',
'templates'
], function (Marionette, templates) {
'use strict'; return Marionette.ItemView.extend({
template: templates.header,//参考templates.js里面的模板的定义,通过key取值返回value ui: {
input: '#new-todo'//在Merionette中ItemView可以用ui来组织dom元素方便使用,返回的是jQuery对象
}, events: {
'keypress #new-todo': 'onInputKeypress'//原生的backbone的view dom事件绑定,监听Enter键触发下面的新建一个新的todo事件
}, onInputKeypress: function (event) {
var ENTER_KEY = 13;
var todoText = this.ui.input.val().trim(); if (event.which === ENTER_KEY && todoText) {
this.collection.create({
title: todoText
}); this.ui.input.val('');
}
}
});
});

js/templates/todoListCompositeView.tmpl

<input id="toggle-all" type="checkbox">
<label for="toggle-all">Mark all as complete</label>
<ul id="todo-list"></ul>

js/views/TodoListCompositeView.js

/*global define */
define([
'marionette',
'templates',
'views/TodoItemView'
], function (Marionette, templates, ItemView) {
'use strict'; return Marionette.CompositeView.extend({//compositeView是一种混合view,里面可以包含有itemview
template: templates.todosCompositeView,//在js/templates.js里面定义 itemView: ItemView,//指定itemview为views/TodoItemView itemViewContainer: '#todo-list',//itemview的container或者说外面的dom节点是#todo-list,注意 ui: {
toggle: '#toggle-all'
}, events: {
'click #toggle-all': 'onToggleAllClick'//把全部标记为已完成的ui,及定义相应的事件
}, initialize: function () {
this.listenTo(this.collection, 'all', this.updateToggleCheckbox, this);//设置监听collection的任意变动都会触发更新checkbox事件
}, onRender: function () {//render时也触发
this.updateToggleCheckbox();
}, updateToggleCheckbox: function () {
var allCompleted = this.collection.reduce(function (lastModel, thisModel) {//调用underscore库的reduce函数把collection中所有的model的completed值循环作一次"与"运算得出有没有没完成,返回true/false
return lastModel && thisModel.get('completed');
}, true); this.ui.toggle.prop('checked', allCompleted);//如果是allcompleted是true则显示为checked状态
}, onToggleAllClick: function (event) {//点击事件触发把所有的item标为completed或者反选
var isChecked = event.currentTarget.checked; this.collection.each(function (todo) {
todo.save({ completed: isChecked });
});
}
});
});

js/templates/todoItemView.tmpl

<div class="view">
<input class="toggle" type="checkbox"<% if (completed) { %> checked<% } %>>
<label><%= title %></label>
<button class="destroy"></button>
</div>
<input class="edit" value="<%= title %>">

js/views/TodoItemView.js

/*global define */
define([
'marionette',
'templates'
], function (Marionette, templates) {
'use strict'; var ENTER_KEY = 13;
var ESCAPE_KEY = 27; return Marionette.CompositeView.extend({//这个view也相对比较复杂,功能也比较多,所以用了compositeview
tagName: 'li', template: templates.todoItemView, value: '',//下面会用到 ui: {
edit: '.edit'
}, events: {//绑定相应的事件
'click .toggle': 'toggle',
'click .destroy': 'destroy',
'dblclick label': 'onEditDblclick',
'keydown .edit': 'onEditKeyDown',
'blur .edit': 'onEditBlur'
}, initialize: function () {//初始化时执行
this.value = this.model.get('title'); this.listenTo(this.model, 'change', this.render, this);
}, onRender: function () {//每次render时执行
this.$el
.removeClass('active completed')
.addClass(this.model.get('completed') ? 'completed' : 'active');
}, destroy: function () {
this.model.destroy();
}, toggle: function () {
this.model.toggle().save();
}, toggleEditingMode: function () {
this.$el.toggleClass('editing');
}, onEditDblclick: function () {
this.toggleEditingMode(); this.ui.edit.focus().val(this.value);
}, onEditKeyDown: function (event) {
if (event.which === ENTER_KEY) {
this.ui.edit.trigger('blur');
} if (event.which === ESCAPE_KEY) {
this.ui.edit.val(this.model.get('title'));
this.ui.edit.trigger('blur');
}
}, onEditBlur: function (event) {
this.value = event.target.value.trim(); if (this.value) {
this.model.set('title', this.value).save();
} else {
this.destroy();
} this.toggleEditingMode();
}
});
});

js/templates/footer.tmpl

<span id="todo-count"><strong>0</strong> items left</span>
<ul id="filters">
<li>
<a href="#/">All</a>
</li>
<li>
<a href="#/active">Active</a>
</li>
<li>
<a href="#/completed">Completed</a>
</li>
</ul>
<button id="clear-completed"></button>

js/views/Footer.js

/*global define */
define([
'marionette',
'templates',
'views/ActiveCount',
'views/CompletedCount'
], function (Marionette, templates, ActiveCount, CompletedCount) {
'use strict'; return Marionette.Layout.extend({//footer功能点比较多,有a热点,也有统计用的view如activeCount或者completedCount,所以可以用Layoutview
template: templates.footer, regions: {
activeCount: '#todo-count',//划分regions来管理
completedCount: '#clear-completed'
}, ui: {
filters: '#filters a'
}, events: {
'click #clear-completed' : 'onClearClick'
}, onRender: function () {
this.activeCount.show(new ActiveCount({ collection: this.collection }));//调相应的子ActiveCount来显示
this.completedCount.show(new CompletedCount({ collection: this.collection }));//调相应的子ActiveCount来显示
}, updateFilterSelection: function (filter) {//在app.js外面被调用
this.ui.filters
.removeClass('selected')
.filter('[href="#/' + filter + '"]')
.addClass('selected');//纯UI控制显示或者隐藏相应的item
}, onClearClick: function () {
window.app.vent.trigger('todoList:clear:completed');//调外部在app.js定义的函数
}
});
});

js/views/ActiveCount.js

/*global define */
define([
'marionette',
'jquery'
], function (Marionette, $) {
'use strict'; return Marionette.View.extend({//用最简单的view
initialize: function () {
this.listenTo(this.collection, 'all', this.render, this);
}, render: function () {//直接render UI控制显示,计算显示还有多少todo没complete然后显示
this.$el = $('#todo-count'); var itemsLeft = this.collection.getActive().length;
var itemsWord = itemsLeft < 1 || itemsLeft > 1 ? 'items' : 'item'; this.$el.html('<strong>' + itemsLeft + '</strong> ' + itemsWord + ' left');
}
});
});

js/views/CompletedCount.js

/*global define */
define([
'marionette',
'jquery'
], function (Marionette, $) {
'use strict'; return Marionette.View.extend({//用最简单的view
initialize: function () {
this.listenTo(this.collection, 'all', this.render, this);
}, render: function () {//直接render UI控制显示,计算有多少todo已经complete然后显示相应的clear completed的菜单
this.$el = $('#clear-completed'); var completedTodos = this.collection.getCompleted(); this.$el
.toggle(completedTodos.length > 0)
.html('Clear completed (' + completedTodos.length + ')');
}
});
});

总的来说,统一处理是在app.js里面,然后各个子模块提供服务

最后是tpl模板引擎 js/lib/tpl.js 原生js编写,符合AMD规范的模块

/**
* Adapted from the official plugin text.js
*
* Uses UnderscoreJS micro-templates : http://documentcloud.github.com/underscore/#template
* @author Julien Caban猫s <julien@zeeagency.com>
* @version 0.2
*
* @license RequireJS text 0.24.0 Copyright (c) 2010-2011, The Dojo Foundation All Rights Reserved.
* Available via the MIT or new BSD license.
* see: http://github.com/jrburke/requirejs for details
*/
/*jslint regexp: false, nomen: false, plusplus: false, strict: false */
/*global require: false, XMLHttpRequest: false, ActiveXObject: false,
define: false, window: false, process: false, Packages: false,
java: false */ (function () {
//>>excludeStart('excludeTpl', pragmas.excludeTpl)
var progIds = ['Msxml2.XMLHTTP', 'Microsoft.XMLHTTP', 'Msxml2.XMLHTTP.4.0'], xmlRegExp = /^\s*<\?xml(\s)+version=[\'\"](\d)*.(\d)*[\'\"](\s)*\?>/im, bodyRegExp = /<body[^>]*>\s*([\s\S]+)\s*<\/body>/im, buildMap = [], templateSettings = {
evaluate : /<%([\s\S]+?)%>/g,
interpolate : /<%=([\s\S]+?)%>/g
}, /**
* JavaScript micro-templating, similar to John Resig's implementation.
* Underscore templating handles arbitrary delimiters, preserves whitespace,
* and correctly escapes quotes within interpolated code.
*/
template = function(str, data) {
var c = templateSettings;
var tmpl = 'var __p=[],print=function(){__p.push.apply(__p,arguments);};' +
'with(obj||{}){__p.push(\'' +
str.replace(/\\/g, '\\\\')
.replace(/'/g, "\\'")
.replace(c.interpolate, function(match, code) {
return "'," + code.replace(/\\'/g, "'") + ",'";
})
.replace(c.evaluate || null, function(match, code) {
return "');" + code.replace(/\\'/g, "'")
.replace(/[\r\n\t]/g, ' ') + "; __p.push('";
})
.replace(/\r/g, '')
.replace(/\n/g, '')
.replace(/\t/g, '')
+ "');}return __p.join('');";
return tmpl; /** /
var func = new Function('obj', tmpl);
return data ? func(data) : func;
/**/
};
//>>excludeEnd('excludeTpl') define(function () {
//>>excludeStart('excludeTpl', pragmas.excludeTpl)
var tpl; var get, fs;
if (typeof window !== "undefined" && window.navigator && window.document) {
get = function (url, callback) { var xhr = tpl.createXhr();
xhr.open('GET', url, true);
xhr.onreadystatechange = function (evt) {
//Do not explicitly handle errors, those should be
//visible via console output in the browser.
if (xhr.readyState === 4) {
callback(xhr.responseText);
}
};
xhr.send(null);
};
} else if (typeof process !== "undefined" &&
process.versions &&
!!process.versions.node) {
//Using special require.nodeRequire, something added by r.js.
fs = require.nodeRequire('fs'); get = function (url, callback) { callback(fs.readFileSync(url, 'utf8'));
};
}
return tpl = {
version: '0.24.0',
strip: function (content) {
//Strips <?xml ...?> declarations so that external SVG and XML
//documents can be added to a document without worry. Also, if the string
//is an HTML document, only the part inside the body tag is returned.
if (content) {
content = content.replace(xmlRegExp, "");
var matches = content.match(bodyRegExp);
if (matches) {
content = matches[1];
}
} else {
content = "";
} return content;
}, jsEscape: function (content) {
return content.replace(/(['\\])/g, '\\$1')
.replace(/[\f]/g, "\\f")
.replace(/[\b]/g, "\\b")
.replace(/[\n]/g, "")
.replace(/[\t]/g, "")
.replace(/[\r]/g, "");
}, createXhr: function () {
//Would love to dump the ActiveX crap in here. Need IE 6 to die first.
var xhr, i, progId;
if (typeof XMLHttpRequest !== "undefined") {
return new XMLHttpRequest();
} else {
for (i = 0; i < 3; i++) {
progId = progIds[i];
try {
xhr = new ActiveXObject(progId);
} catch (e) {} if (xhr) {
progIds = [progId]; // so faster next time
break;
}
}
} if (!xhr) {
throw new Error("require.getXhr(): XMLHttpRequest not available");
} return xhr;
}, get: get, load: function (name, req, onLoad, config) { //Name has format: some.module.filext!strip
//The strip part is optional.
//if strip is present, then that means only get the string contents
//inside a body tag in an HTML string. For XML/SVG content it means
//removing the <?xml ...?> declarations so the content can be inserted
//into the current doc without problems. var strip = false, url, index = name.indexOf("."),
modName = name.substring(0, index),
ext = name.substring(index + 1, name.length); index = ext.indexOf("!"); if (index !== -1) {
//Pull off the strip arg.
strip = ext.substring(index + 1, ext.length);
strip = strip === "strip";
ext = ext.substring(0, index);
} //Load the tpl.
url = 'nameToUrl' in req ? req.nameToUrl(modName, "." + ext) : req.toUrl(modName + "." + ext); tpl.get(url, function (content) {
content = template(content); if(!config.isBuild) {
//if(typeof window !== "undefined" && window.navigator && window.document) {
content = new Function('obj', content);
}
content = strip ? tpl.strip(content) : content; if (config.isBuild && config.inlineText) {
buildMap[name] = content;
}
onLoad(content);
}); }, write: function (pluginName, moduleName, write) {
if (moduleName in buildMap) {
var content = tpl.jsEscape(buildMap[moduleName]);
write("define('" + pluginName + "!" + moduleName +
"', function() {return function(obj) { " +
content.replace(/(\\')/g, "'").replace(/(\\\\)/g, "\\")+
"}});\n");
}
}
};
//>>excludeEnd('excludeTpl')
return function() {};
});
//>>excludeEnd('excludeTpl')
}());