前端MVC设计模式

前言

  前端的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的设计模式具备一定的优势。

  

参考:
1. http://www.cnblogs.com/rush/archive/2013/04/29/3051191.html
2. http://www.cnblogs.com/ego/archive/2009/03/06/1404328.html
3.  http://www.programmer.com.cn/15552/
4.  http://www.infoq.com/cn/news/2012/05/js-mvc-framework
5.  http://addyosmani.com/blog/understanding-mvc-and-mvp-for-javascript-and-backbone-developers/
6.  http://www.cnblogs.com/2018/archive/2011/05/20/2045893.html
7.  http://www.codeproject.com/Articles/228214/Understanding-Basics-of-UI-Design-Pattern-MVC-MVP
8.  http://www.cnblogs.com/piaopiao7891/archive/2012/09/04/2670390.html
zy531
关注 关注
  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
MVC设计模式概念及优缺点
08-18
MVC三层架构 MVC是 模型(Model),视图(View)和控制(Controller)的缩写,其目的实现Web系统的职能分工。其中Model层实现系统中的业务逻辑,通常可以用JavaBean或EJB来实现; View层用于与用户的交互,通常用JSP来实现...
前端常见八大设计模式
m0_57307213的博客
09-22 1万+
设计模式
JavaScript设计模式es6(23种)
IT回忆录
01-01 2716
设计模式简介 设计模式代表了最佳的实践,通常被有经验的面向对象的软件开发人员所采用。设计模式是软件开发人员在软件开发过程中面临的一般问题的解决方案。这些解决方案是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的。 设计模式是一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了重用代码、让代码更容易被他人理解、保证代码可靠性。 毫无疑问,设计模式于己于...
前端常见的设计模式
最新发布
qq_53742640的博客
02-16 1493
js复制代码// 需求:新员工入职,按照规定流程,进行相关培训和办理好员工相关资料init() {// 初始化员工信息// 创建员工名片// 入职培训// 训后测试。
前端常用设计模式简介
weixin_42046203的博客
07-14 392
设计模式的原则是找出程序中的变化,并将变化封装起来,实现高效的可复用性。核心在于意图,而不在结构。通过设计模式可以帮助我们增强代码的可重用性、可扩充性、 可维护性、灵活性。我们使用设计模式的最终目的是为了实现代码的高类聚和低耦合。你是否思考过这样的一个问题,如何让代码写的更有健壮性,其实核心在于把握变与不变。确保变的部分更加灵活,不变的地方更加稳定,而使用设计模式可以让我们达到这样的目的。
前端MVC
macycle的博客
07-24 1852
1.MVC是什么 mvc是一种架构设计模式,它包含三类对象,分别是M(model)层、V(view)层、C(controller)层。将这三层分离以提高灵活性与复用性。 M-Model(数据模型)用于封装与应用程序的业务逻辑相关的数据以及对数据的处理方法,会有一个或多个视图监听此模型。一旦模型的数据发生变化,模型将通知有关的视图。 const m = { data: { n: parseInt(localStorage.getItem('n')) }, create(){}, d
mode封装model层 +controller层控制router路由
lucky的专栏
06-25 784
app /** * app.js 入门模块 * 职责: * 创建服务 * 做一些服务相关配置 * 模板引擎 * body-parser 解析表单 post 请求体 * 提供静态资源服务 * 挂载路由 * 监听端口启动服务 */ var express = require('express') var router = re...
前端之Android入门:MVC模式(中)
03-03
MVC模式的最基本概念是分层设计,把我们的代码基于View(视图)、Model(模型)、Controller(控制器)进行分类封装,这样做的目的是为了清晰结构,使代码更易维护和扩展。在上一篇文章中,我们完成了计算器的界面...
图书管理系统,使用MVC设计模式(vo,dao,service,servlet),前端采用Bootstrap
07-05
简单的图书管理系统,前端采用了Bootstrap,后端采用MVC设计模式,包括:vo,dao,service,servlet,filter,listener;整个项目实现了图书的增删改查,出入库操作,管理员的增删改查操作,操作日志记录,登录...
前端框架系列之(mvc
vv_bug
06-16 1935
前言 前面我们写了三篇关于es装饰器的文章了,感兴趣的可以去看看: 前端框架系列之(装饰器Decorator) 前端框架系列之(vue-class-component) 前端框架系列之(vue-property-decorator) 简介 MVC 是一种使用 MVC(Model View Controller 模型-视图-控制器)设计创建 Web 应用程序的模式: Model(模型)表示应用程序核心(比如数据库记录列表)。 View(视图)显示数据(数据库记录)。 Controller(控制器)处理输
前端常见设计模式
quitv的博客
04-04 856
外观模式(Facade Pattern)是一种结构型设计模式,其主要目的是简化复杂系统的接口并提供一个更高级别的接口以供外部使用。可以将外观模式想象成一个门面或者外观,类似于房子的门面,它把整个系统隐藏在其背后。对于外部使用者而言,只需要通过门面提供的接口来操作系统,而不需要关心背后的实现细节。外观模式的一个生动的例子是手机的操作界面。手机的操作界面为用户提供了一个简单易用的接口,可以通过点击屏幕上的图标、按钮来进行操作,但实际上在背后有许多不同的系统组件在协作工作。
前端开发中常用的几种设计模式
热门推荐
纸飞机博客
08-17 2万+
设计模式是对软件设计开发过程中反复出现的某类问题的通用解决方案。设计模式更多的是指导思想和方法论,而不是现成的代码,当然每种设计模式都有每种语言中的具体实现方式。学习设计模式更多的是理解各种模式的内在思想和解决的问题,毕竟这是前人无数经验总结成的最佳实践,而代码实现则是对加深理解的辅助。 设计模式可以分为三大类: 结构型模式(Structural Patterns):通过识别系统中组件间的简单关系来简化系统的设计。 创建型模式(Creational Patterns):处理对象的创..
你应该知道的 9 种 前端设计模式
caishijian2008的专栏
05-19 892
本篇文章给大家介绍 9 种 前端设计模式。有一定的参考价值,有需要的朋友可以参考一下,希望对大家有所帮助。 什么是设计模式设计模式是对软件设计开发过程中反复出现的某类问题的通用解决方案。设计模式更多的是指导思想和方法论,而不是现成的代码,当然每种设计模式都有每种语言中的具体实现方式。学习设计模式更多的是理解各种模式的内在思想和解决的问题,毕竟这是前人无数经验总结成的最佳实践,而代码实现则是对加深理解的辅助。 设计模式的类型 设计模式可以分为三大类: 结构型模式(Structural P
前端常用的设计模式
weixin_47964837的博客
09-15 732
前端常用设计模式
前端框架MVC和MVVM的理解
追逐丶的博客
08-19 7775
一、概念解释 MVVM 是Model-View-ViewModel 的缩写,它是一种基于前端开发的架构模式,其核心是提供对View 和 ViewModel 的双向数据绑定,这使得ViewModel 的状态改变可以自动传递给 View,即所谓的数据双向绑定。 以Vue.js 为例。Vue是一个提供了 MVVM 风格的双向数据绑定的 Javascript 库,专注于View 层。它的核心是 MVVM ......
前端开发中常用设计模式-总结篇
weixin_43805705的博客
08-29 1836
本文是向大家介绍前端开发中常用的设计模式,它使我们编写的代码更容易被复用,也更容易被人理解,并且保证代码的稳定可靠性。
前端MVC(整理)
u011402106的博客
05-12 1281
MVC是一种设计模式,它将应用划分为3个部分:M-Model数据(模型)、V-View展示层(视图)、C-Controller用户交互(控制器) MVC允许在不改变视图的情况下改变视图对用户输入的响应方式,用户对view的操作交给了Controller处理,在Controller中响应View的事件调用Model的接口对数据进行操作,一旦Model发生变化便通知相关视图进行更新。 如果前端没有框架,只有使用原生html+js,MVC模式可以这样理解。将html看成view,js看成controller,负
前端常用的8种设计模式
M_Edison的博客
02-19 3245
文章目录1.引入2.单例模式3.装饰器模式3.适配器模式4.观察者模式(发布订阅模式)5.策略模式6.模板模式7.代理模式8.外观模式9.面试点 1.引入 简介: 设计模式代表了最佳的实践,通常被有经验的面向对象的软件开发人员所采用。设计模式是软件开发人员在软件开发过程中面临的一般问题的解决方案。 设计模式是一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总结。 使用设计模式是为了重用代码、让代码更容易被他人理解、保证代码可靠性。 毫无疑问,设计模式于己于他人于系统都是多赢的,设计模式使代码
q前端框架中设计模式
08-14
前端框架中也可以应用设计模式来帮助开发人员更好地组织和管理代码。有一些常见的设计模式前端开发中被广泛使用,例如: 1. MVC模式(Model-View-Controller):MVC模式是一种常见的架构模式,用于将应用程序的逻辑、数据和用户界面分离。在前端开发中,可以使用MVC模式来将数据、视图和控制器分离,以便更好地组织和管理代码。 2. 观察者模式(Observer Pattern):观察者模式用于实现对象之间的一对多依赖关系。在前端开发中,观察者模式可以用于实现事件监听和响应,例如当用户点击按钮时,触发相应的事件处理函数。 3. 单例模式(Singleton Pattern):单例模式用于确保一个类只有一个实例,并提供一个全局访问点。在前端开发中,单例模式可以用于管理全局状态、共享资源或提供统一的配置管理。 4. 工厂模式(Factory Pattern):工厂模式用于创建对象的过程中,将创建逻辑和具体对象的实现分离。在前端开发中,工厂模式可以用于创建不同类型的对象,例如根据用户的角色类型创建不同的导航菜单。 以上只是几个常见的设计模式前端开发中的应用,实际上还有更多的设计模式可以用于解决特定的问题。对于前端开发人员来说,了解不同的设计模式,并根据具体的需求进行选择和应用,可以帮助提高代码的可维护性和扩展性。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [前端开发中常用的几种设计模式](https://blog.csdn.net/shadowfall/article/details/112001884)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
写文章

热门文章

  • 常见的 Kerberos 错误消息 11466
  • Challenge/Response认证 5994
  • Bash字符串处理(获取文件名和后缀名) 5650
  • linux中fcntl()、lockf、flock的区别 4092
  • Kerberos V5 Library Error Codes 4020

分类专栏

  • 安全 1篇
  • C++ 13篇
  • linux 16篇

最新评论

  • PHP操作证书

    2301_77448004: 大佬,想请教下关于证书的检验一般考虑哪些方面?该怎么做呢?

  • hex2bin

    普通网友: 码住,求博主联系方式,我的微信cto51shequ,在线等回复

  • OpenSSL API 签发证书

    开源中国系统管理员: 您好,麻烦问下,c 调用openssl生成双向证书的话该怎么实现呀

  • SQL Server 2005数据库日志文件损坏的情况下如何恢复数据库

    wang哥: 也可以直接从mdf里面提取table ,procedure,view等,生成新的数据库。再附加即可。QQ:80554803

  • OpenSSL API 签发证书

    Vince352: 你好,请问我编译的时候报错,错误内容为, /bin/ld: ./src/LhtwSSL.o: relocation R_X86_64_32 against `.rodata' can not be used when making a shared object; recompile with -fPIC ./src/LhtwSSL.o: could not read symbols: 错误的值 collect2: 错误:ld 返回 1 make: *** [libLhtwSSL.so] 错误 1 是不是除了.h文件还要导入什么文件啊?如何导入?

您愿意向朋友推荐“博客详情页”吗?

  • 强烈不推荐
  • 不推荐
  • 一般般
  • 推荐
  • 强烈推荐
提交

最新文章

  • WEB/系统安全
  • Linux中flock和fcntl区别
  • linux中fcntl()、lockf、flock的区别
2019年1篇
2016年3篇
2015年20篇
2014年20篇
2013年3篇
2012年7篇
2011年7篇
2010年8篇
2009年2篇

目录

目录

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值

深圳SEO优化公司龙岩网站优化报价松原seo按天扣费哪家好阜阳市网站设计公司芜湖市网站改版多少钱常德市网站建设哪家专业柳州市网络推广哪家专业内江市网站建设哪家专业襄阳市seo按天计费价格南联阿里店铺运营哪家好泰安网站定制推荐北京市网站建设哪家专业黄山市网页制作多少钱福田网站建设多少钱资阳市网站设计公司衢州市建站宜宾市模板网站建设公司晋城seo按天计费价格阜新seo按天计费推荐周口网站设计衡阳市网站制作多少钱辽源seo按天扣费河源市seo按天计费公司乌海市品牌网站设计鹤岗阿里店铺运营柳州市网站制作多少钱沧州市网站推广哪家专业荆州网页设计多少钱抚州网页制作多少钱兴安盟模板网站建设多少钱南京市seo排名哪家专业歼20紧急升空逼退外机英媒称团队夜以继日筹划王妃复出草木蔓发 春山在望成都发生巨响 当地回应60岁老人炒菠菜未焯水致肾病恶化男子涉嫌走私被判11年却一天牢没坐劳斯莱斯右转逼停直行车网传落水者说“没让你救”系谣言广东通报13岁男孩性侵女童不予立案贵州小伙回应在美国卖三蹦子火了淀粉肠小王子日销售额涨超10倍有个姐真把千机伞做出来了近3万元金手镯仅含足金十克呼北高速交通事故已致14人死亡杨洋拄拐现身医院国产伟哥去年销售近13亿男子给前妻转账 现任妻子起诉要回新基金只募集到26元还是员工自购男孩疑遭霸凌 家长讨说法被踢出群充个话费竟沦为间接洗钱工具新的一天从800个哈欠开始单亲妈妈陷入热恋 14岁儿子报警#春分立蛋大挑战#中国投资客涌入日本东京买房两大学生合买彩票中奖一人不认账新加坡主帅:唯一目标击败中国队月嫂回应掌掴婴儿是在赶虫子19岁小伙救下5人后溺亡 多方发声清明节放假3天调休1天张家界的山上“长”满了韩国人?开封王婆为何火了主播靠辱骂母亲走红被批捕封号代拍被何赛飞拿着魔杖追着打阿根廷将发行1万与2万面值的纸币库克现身上海为江西彩礼“减负”的“试婚人”因自嘲式简历走红的教授更新简介殡仪馆花卉高于市场价3倍还重复用网友称在豆瓣酱里吃出老鼠头315晚会后胖东来又人满为患了网友建议重庆地铁不准乘客携带菜筐特朗普谈“凯特王妃P图照”罗斯否认插足凯特王妃婚姻青海通报栏杆断裂小学生跌落住进ICU恒大被罚41.75亿到底怎么缴湖南一县政协主席疑涉刑案被控制茶百道就改标签日期致歉王树国3次鞠躬告别西交大师生张立群任西安交通大学校长杨倩无缘巴黎奥运

深圳SEO优化公司 XML地图 TXT地图 虚拟主机 SEO 网站制作 网站优化