侃侃前端MVC設計模式

前言

  前端的MVC,近幾年一直很火,你們也都紛紛討論着,因而乎,抽空總結一下這個知識點。看了些文章,結合實踐略做總結並發表一下本身的見解。  javascript

  最初接觸MVC是後端Java的MVC架構,用一張圖來表示之——css


  這樣,咱們讓每個層次去關注並作好一件事情,層與層之間保持鬆耦合,咱們能夠對每個層次單獨作好測試工做。如此,咱們可讓代碼更具可維護性。
  所以,借鑑於後端的這種MVC設計思想(更多的我想是一種優秀的、通過考驗的實踐模式),針對愈來愈複雜的JavaScript應用程序,便有了猜測,咱們是否可使用MVC的設計思想,編寫出高維護性的前端程序。
 
1、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架構原則的。模型是最應該從你的應用中解耦出來的部分。
當控制器從服務器抓取數據或建立新的記錄時,它就將數據包裝成模型實例。也就是說,咱們的數據是面向對象的,任何定義在這個數據模型上的函數或邏輯均可以直接被調用。html

  2>  視圖——

視圖層是呈現給用戶的,用戶與之產生交互。在JavaScript應用中,視圖大都是由HTML、CSS、JavaScript模板組成的。除了模板中簡單的條件語句以外,視圖不該當包含任何其餘邏輯。
將邏輯混入視圖之中是編程的大忌,這並非說MVC不容許包含視覺呈現相關的邏輯,只要這部分邏輯沒有定義在視圖以內便可。咱們將視覺呈現邏輯歸類爲「視圖助手」(helper):和視圖相關的獨立的小工具函數。
來看下面的例子,騎在視圖中包含了邏輯,這是一個範例,平時不該當這樣作:前端

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

在這段代碼中,咱們把formatDate()函數直接插入視圖中,這違反了MVC的原則,結果致使標籤看上去像大雜燴同樣不可維護。能夠將視覺呈現邏輯剝離出來放入試圖助手中,正以下面的代碼就避免了這個問題,可讓這個應用的結構知足MVC。java

// helper.js
var helper = {};
helper.formateDate(date) {
/* ... */
};

// template.html
<div>
    ${ helper.formate(this.date) }
</div>

此外,全部視覺呈現邏輯都包含在helper變量中,這是一個命名空間,能夠防止衝突並保持代碼清晰、可擴展。jquery

  3>  控制器——

控制器是模型和視圖之間的紐帶。控制器從視圖獲取事件和輸入,對它們(極可能包含模型)進行處理,並相應地更新視圖。當頁面加載時,控制器會給視圖添加事件監聽,好比監聽表單提交或按鈕點擊。而後,當用戶和你的應用產生交互時,控制器中的事件觸發器就開始工做了。
咱們用簡單的jQuery代碼來實現控制器——git

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的實現。
 
2、使用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.jsgithub

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的業務邏輯,並無嚴格抽象出來,而是寫入對應的事件當中。

  接下來,看看其餘優秀的框架如何去作的。ajax

 

3、前端MVC框架express

  相信你們都聽過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經過命令來控制

 

4、前端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邏輯的頻繁變化(這裏僅僅針對富應用程序)。

 

5、總結  

  總之,既然學習了MVC這個設計模式,固然,咱們不必定非要去採用某一個框架(學習曲線、嵌入性、文件大小、兼容性、應用場景等等咱們都要進行考慮),咱們無需放大前端框架的做用,咱們須要領會的僅僅是其在前端應用的思想。就像最初jQuery模擬實現MVC的方式同樣,我再來總結幾個關鍵點——

1.構造模型Model
2.分離事件綁定,造成Controller
3.維護模型Model(and 模型集合Model Collection),經過Model的改變,通知對應的View從新渲染
4.分離View顯示邏輯

  這樣,咱們藉助MVC的設計思想,可以現有代碼進行重構,固然也可以對將來的代碼進行必定展望。

  固然,每個項目都有自身的特色,我的認爲,針對富應用(尤爲對增刪改的操做佔比較大的比例)的項目,MVC的設計模式具有必定的優點。

  

參考:
相關文章
相關標籤/搜索