前言

  前端的MVC,近几年一直很火,大家也都纷纷讨论着,于是乎,抽空总结一下这个知识点。看了些文章,结合实践略作总结并发表一下自己的看法。  

  最初接触MVC是后端Java的MVC架构,用一张图来表示之——

  这样,我们让每一个层次去关注并做好一件事情,层与层之间保持松耦合,我们可以对每一个层次单独做好测试工作。如此,我们可以让代码更具可维护性。
  因此,借鉴于后端的这种MVC设计思想(更多的我想是一种优秀的、经过考验的实践模式),针对越来越复杂的JavaScript应用程序,便有了猜想,我们是否可以使用MVC的设计思想,编写出高维护性的前端程序。
 
一、MVC定义
  先来看看《基于MVC的JavaScript Web富应用开发》对MVC的定义——
MVC是一种设计模式,它将应用划分为3个部分:数据(模型)、展现层(视图)和用户交互(控制器)。换句话说,一个事件的发生是这样的过程:
  1. 用户和应用产生交互。
  2. 控制器的事件处理器被触发。
  3. 控制器从模型中请求数据,并将其交给视图。
  4. 视图将数据呈现给用户。
我们不用类库或框架就可以实现这种MVC架构模式。关键是要将MVC的每部分按照职责进行划分,将代码清晰地分割为若干部分,并保持良好的解耦。这样可以对每个部分进行独立开发、测试和维护。
  而今,流行的MVC框架比比皆是,如Embejs、Angular.js、Backbone.js、Knockout.js等等——
  
  通过上图,我们我们可以清楚地了解Javascript MVC框架之间的特性,复杂度和学习曲线的区别,从左到右我们了解到各个Javascript MVC框架是否支持数据绑定(Data Binding)、模板(Templating)和持久化等特性,从下到上MVC框架的复杂性递增。
  当然,“我们不用类库或框架就可以实现这种MVC架构模式。”因此,我们需要对MVC的每一个部分,做一个详细的剖析——
  1> 模型——

模型用来存放应用的所有数据对象。比如,可能有一个User模型,用以存放用户列表、他们的属性及所有与模型有关的逻辑。
模型不必知道视图和控制器的逻辑。任何事件处理代码、视图模板,以及那些和模型无关的逻辑都应当隔离在模型之外。
将模型的代码和视图的代码混在一起,是违反MVC架构原则的。模型是最应该从你的应用中解耦出来的部分。
当控制器从服务器抓取数据或创建新的记录时,它就将数据包装成模型实例。也就是说,我们的数据是面向对象的,任何定义在这个数据模型上的函数或逻辑都可以直接被调用。

  2> 视图——

视图层是呈现给用户的,用户与之产生交互。在JavaScript应用中,视图大都是由HTML、CSS、JavaScript模板组成的。除了模板中简单的条件语句之外,视图不应当包含任何其他逻辑。
将逻辑混入视图之中是编程的大忌,这并不是说MVC不允许包含视觉呈现相关的逻辑,只要这部分逻辑没有定义在视图之内即可。我们将视觉呈现逻辑归类为“视图助手”(helper):和视图相关的独立的小工具函数。
来看下面的例子,骑在视图中包含了逻辑,这是一个范例,平时不应当这样做:

<div>
<script>
function formatDate(date) {
/* ... */
}
</script>
${ formateDate(this.date) }
</div>

在这段代码中,我们把formatDate()函数直接插入视图中,这违反了MVC的原则,结果导致标签看上去像大杂烩一样不可维护。可以将视觉呈现逻辑剥离出来放入试图助手中,正如下面的代码就避免了这个问题,可以让这个应用的结构满足MVC。

// helper.js
var helper = {};
helper.formateDate(date) {
/* ... */
}; // template.html
<div>
${ helper.formate(this.date) }
</div>

此外,所有视觉呈现逻辑都包含在helper变量中,这是一个命名空间,可以防止冲突并保持代码清晰、可扩展。

  3> 控制器——

控制器是模型和视图之间的纽带。控制器从视图获取事件和输入,对它们(很可能包含模型)进行处理,并相应地更新视图。当页面加载时,控制器会给视图添加事件监听,比如监听表单提交或按钮点击。然后,当用户和你的应用产生交互时,控制器中的事件触发器就开始工作了。
我们用简单的jQuery代码来实现控制器——

var Controller = {};

(Controller.users = function($) {
var nameClick = function() {
/* ... */
}; // 在页面加载时绑定事件监听
$(function() {
$('#view .name').click(nameClick);
});
})(jQuery);
  现在,我们知道了M(Model)、V(View)、C(Controller)每个部分的工作内容,我们就可以轻松实现属于我们自己的MVC应用程序了,当然,我们完全不必依赖那些流行与否的MVC框架。
  接下来,针对业界MVC的DEMO-todo的例子(项目主页:http://todomvc.com/),简单对比使用jQuery实现mvc及各框架对MVC的实现。
 
二、使用jQuery实现MVC
  先了解这个todo-demo——
  1. 初始化查询列表——
  
  2.添加记录——
  
  3.删除记录——
  
  4.修改记录——
  
  5.对model集合的操作(标示那些完成、清除完成项)
  
  整体而言,这是简单的一个富应用小程序,我们先看看使用jQuery模拟MVC去实现之——
  1> app.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">
<span id="todo-count"><strong>0</strong> item left</span>
<button id="clear-completed">Clear completed</button>
</footer>
</section>
<footer id="info">
<p>Double-click to edit a todo</p>
<p>Created by <a href="http://github.com/sindresorhus">Sindre Sorhus</a></p>
<p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
</footer> <!-- ************************************* template begin *********************************** -->
<!-- 针对模型的模板 -->
<script id="todo-template" type="text/x-handlebars-template">
<!-- 这里对todo模型数组进行迭代循环 -->
{{#this}}
<!-- 会看到,这里具有简单的if语句,即这里具备显示逻辑 -->
<li {{#if completed}}class="completed"{{/if}} data-id="{{id}}">
<div class="view">
<input class="toggle" type="checkbox" {{#if completed}}checked{{/if}}>
<label>{{title}}</label>
<button class="destroy"></button>
</div>
<input class="edit" value="{{title}}">
</li>
{{/this}}
</script>
<!-- /针对模型的模板 -->
<!-- footer模板,记录还剩下多少没有完成等 -->
<script id="footer-template" type="text/x-handlebars-template">
<span id="todo-count"><strong>{{activeTodoCount}}</strong> {{activeTodoWord}} left</span>
{{#if completedTodos}}
<button id="clear-completed">Clear completed ({{completedTodos}})</button>
{{/if}}
</script>
<!-- /footer模板 -->
<!-- ************************************* template end *********************************** --> <script src="js/base/base.js"></script>
<script src="js/lib/jquery.js"></script>
<script src="js/lib/handlebars.js"></script> <!-- app begin -->
<script src="js/app.js"></script>

app.html

  2> app.js

jQuery(function() {
'use strict'; // 这里是一些工具函数的抽取,包括
// 1.ID生成器
// 2.显示格式化
// 3.localStorage存储
var Utils = {
uuid : function() {
/*jshint bitwise:false */
var i, random;
var uuid = ''; for ( i = 0; i < 32; i++) {
random = Math.random() * 16 | 0;
if (i === 8 || i === 12 || i === 16 || i === 20) {
uuid += '-';
}
uuid += (i === 12 ? 4 : (i === 16 ? (random & 3 | 8) : random)).toString(16);
} return uuid;
},
pluralize : function(count, word) {
return count === 1 ? word : word + 's';
},
store : function(namespace, data) {
if (arguments.length > 1) {
return localStorage.setItem(namespace, JSON.stringify(data));
} else {
var store = localStorage.getItem(namespace);
return (store && JSON.parse(store)) || [];
}
}
}; var Todo = function(id, title, completed) {
this.id = id;
this.title = title;
this.completed = completed;
} var App = { init: function() {
this.ENTER_KEY = 13;
this.todos = Utils.store('todos-jquery');
this.cacheElements();
this.bindEvents();
}, // 这里是缓存一些必要的dom节点,提高性能
cacheElements: function() {
this.todoTemplate = Handlebars.compile($('#todo-template').html());
this.footerTemplate = Handlebars.compile($('#footer-template').html());
this.$todoApp = $('#todoapp');
this.$header = this.$todoApp.find('#header');
this.$main = this.$todoApp.find('#main');
this.$footer = this.$todoApp.find('#footer');
this.$newTodo = this.$header.find('#new-todo');
this.$toggleAll = this.$main.find('#toggle-all');
this.$todoList = this.$main.find('#todo-list');
this.$count = this.$footer.find('#todo-count');
this.$clearBtn = this.$footer.find('#clear-completed');
}, // 模拟Controller实现:所有的事件监听在这里绑定
bindEvents: function() {
var list = this.$todoList;
this.$newTodo.on('keyup', this.create);
this.$toggleAll.on('change', this.toggleAll);
this.$footer.on('click', '#clear-completed', this.destroyCompleted);
list.on('change', '.toggle', this.toggle);
list.on('dblclick', 'label', this.edit);
list.on('keypress', '.edit', this.blurOnEnter);
list.on('blur', '.edit', this.update);
list.on('click', '.destroy', this.destroy);
}, // 渲染记录列表:当模型数据发生改变的时候,对应的事件处理程序调用该方法,从而实现对应DOM的重新渲染
render: function() {
this.$todoList.html(this.todoTemplate(this.todos));
this.$main.toggle(!!this.todos.length);
this.$toggleAll.prop('checked', !this.activeTodoCount());
this.renderFooter();
Utils.store('todos-jquery', this.todos);
}, // 渲染底部
renderFooter: function () {
var todoCount = this.todos.length;
var activeTodoCount = this.activeTodoCount();
var footer = {
activeTodoCount: activeTodoCount,
activeTodoWord: Utils.pluralize(activeTodoCount, 'item'),
completedTodos: todoCount - activeTodoCount
}; this.$footer.toggle(!!todoCount);
this.$footer.html(this.footerTemplate(footer));
}, // 创建记录
create: function (e) {
var $input = $(this);
var val = $.trim($input.val()); if (e.which !== App.ENTER_KEY || !val) {
return;
} App.todos.push({
id: Utils.uuid(),
title: val,
completed: false
}); // 记录添加后,通知重新渲染页面
App.render();
}, // 其他业务逻辑函数
edit: function() {},
destroy: function() {}
/* ... */ } App.init(); });

app.js

  这样,我们使用jQuery实现了mvc架构的小应用程序,我再分析一下这个小demo的特点——
1.维护的model是todo实例的列表,这样,我们对增加记录、删改某一条记录,都要重新渲染整个列表,这样,导致性能的拙劣行。当然,改进的方式是对每一个实例进行对应dom的绑定。
2.这里的View中,我们看到其中参杂了一些显示逻辑,显然,我提倡这样去做,而非在js中去控制业务逻辑。然而,我们在实际开发的过程当中,我们必然涉及到复杂的显示逻辑,这样,我们可以向之前所说的那样,利用单独编写显示逻辑helper,这与MVC的设计思想并不违背,确保高维护性及扩展性。
3.这里有关模型todos的业务逻辑,并没有严格抽象出来,而是写入对应的事件当中。

  接下来,看看其他优秀的框架如何去做的。

三、前端MVC框架

  相信大家都听过MVC、MVP、MVVM了,三者的简单定义——

(1)MVC: 模型-视图-控制器(Model View Controller)
(2)MVP: 模型-视图-表现类(Model-View-Presenter)
(3)MVVM:模型-视图-视图模型(Model-View-ViewModel)

  它们三者的发展过程是MVC->MVP->MVVM,我们分别来看这三者——

  1> Ember.js(MVC)

  先看看项目整体文件架构——

  

  会发现,主要是有controller、model、router,先引入index.html中的模板(同样使用的是Handlebars)——

<script type="text/x-handlebars" data-template-name="todos">
<section id="todoapp">
<header id="header">
<h1>todos</h1>
<!-- 这里的action属性指定了对应的TodosController中的createTodo方法 -->
{{input id="new-todo" type="text" value=newTitle action="createTodo" placeholder="What needs to be done?"}}
</header>
{{#if length}}
<section id="main">
<ul id="todo-list">
{{#each filteredTodos itemController="todo"}}
<li {{bind-attr class="isCompleted:completed isEditing:editing"}}>
{{#if isEditing}}
{{edit-todo class="edit" value=bufferedTitle focus-out="doneEditing" insert-newline="doneEditing" escape-press="cancelEditing"}}
{{else}}
{{input type="checkbox" class="toggle" checked=isCompleted}}
<label {{action "editTodo" on="doubleClick"}}>{{title}}</label>
<button {{action "removeTodo"}} class="destroy"></button>
{{/if}}
</li>
{{/each}}
</ul>
{{input type="checkbox" id="toggle-all" checked=allAreDone}}
</section>
<footer id="footer">
<span id="todo-count">{{{remainingFormatted}}}</span>
<ul id="filters">
<li>
{{#link-to "todos.index" activeClass="selected"}}All{{/link-to}}
</li>
<li>
{{#link-to "todos.active" activeClass="selected"}}Active{{/link-to}}
</li>
<li>
{{#link-to "todos.completed" activeClass="selected"}}Completed{{/link-to}}
</li>
</ul>
{{#if hasCompleted}}
<button id="clear-completed" {{action "clearCompleted"}}>
Clear completed ({{completed}})
</button>
{{/if}}
</footer>
{{/if}}
</section>
<footer id="info">
<p>Double-click to edit a todo</p>
<p>
Created by
<a href="http://github.com/tomdale">Tom Dale</a>,
<a href="http://github.com/addyosmani">Addy Osmani</a>
</p>
<p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
</footer>
</script>

index.html

  会发现,模板代码添加了一些晦涩的属性标签。对于Ember.js的使用,我们需要创建一个Ember应用程序实例(app.js文件中)——

window.Todos = Ember.Application.create();
  紧接着我们需要渲染模板中的数据,由于渲染模板的内容是根据路由选择后动态获取的模板内容,当我们的应用程序启动时,路由是负责显示模板,加载数据,以及管理应用程序的状态。
  在router.js中——
Todos.Router.map(function () {
this.resource('todos', { path: '/' }, function () {
this.route('active');
this.route('completed');
});
});
// 这里进行了硬绑定,即对应的模板名字为data-template-name="todos"
Todos.TodosRoute = Ember.Route.extend({
model: function () {
// 显示设定该路由的的model数据
// return this.store.find('todo');
return [{
id: 1,
title: 'todo1',
compeled: false
}];
}
}); // 下面定义了三个子路由
// #/index
Todos.TodosIndexRoute = Ember.Route.extend({
setupController: function () {
// 显示定义对应的controller程序
this.controllerFor('todos').set('filteredTodos', this.modelFor('todos'));
}
}); // #/active
Todos.TodosActiveRoute = Ember.Route.extend({
setupController: function () {
var todos = this.store.filter('todo', function (todo) {
return !todo.get('isCompleted');
}); this.controllerFor('todos').set('filteredTodos', todos);
}
}); // #/completed
Todos.TodosCompletedRoute = Ember.Route.extend({
setupController: function () {
var todos = this.store.filter('todo', function (todo) {
return todo.get('isCompleted');
}); this.controllerFor('todos').set('filteredTodos', todos);
}
});

router.js

  会发现,这里的3个特点:
1. 模板文件的模板名称data-template-name="todos"对应的路由模板便是Todos.TodosRoute;
2. 对该路由显示指定对应模板的数据模型。当然对这里的数据模型(即上面的model属性)同样进行了硬绑定(即对应的todo.js)——
Todos.todo = DS.Model.extend({
title: DS.attr('string'),
isCompleted: DS.attr('boolean'),
saveWhenCompletedChanged: function() {
this.save();
}.observes('isCompleted')
});
3. 对该路由同样能够指定对应的controller(上面的setController属性)。这里主要侦听对hash改变,对数据进行过滤操作。

  下面我们看一看对Controller的定义,当然存在一定的硬绑定(潜规则)——todos-controller.js

Todos.TodosController = Ember.ArrayController.extend({

    // 针对model集合的的交互在这里定义
actions: {
// 该方法的调用时在对应的dom节点中进行绑定,即对应模板中的下列语句
// {{input id="new-todo" type="text" value=newTitle action="createTodo" placeholder="What needs to be done?"}}
createTodo: function() {
var title, todo; title = this.get('newTitle').trim();
if (!title) {
return;
} todo = {
title: title,
isCompleted: false
}; todo.save(); this.set('newTitle', ''); },
/* ... */
}, // 以下主要定义显示逻辑
remaining: function () {
return this.filterProperty('isCompleted', false).get('length');
}.property('@each.isCompleted'), // 对应的dom调用时<span id="todo-count">{{{remainingFormatted}}}</span>
remainingFormatted: function () {
var remaining = this.get('remaining');
var plural = remaining === 1 ? 'item' : 'items';
return '<strong>%@</strong> %@ left'.fmt(remaining, plural);
}.property('remaining'),
/* ... */ });

todos-controller.js

  会发现上面的这个controller是针对model集合的,对单条model记录的controller,放在todo-controller.js文件中——

Todos.TodoController = Ember.ObjectController.extend({

    isEditing: false,

    // 缓存title
bufferedTitle: Ember.computed.oneWay('title'), // 这里包含了对单条记录的所有增删改查的操作
actions: { editTodo: function() {
this.set('isEditing', true);
}, doneEditing: function() {
var bufferedTitle = this.get('bufferedTitle').trim(); if (Ember.isEmpty(bufferedTitle)) {
Ember.run.debounce(this, this.send, 'removeTodo', 0);
} else {
var todo = this.get('model');
todo.set('title', bufferedTitle);
todo.save();
} this.set('bufferedTitle', bufferedTitle);
this.set('isEditing', false);
}, cancelEditing: function() {
this.set('bufferedTitle', this.get('title'));
this.set('Editing', false);
}, removeTodo: function() {
var todo = this.get('model'); todo.deleteRecord();
todo.save();
}
}
});

todo-controller.js

  对这些方法的调用,看一看对应的模板文件就知道了——

<ul id="todo-list">
{{#each filteredTodos itemController="todo"}}
<li {{bind-attr class="isCompleted:completed isEditing:editing"}}>
{{#if isEditing}}
{{edit-todo class="edit" value=bufferedTitle focus-out="doneEditing" insert-newline="doneEditing" escape-press="cancelEditing"}}
{{else}}
{{input type="checkbox" class="toggle" checked=isCompleted}}
<label {{action "editTodo" on="doubleClick"}}>{{title}}</label>
<button {{action "removeTodo"}} class="destroy"></button>
{{/if}}
</li>
{{/each}}
</ul>

  会发现,红色标注的部分,正是我们在todo-controler.js中定义的事件。还会发现,Ember.js封装了一些事件属性,如——

focus-out
insert-newline
escape-press
doubleClick

  到这儿,Ember.js的内容就简单介绍完了,总结一下——

1. 程序的加载入口是rounter(即app.TemplatenameRouter),来指定对应的model及controller。路由是负责显示模板,加载数据,以及管理应用程序的状态。
2. 程序的交互入口是controller,这里面包含两个类型的controller,一个是对应model集合的controller,一个是对应model的controller。两者各司其职,增加了代码的可维护性。

  Ember.js是典型的MVC(这里有别于MVP、MVVM的设计模式类)框架,还有一个比较典型的MVC框架便是Angular.js,和Ember.js的设计思想大致相同。

  从Ember.js的应用,我们可以理解MVC的特点——MVC的View直接与Model打交道,Controller仅仅起一个“桥梁”作用,它负责把View的请求转发给Model,再负责把Model处理结束的消息通知View。Controller就是一个消息分发器。不传递数据(业务结果),Controller是用来解耦View和Model的,具体一点说,就是为了让UI与逻辑分离(界面与代码分离)。

  

  2>Backbone.js(MVP)

  依旧先看一下文件架构——

  

  相对于Ember.js和Angular.js,它的模板比较清爽——

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

模板代码

  这是由于添加了Presenter的原因,事件的绑定及页面view的变化,全部由Presenter去做。

  这里存在一个model集合的概念,即这里的collection.js——

(function() {
'use strict'; var Todos = Backbone.Collection.extend({
model: app.Todo, localStorage: new Backbone.LocalStorage('todos-backbone'), // Filter down the list of all todo items that are finished.
completed: function () {
return this.filter(function (todo) {
return todo.get('completed');
});
}, // Filter down the list to only todo items that are still not finished.
remaining: function () {
return this.without.apply(this, this.completed());
}, nextOrder: function() {
if (this.length === 0) {
return 1;
}
return this.last().get('order') + 1;
}, //
comparator: function(todo) {
return todo.get('order');
}
}); app.todos = new Todos(); })();

collection.js

  app-view.js生成应用的一个Presenter实例(new AppView()),并由该实例来绑定事件,并控制集合todos的变化(用户通过view产生交互来触发),一旦todos发生变化,来触发对应的view变化。同样的,这里的todo-view.js干的是同样一件事,只不过针对的是model单个对象。

  从Backbone.js的应用,我们可以理解MVP的特点——Presenter直接调用Model的接口方法,当Model中的数据发生改变,通知Presenter进行对应的View改变。从而使得View不再与Model产生交互。

  3> Knockout.js(MVVM)

  先看看它的页面——

<section id="todoapp" data-bind="">
<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 (<span data-bind="text: completedCount"></span>)
</button>
</footer>
</section>
<script src="js/base/base.js"></script>
<script src="js/lib/knockout.js"></script>
<script src="js/app.js"></script>

页面代码

  会发现很多data-bind属性,先不管它,我们在看看ViewModel的定义——

// 针对view来创建ViewModel
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('');
}
}; // 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));
}.bind(this)).extend({
throttle: 500
}); // save at most twice per second
};

ViewModel定义

  会发现,视图View中的data-bind属性值正是ViewModel实例的对应方法,这似乎看起来很像是视图助手helper要做的事情。其实不然,这里的ViewModel,顾名思义,是对View的一次抽象,即对View再提取其对应的模型。

  MVVM的特点如下——

1. ViewModel是model和View的中间接口
2. ViewMode提供View与Model数据之间的命令,即这里的data-bind的值,ViewModel中的方法
3. UI的渲染均由ViewModel通过命令来控制

四、前端MVC模式与传统开发模式的对比

  传统的开发模式,大多基于事件驱动的编码组织,举个例子——

$('#update').click(function(e) {
// 1.事件处理程序
e.preventDefault(); // 2.获取对应的model的属性值
var title = $('#text').val(); // 3.调用业务逻辑
$.ajax({
url : '/xxx',
type : 'POST',
data : {
title : title,
completed : false
},
success : function(data) {
// 4.对data进行处理,并进行对应的dom渲染
},
error: function() {
// 4.错误处理
}
}); });

  优化一些,我们可以分离事件处理程序和业务逻辑,在这里,就不延伸举例了。总之,传统的开发模式,并没有分层的概念,即没有model、view、controller。好的方面是我们可以对单独的业务逻辑进行抽取并单独测试。并对这个部分代码进行复用及封装。坏的方面,当应用变得越来越复杂的时候,就会显得代码凌乱,维护性日益变差。

  有同学可能会说,还可以结合面向对象、单命名空间的方式,让代码看起来更加优雅,更具可维护性。但是还是没有办法有效去分离UI逻辑的频繁变化(这里仅仅针对富应用程序)。

五、总结  

  总之,既然学习了MVC这个设计模式,当然,我们不一定非要去采用某一个框架(学习曲线、嵌入性、文件大小、兼容性、应用场景等等我们都要进行考虑),我们无需放大前端框架的作用,我们需要领会的仅仅是其在前端应用的思想。就像最初jQuery模拟实现MVC的方式一样,我再来总结几个关键点——

1.构造模型Model
2.分离事件绑定,形成Controller
3.维护模型Model(and 模型集合Model Collection),通过Model的改变,通知对应的View重新渲染
4.分离View显示逻辑

  这样,我们借助MVC的设计思想,能够现有代码进行重构,当然也能够对未来的代码进行一定展望。

  当然,每一个项目都有自身的特点,个人认为,针对富应用(尤其对增删改的操作占比较大的比例)的项目,MVC的设计模式具备一定的优势。

  

参考:
 
 

侃侃前端MVC设计模式的更多相关文章

  1. 前端mvc mvp mvvm 架构介绍(vue重构项目一)

    首先 我们为什么重构这个项目 1:我们现有的技术是前后台不分离,页面上采用esayUI+jq构成的单页面,每个所谓的单页面都是从后台胜场的唯一Id 与前端绑定,即使你找到了那个页面元素,也找不到所在的 ...

  2. 前端MVC框架Backbone 1.1.0源码分析(一)

    前言 如何定义库与框架 前端的辅助工具太多太多了,那么我们是如何定义库与框架? jQuery是目前用的最广的库了,但是整体来讲jQuery目的性很也明确针对“DOM操作”,当然自己写一个原生态方法也能 ...

  3. 谈谈JAVA工程狮面试中经常遇到的面试题目------什么是MVC设计模式

    作为一名java工程狮,大家肯定经历过很多面试,但每次几乎都会被问到什么是MVC设计模式,你是怎么理解MVC的类似这样的一系列关于MVC的问题. [出现频率] [关键考点] MVC的含义 MVC的结构 ...

  4. 【blade的UI设计】理解前端MVC与分层思想

    前言 最近校招要来了,很多大三的同学一定按捺不住心中的焦躁,其中有期待也有彷徨,或许更多的是些许担忧,最近在开始疯狂的复习了吧 这里小钗有几点建议给各位: ① 不要看得太重,关心则乱,太紧张反而表现不 ...

  5. 前端MVC学习总结——AngularJS验证、过滤器

    前端MVC学习总结--AngularJS验证.过滤器 目录 一.验证 二.过滤器 2.1.内置过滤器 2.1.1.在模板中使用过滤器 2.1.2.在脚本中调用过滤函数 2.2.自定义过滤器 三.指令( ...

  6. 我对前端MVC的理解

    前端MVC:(model.view.controller)模型.视图.控制器 MVC的逻辑都应该以函数的形式包装好,然后按产品业务和交互需求,使用对应的设计模式组装成合适的MVC对象或类. MVC逻辑 ...

  7. 第13天 JSTL标签、MVC设计模式、BeanUtils工具类

    第13天 JSTL标签.MVC设计模式.BeanUtils工具类 目录 1.    JSTL的核心标签库使用必须会使用    1 1.1.    c:if标签    1 1.2.    c:choos ...

  8. 前端 MVC 变形记

    背景: MVC是一种架构设计模式,它通过关注点分离鼓励改进应用程序组织.在过去,MVC被大量用于构建桌面和服务器端应用程序,如今Web应用程序的开 发已经越来越向传统应用软件开发靠拢,Web和应用之间 ...

  9. 一、JSP九大内置对象 二、JAVAEE三层架构和MVC设计模式 三、Ajax

    一.JSP九大内置对象###<1>概念 不需要预先申明和定义,可以直接在jsp代码中直接使用 在JSP转换成Servlet之后,九大对象在Servlet中的service方法中对其进行定义 ...

随机推荐

  1. Nagios页面介绍(四)

    四.nagios页面介绍 Nagios 4.0.8版本登录后图片

  2. autofac 初步学习

    //数据处理接口 public interface IDal<T> where T : class { void Insert (T model); void Update(T model ...

  3. 打印多边形的菱形(for的嵌套)

    Console.WriteLine("请输入一个数字,会出现一个多边的菱形:"); int n = Convert.ToInt32(Console.ReadLine()); ; i ...

  4. Java Web 设置默认首页

    一.问题描述 这里所谓的默认首页,是指在访问项目根目录时(如 http://localhost:8080/zhx-web/ )展示的页面,通过在web.xml里配置 <welcome-file- ...

  5. dedecms首页调用栏目内容和单页内容的方法

    常用的需要调到首页来的单页内容,比如企业简介.联系我们等等内容,我们在首页可能都要进行体现.通过常规的方式,包括查阅dede官方论坛资料,都找不到比较合适的答案.今天我们就提供两种方式进行调用. 我们 ...

  6. struts返回json

    <param name="includeProperties"> </param> 这个属性表示要包含进JSON数据中的数据.<param name= ...

  7. java gc的工作原理、如何优化GC的性能、如何和GC进行有效的交互

    java gc的工作原理.如何优化GC的性能.如何和GC进行有效的交互 一个优秀的Java 程序员必须了解GC 的工作原理.如何优化GC的性能.如何和GC进行有效的交互,因为有一些应用程序对性能要求较 ...

  8. Java Socket发送与接收HTTP消息简单实现

    在上次Java Socket现实简单的HTTP服务我 们实现了简单的HTTP服务,它可以用来模拟HTTP服务,用它可以截获HTTP请求的原始码流,让我们很清楚的了解到我们向服务发的HTTP消息的结 构 ...

  9. Android-调用优酷SDK上传视频

    最近在研究用优酷的SDK来进行视频上传的功能,由于优酷的SDK只是提供了一个上传的sample code,没有涉及到授权的过程,对于新手来说,可能非常棘手,现在分享一下我的思路:   程序实现前我们先 ...

  10. C语言异常处理和连接数据库

    #include <stdio.h> #include <setjmp.h> jmp_buf j; void Exception(void); double diva(doub ...