前言

  前端的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):和视图相关的独立的小工具函数。
来看下面的例子,骑在视图中包含了逻辑,这是一个范例,平时不应当这样做:

  1. <div>
  2. <script>
  3. function formatDate(date) {
  4. /* ... */
  5. }
  6. </script>
  7. ${ formateDate(this.date) }
  8. </div>

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

  1. // helper.js
  2. var helper = {};
  3. helper.formateDate(date) {
  4. /* ... */
  5. };
  6.  
  7. // template.html
  8. <div>
  9. ${ helper.formate(this.date) }
  10. </div>

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

  3> 控制器——

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

  1. var Controller = {};
  2.  
  3. (Controller.users = function($) {
  4. var nameClick = function() {
  5. /* ... */
  6. };
  7.  
  8. // 在页面加载时绑定事件监听
  9. $(function() {
  10. $('#view .name').click(nameClick);
  11. });
  12. })(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
  1. <section id="todoapp">
  2. <header id="header">
  3. <h1>todos</h1>
  4. <input id="new-todo" placeholder="What needs to be done?" autofocus>
  5. </header>
  6. <section id="main">
  7. <input id="toggle-all" type="checkbox">
  8. <label for="toggle-all">Mark all as complete</label>
  9. <ul id="todo-list"></ul>
  10. </section>
  11. <footer id="footer">
  12. <span id="todo-count"><strong>0</strong> item left</span>
  13. <button id="clear-completed">Clear completed</button>
  14. </footer>
  15. </section>
  16. <footer id="info">
  17. <p>Double-click to edit a todo</p>
  18. <p>Created by <a href="http://github.com/sindresorhus">Sindre Sorhus</a></p>
  19. <p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
  20. </footer>
  21.  
  22. <!-- ************************************* template begin *********************************** -->
  23. <!-- 针对模型的模板 -->
  24. <script id="todo-template" type="text/x-handlebars-template">
  25. <!-- 这里对todo模型数组进行迭代循环 -->
  26. {{#this}}
  27. <!-- 会看到,这里具有简单的if语句,即这里具备显示逻辑 -->
  28. <li {{#if completed}}class="completed"{{/if}} data-id="{{id}}">
  29. <div class="view">
  30. <input class="toggle" type="checkbox" {{#if completed}}checked{{/if}}>
  31. <label>{{title}}</label>
  32. <button class="destroy"></button>
  33. </div>
  34. <input class="edit" value="{{title}}">
  35. </li>
  36. {{/this}}
  37. </script>
  38. <!-- /针对模型的模板 -->
  39. <!-- footer模板,记录还剩下多少没有完成等 -->
  40. <script id="footer-template" type="text/x-handlebars-template">
  41. <span id="todo-count"><strong>{{activeTodoCount}}</strong> {{activeTodoWord}} left</span>
  42. {{#if completedTodos}}
  43. <button id="clear-completed">Clear completed ({{completedTodos}})</button>
  44. {{/if}}
  45. </script>
  46. <!-- /footer模板 -->
  47. <!-- ************************************* template end *********************************** -->
  48.  
  49. <script src="js/base/base.js"></script>
  50. <script src="js/lib/jquery.js"></script>
  51. <script src="js/lib/handlebars.js"></script>
  52.  
  53. <!-- app begin -->
  54. <script src="js/app.js"></script>

app.html

  2> app.js

  1. jQuery(function() {
  2. 'use strict';
  3.  
  4. // 这里是一些工具函数的抽取,包括
  5. // 1.ID生成器
  6. // 2.显示格式化
  7. // 3.localStorage存储
  8. var Utils = {
  9. uuid : function() {
  10. /*jshint bitwise:false */
  11. var i, random;
  12. var uuid = '';
  13.  
  14. for ( i = 0; i < 32; i++) {
  15. random = Math.random() * 16 | 0;
  16. if (i === 8 || i === 12 || i === 16 || i === 20) {
  17. uuid += '-';
  18. }
  19. uuid += (i === 12 ? 4 : (i === 16 ? (random & 3 | 8) : random)).toString(16);
  20. }
  21.  
  22. return uuid;
  23. },
  24. pluralize : function(count, word) {
  25. return count === 1 ? word : word + 's';
  26. },
  27. store : function(namespace, data) {
  28. if (arguments.length > 1) {
  29. return localStorage.setItem(namespace, JSON.stringify(data));
  30. } else {
  31. var store = localStorage.getItem(namespace);
  32. return (store && JSON.parse(store)) || [];
  33. }
  34. }
  35. };
  36.  
  37. var Todo = function(id, title, completed) {
  38. this.id = id;
  39. this.title = title;
  40. this.completed = completed;
  41. }
  42.  
  43. var App = {
  44.  
  45. init: function() {
  46. this.ENTER_KEY = 13;
  47. this.todos = Utils.store('todos-jquery');
  48. this.cacheElements();
  49. this.bindEvents();
  50. },
  51.  
  52. // 这里是缓存一些必要的dom节点,提高性能
  53. cacheElements: function() {
  54. this.todoTemplate = Handlebars.compile($('#todo-template').html());
  55. this.footerTemplate = Handlebars.compile($('#footer-template').html());
  56. this.$todoApp = $('#todoapp');
  57. this.$header = this.$todoApp.find('#header');
  58. this.$main = this.$todoApp.find('#main');
  59. this.$footer = this.$todoApp.find('#footer');
  60. this.$newTodo = this.$header.find('#new-todo');
  61. this.$toggleAll = this.$main.find('#toggle-all');
  62. this.$todoList = this.$main.find('#todo-list');
  63. this.$count = this.$footer.find('#todo-count');
  64. this.$clearBtn = this.$footer.find('#clear-completed');
  65. },
  66.  
  67. // 模拟Controller实现:所有的事件监听在这里绑定
  68. bindEvents: function() {
  69. var list = this.$todoList;
  70. this.$newTodo.on('keyup', this.create);
  71. this.$toggleAll.on('change', this.toggleAll);
  72. this.$footer.on('click', '#clear-completed', this.destroyCompleted);
  73. list.on('change', '.toggle', this.toggle);
  74. list.on('dblclick', 'label', this.edit);
  75. list.on('keypress', '.edit', this.blurOnEnter);
  76. list.on('blur', '.edit', this.update);
  77. list.on('click', '.destroy', this.destroy);
  78. },
  79.  
  80. // 渲染记录列表:当模型数据发生改变的时候,对应的事件处理程序调用该方法,从而实现对应DOM的重新渲染
  81. render: function() {
  82. this.$todoList.html(this.todoTemplate(this.todos));
  83. this.$main.toggle(!!this.todos.length);
  84. this.$toggleAll.prop('checked', !this.activeTodoCount());
  85. this.renderFooter();
  86. Utils.store('todos-jquery', this.todos);
  87. },
  88.  
  89. // 渲染底部
  90. renderFooter: function () {
  91. var todoCount = this.todos.length;
  92. var activeTodoCount = this.activeTodoCount();
  93. var footer = {
  94. activeTodoCount: activeTodoCount,
  95. activeTodoWord: Utils.pluralize(activeTodoCount, 'item'),
  96. completedTodos: todoCount - activeTodoCount
  97. };
  98.  
  99. this.$footer.toggle(!!todoCount);
  100. this.$footer.html(this.footerTemplate(footer));
  101. },
  102.  
  103. // 创建记录
  104. create: function (e) {
  105. var $input = $(this);
  106. var val = $.trim($input.val());
  107.  
  108. if (e.which !== App.ENTER_KEY || !val) {
  109. return;
  110. }
  111.  
  112. App.todos.push({
  113. id: Utils.uuid(),
  114. title: val,
  115. completed: false
  116. });
  117.  
  118. // 记录添加后,通知重新渲染页面
  119. App.render();
  120. },
  121.  
  122. // 其他业务逻辑函数
  123. edit: function() {},
  124. destroy: function() {}
  125. /* ... */
  126.  
  127. }
  128.  
  129. App.init();
  130.  
  131. });

app.js

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

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

三、前端MVC框架

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

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

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

  1> Ember.js(MVC)

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

  

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

  1. <script type="text/x-handlebars" data-template-name="todos">
  2. <section id="todoapp">
  3. <header id="header">
  4. <h1>todos</h1>
  5. <!-- 这里的action属性指定了对应的TodosController中的createTodo方法 -->
  6. {{input id="new-todo" type="text" value=newTitle action="createTodo" placeholder="What needs to be done?"}}
  7. </header>
  8. {{#if length}}
  9. <section id="main">
  10. <ul id="todo-list">
  11. {{#each filteredTodos itemController="todo"}}
  12. <li {{bind-attr class="isCompleted:completed isEditing:editing"}}>
  13. {{#if isEditing}}
  14. {{edit-todo class="edit" value=bufferedTitle focus-out="doneEditing" insert-newline="doneEditing" escape-press="cancelEditing"}}
  15. {{else}}
  16. {{input type="checkbox" class="toggle" checked=isCompleted}}
  17. <label {{action "editTodo" on="doubleClick"}}>{{title}}</label>
  18. <button {{action "removeTodo"}} class="destroy"></button>
  19. {{/if}}
  20. </li>
  21. {{/each}}
  22. </ul>
  23. {{input type="checkbox" id="toggle-all" checked=allAreDone}}
  24. </section>
  25. <footer id="footer">
  26. <span id="todo-count">{{{remainingFormatted}}}</span>
  27. <ul id="filters">
  28. <li>
  29. {{#link-to "todos.index" activeClass="selected"}}All{{/link-to}}
  30. </li>
  31. <li>
  32. {{#link-to "todos.active" activeClass="selected"}}Active{{/link-to}}
  33. </li>
  34. <li>
  35. {{#link-to "todos.completed" activeClass="selected"}}Completed{{/link-to}}
  36. </li>
  37. </ul>
  38. {{#if hasCompleted}}
  39. <button id="clear-completed" {{action "clearCompleted"}}>
  40. Clear completed ({{completed}})
  41. </button>
  42. {{/if}}
  43. </footer>
  44. {{/if}}
  45. </section>
  46. <footer id="info">
  47. <p>Double-click to edit a todo</p>
  48. <p>
  49. Created by
  50. <a href="http://github.com/tomdale">Tom Dale</a>,
  51. <a href="http://github.com/addyosmani">Addy Osmani</a>
  52. </p>
  53. <p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
  54. </footer>
  55. </script>

index.html

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

  1. window.Todos = Ember.Application.create();
  紧接着我们需要渲染模板中的数据,由于渲染模板的内容是根据路由选择后动态获取的模板内容,当我们的应用程序启动时,路由是负责显示模板,加载数据,以及管理应用程序的状态。
  在router.js中——
  1. Todos.Router.map(function () {
  2. this.resource('todos', { path: '/' }, function () {
  3. this.route('active');
  4. this.route('completed');
  5. });
  6. });
  7. // 这里进行了硬绑定,即对应的模板名字为data-template-name="todos"
  8. Todos.TodosRoute = Ember.Route.extend({
  9. model: function () {
  10. // 显示设定该路由的的model数据
  11. // return this.store.find('todo');
  12. return [{
  13. id: 1,
  14. title: 'todo1',
  15. compeled: false
  16. }];
  17. }
  18. });
  19.  
  20. // 下面定义了三个子路由
  21. // #/index
  22. Todos.TodosIndexRoute = Ember.Route.extend({
  23. setupController: function () {
  24. // 显示定义对应的controller程序
  25. this.controllerFor('todos').set('filteredTodos', this.modelFor('todos'));
  26. }
  27. });
  28.  
  29. // #/active
  30. Todos.TodosActiveRoute = Ember.Route.extend({
  31. setupController: function () {
  32. var todos = this.store.filter('todo', function (todo) {
  33. return !todo.get('isCompleted');
  34. });
  35.  
  36. this.controllerFor('todos').set('filteredTodos', todos);
  37. }
  38. });
  39.  
  40. // #/completed
  41. Todos.TodosCompletedRoute = Ember.Route.extend({
  42. setupController: function () {
  43. var todos = this.store.filter('todo', function (todo) {
  44. return todo.get('isCompleted');
  45. });
  46.  
  47. this.controllerFor('todos').set('filteredTodos', todos);
  48. }
  49. });

router.js

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

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

  1. Todos.TodosController = Ember.ArrayController.extend({
  2.  
  3. // 针对model集合的的交互在这里定义
  4. actions: {
  5. // 该方法的调用时在对应的dom节点中进行绑定,即对应模板中的下列语句
  6. // {{input id="new-todo" type="text" value=newTitle action="createTodo" placeholder="What needs to be done?"}}
  7. createTodo: function() {
  8. var title, todo;
  9.  
  10. title = this.get('newTitle').trim();
  11. if (!title) {
  12. return;
  13. }
  14.  
  15. todo = {
  16. title: title,
  17. isCompleted: false
  18. };
  19.  
  20. todo.save();
  21.  
  22. this.set('newTitle', '');
  23.  
  24. },
  25. /* ... */
  26. },
  27.  
  28. // 以下主要定义显示逻辑
  29. remaining: function () {
  30. return this.filterProperty('isCompleted', false).get('length');
  31. }.property('@each.isCompleted'),
  32.  
  33. // 对应的dom调用时<span id="todo-count">{{{remainingFormatted}}}</span>
  34. remainingFormatted: function () {
  35. var remaining = this.get('remaining');
  36. var plural = remaining === 1 ? 'item' : 'items';
  37. return '<strong>%@</strong> %@ left'.fmt(remaining, plural);
  38. }.property('remaining'),
  39. /* ... */
  40.  
  41. });

todos-controller.js

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

  1. Todos.TodoController = Ember.ObjectController.extend({
  2.  
  3. isEditing: false,
  4.  
  5. // 缓存title
  6. bufferedTitle: Ember.computed.oneWay('title'),
  7.  
  8. // 这里包含了对单条记录的所有增删改查的操作
  9. actions: {
  10.  
  11. editTodo: function() {
  12. this.set('isEditing', true);
  13. },
  14.  
  15. doneEditing: function() {
  16. var bufferedTitle = this.get('bufferedTitle').trim();
  17.  
  18. if (Ember.isEmpty(bufferedTitle)) {
  19. Ember.run.debounce(this, this.send, 'removeTodo', 0);
  20. } else {
  21. var todo = this.get('model');
  22. todo.set('title', bufferedTitle);
  23. todo.save();
  24. }
  25.  
  26. this.set('bufferedTitle', bufferedTitle);
  27. this.set('isEditing', false);
  28. },
  29.  
  30. cancelEditing: function() {
  31. this.set('bufferedTitle', this.get('title'));
  32. this.set('Editing', false);
  33. },
  34.  
  35. removeTodo: function() {
  36. var todo = this.get('model');
  37.  
  38. todo.deleteRecord();
  39. todo.save();
  40. }
  41. }
  42. });

todo-controller.js

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

  1. <ul id="todo-list">
  2. {{#each filteredTodos itemController="todo"}}
  3. <li {{bind-attr class="isCompleted:completed isEditing:editing"}}>
  4. {{#if isEditing}}
  5. {{edit-todo class="edit" value=bufferedTitle focus-out="doneEditing" insert-newline="doneEditing" escape-press="cancelEditing"}}
  6. {{else}}
  7. {{input type="checkbox" class="toggle" checked=isCompleted}}
  8. <label {{action "editTodo" on="doubleClick"}}>{{title}}</label>
  9. <button {{action "removeTodo"}} class="destroy"></button>
  10. {{/if}}
  11. </li>
  12. {{/each}}
  13. </ul>

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

  1. focus-out
  2. insert-newline
  3. escape-press
  4. doubleClick

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

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

  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,它的模板比较清爽——

  1. <script type="text/template" id="item-template">
  2. <div class="view">
  3. <input class="toggle" type="checkbox" <%= completed ? 'checked' : '' %>>
  4. <label><%- title %></label>
  5. <button class="destroy"></button>
  6. </div>
  7. <input class="edit" value="<%- title %>">
  8. </script>
  9.  
  10. <script type="text/template" id="stats-template">
  11. <span id="todo-count">
  12. <strong><%= remaining %></strong><%= remaining === 1 ? 'item' : 'items' %> left
  13. </span>
  14. <ul id="filters">
  15. <li>
  16. <a class="selected" href="#/">All</a>
  17. </li>
  18. <li>
  19. <a href="#/active">Active</a>
  20. </li>
  21. <li>
  22. <a href="#/completed">Completed</a>
  23. </li>
  24. </ul>
  25. <% if (completed) { %>
  26. <button id="clear-completed">Clear completed (<%= completed %>)</button>
  27. <% } %>
  28. </script>

模板代码

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

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

  1. (function() {
  2. 'use strict';
  3.  
  4. var Todos = Backbone.Collection.extend({
  5. model: app.Todo,
  6.  
  7. localStorage: new Backbone.LocalStorage('todos-backbone'),
  8.  
  9. // Filter down the list of all todo items that are finished.
  10. completed: function () {
  11. return this.filter(function (todo) {
  12. return todo.get('completed');
  13. });
  14. },
  15.  
  16. // Filter down the list to only todo items that are still not finished.
  17. remaining: function () {
  18. return this.without.apply(this, this.completed());
  19. },
  20.  
  21. nextOrder: function() {
  22. if (this.length === 0) {
  23. return 1;
  24. }
  25. return this.last().get('order') + 1;
  26. },
  27.  
  28. //
  29. comparator: function(todo) {
  30. return todo.get('order');
  31. }
  32. });
  33.  
  34. app.todos = new Todos();
  35.  
  36. })();

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)

  先看看它的页面——

  1. <section id="todoapp" data-bind="">
  2. <header id="header">
  3. <h1>todos</h1>
  4. <input id="new-todo" data-bind="value: current, valueUpdate: 'afterkeydown', enterKey: add" placeholder="What needs to be done?" autofocus>
  5. </header>
  6. <section id="main" data-bind="visible: todos().length">
  7. <input id="toggle-all" data-bind="checked: allCompleted" type="checkbox">
  8. <label for="toggle-all">Mark all as complete</label>
  9. <ul id="todo-list" data-bind="foreach: filteredTodos">
  10. <li data-bind="css: { completed: completed, editing: editing }">
  11. <div class="view">
  12. <input class="toggle" data-bind="checked: completed" type="checkbox">
  13. <label data-bind="text: title, event: { dblclick: $root.editItem }"></label>
  14. <button class="destroy" data-bind="click: $root.remove"></button>
  15. </div>
  16. <input class="edit" data-bind="value: title, valueUpdate: 'afterkeydown', enterKey: $root.saveEditing, escapeKey: $root.cancelEditing, selectAndFocus: editing, event: { blur: $root.stopEditing }">
  17. </li>
  18. </ul>
  19. </section>
  20. <footer id="footer" data-bind="visible: completedCount() || remainingCount()">
  21. <span id="todo-count">
  22. <strong data-bind="text: remainingCount">0</strong>
  23. <span data-bind="text: getLabel(remainingCount)"></span> left
  24. </span>
  25. <ul id="filters">
  26. <li>
  27. <a data-bind="css: { selected: showMode() == 'all' }" href="#/all">All</a>
  28. </li>
  29. <li>
  30. <a data-bind="css: { selected: showMode() == 'active' }" href="#/active">Active</a>
  31. </li>
  32. <li>
  33. <a data-bind="css: { selected: showMode() == 'completed' }" href="#/completed">Completed</a>
  34. </li>
  35. </ul>
  36. <button id="clear-completed" data-bind="visible: completedCount, click: removeCompleted">
  37. Clear completed (<span data-bind="text: completedCount"></span>)
  38. </button>
  39. </footer>
  40. </section>
  41. <script src="js/base/base.js"></script>
  42. <script src="js/lib/knockout.js"></script>
  43. <script src="js/app.js"></script>

页面代码

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

  1. // 针对view来创建ViewModel
  2. var ViewModel = function (todos) {
  3.  
  4. // map array of passed in todos to an observableArray of Todo objects
  5. this.todos = ko.observableArray(todos.map(function (todo) {
  6. return new Todo(todo.title, todo.completed);
  7. }));
  8.  
  9. // store the new todo value being entered
  10. this.current = ko.observable();
  11.  
  12. this.showMode = ko.observable('all');
  13.  
  14. this.filteredTodos = ko.computed(function () {
  15. switch (this.showMode()) {
  16. case 'active':
  17. return this.todos().filter(function (todo) {
  18. return !todo.completed();
  19. });
  20. case 'completed':
  21. return this.todos().filter(function (todo) {
  22. return todo.completed();
  23. });
  24. default:
  25. return this.todos();
  26. }
  27. }.bind(this));
  28.  
  29. // add a new todo, when enter key is pressed
  30. this.add = function () {
  31. var current = this.current().trim();
  32. if (current) {
  33. this.todos.push(new Todo(current));
  34. this.current('');
  35. }
  36. };
  37.  
  38. // remove a single todo
  39. this.remove = function (todo) {
  40. this.todos.remove(todo);
  41. }.bind(this);
  42.  
  43. // remove all completed todos
  44. this.removeCompleted = function () {
  45. this.todos.remove(function (todo) {
  46. return todo.completed();
  47. });
  48. }.bind(this);
  49.  
  50. // edit an item
  51. this.editItem = function (item) {
  52. item.editing(true);
  53. item.previousTitle = item.title();
  54. }.bind(this);
  55.  
  56. // stop editing an item. Remove the item, if it is now empty
  57. this.saveEditing = function (item) {
  58. item.editing(false);
  59.  
  60. var title = item.title();
  61. var trimmedTitle = title.trim();
  62.  
  63. // Observable value changes are not triggered if they're consisting of whitespaces only
  64. // Therefore we've to compare untrimmed version with a trimmed one to chech whether anything changed
  65. // And if yes, we've to set the new value manually
  66. if (title !== trimmedTitle) {
  67. item.title(trimmedTitle);
  68. }
  69.  
  70. if (!trimmedTitle) {
  71. this.remove(item);
  72. }
  73. }.bind(this);
  74.  
  75. // cancel editing an item and revert to the previous content
  76. this.cancelEditing = function (item) {
  77. item.editing(false);
  78. item.title(item.previousTitle);
  79. }.bind(this);
  80.  
  81. // count of all completed todos
  82. this.completedCount = ko.computed(function () {
  83. return this.todos().filter(function (todo) {
  84. return todo.completed();
  85. }).length;
  86. }.bind(this));
  87.  
  88. // count of todos that are not complete
  89. this.remainingCount = ko.computed(function () {
  90. return this.todos().length - this.completedCount();
  91. }.bind(this));
  92.  
  93. // writeable computed observable to handle marking all complete/incomplete
  94. this.allCompleted = ko.computed({
  95. //always return true/false based on the done flag of all todos
  96. read: function () {
  97. return !this.remainingCount();
  98. }.bind(this),
  99. // set all todos to the written value (true/false)
  100. write: function (newValue) {
  101. this.todos().forEach(function (todo) {
  102. // set even if value is the same, as subscribers are not notified in that case
  103. todo.completed(newValue);
  104. });
  105. }.bind(this)
  106. });
  107.  
  108. // helper function to keep expressions out of markup
  109. this.getLabel = function (count) {
  110. return ko.utils.unwrapObservable(count) === 1 ? 'item' : 'items';
  111. }.bind(this);
  112.  
  113. // internal computed observable that fires whenever anything changes in our todos
  114. ko.computed(function () {
  115. // store a clean copy to local storage, which also creates a dependency on the observableArray and all observables in each item
  116. localStorage.setItem('todos-knockoutjs', ko.toJSON(this.todos));
  117. }.bind(this)).extend({
  118. throttle: 500
  119. }); // save at most twice per second
  120. };

ViewModel定义

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

  MVVM的特点如下——

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

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

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

  1. $('#update').click(function(e) {
  2. // 1.事件处理程序
  3. e.preventDefault();
  4.  
  5. // 2.获取对应的model的属性值
  6. var title = $('#text').val();
  7.  
  8. // 3.调用业务逻辑
  9. $.ajax({
  10. url : '/xxx',
  11. type : 'POST',
  12. data : {
  13. title : title,
  14. completed : false
  15. },
  16. success : function(data) {
  17. // 4.对data进行处理,并进行对应的dom渲染
  18. },
  19. error: function() {
  20. // 4.错误处理
  21. }
  22. });
  23.  
  24. });

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

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

五、总结  

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

  1. 1.构造模型Model
  2. 2.分离事件绑定,形成Controller
  3. 3.维护模型Modeland 模型集合Model Collection),通过Model的改变,通知对应的View重新渲染
  4. 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. SpringMVC实现一个controller里面有多个方法

    我们都知道,servlet代码一般来说只能在一个servlet中做判断去实现一个servlet响应多个请求, 但是springMVC的话还是比较方便的,主要有两种方式去实现一个controller里能 ...

  2. 由chrome剪贴板问题研究到了js模拟鼠标键盘事件

    写在前面 最近公司在搞浏览器兼容的事情,所有浏览器兼容的问题不得不一个人包了.下面来说一下今天遇到的一个问题吧 大家都知道IE下面如果要获得剪贴板里面的信息的话,代码应该如下所示 window.cli ...

  3. IOS基础之 (十五)知识点

    一 SEL 1. 方法的存储位置 每个类的方法地址列表都存储在类对象中. 每个方法都有一个与之对应的SEL类型的对象. 根据一个SEL对象就可以找到方法的地址,进而调用方法. Person.h #im ...

  4. poj1787Charlie's Change(多重背包+记录路径+好题)

    Charlie's Change Time Limit: 1000MS   Memory Limit: 30000K Total Submissions: 3720   Accepted: 1125 ...

  5. zabbix表结构

    zabbix数据库表结构的重要性 想理解zabbix的前端代码.做深入的二次开发,甚至的调优,那就不能不了解数据库的表结构了. 我们这里采用的zabbix1.8.mysql,所以简单的说下我们mysq ...

  6. hdu 2050 折线分割平面

    训练递推用题,第一次做这个题,蒙的,而且对了. #include <stdio.h> int main(void) { int c,a; scanf("%d",& ...

  7. Mac Sublime Text 2 简单使用

    按 Ctrl+` 调出 console 粘贴以下代码到底部命令行并回车: import urllib2,os;pf='Package Control.sublime-package';ipp=subl ...

  8. IIS负载均衡-Application Request Route详解第二篇:创建与配置Server Farm(转载)

    IIS负载均衡-Application Request Route详解第二篇:创建与配置Server Farm 自从本系列发布之后,收到了很多的朋友的回复!非常感谢,同时很多朋友问到了一些问题,有些问 ...

  9. Ubuntu 为网卡配置静态IP地址

    为网卡配置静态IP地址编辑文件/etc/network/interfaces:sudo vi /etc/network/interfaces并用下面的行来替换有关eth0的行:# The primar ...

  10. Flume-NG内置计数器(监控)源码级分析

    Flume的内置监控怎么整?这个问题有很多人问.目前了解到的信息是可以使用Cloudera Manager.Ganglia有图形的监控工具,以及从浏览器获取json串,或者自定义向其他监控系统汇报信息 ...