前端的MVC,近幾年一直很火,大家也都紛紛討論着,於是乎,抽空總結一下這個知識點。看了些文章,結合實踐略作總結並發表一下自己的看法。
最初接觸MVC是后端Java的MVC架構,用一張圖來表示之——

MVC是一種設計模式,它將應用划分為3個部分:數據(模型)、展現層(視圖)和用戶交互(控制器)。換句話說,一個事件的發生是這樣的過程:
1. 用戶和應用產生交互。
2. 控制器的事件處理器被觸發。
3. 控制器從模型中請求數據,並將其交給視圖。
4. 視圖將數據呈現給用戶。
我們不用類庫或框架就可以實現這種MVC架構模式。關鍵是要將MVC的每部分按照職責進行划分,將代碼清晰地分割為若干部分,並保持良好的解耦。這樣可以對每個部分進行獨立開發、測試和維護。

模型用來存放應用的所有數據對象。比如,可能有一個User模型,用以存放用戶列表、他們的屬性及所有與模型有關的邏輯。
模型不必知道視圖和控制器的邏輯。任何事件處理代碼、視圖模板,以及那些和模型無關的邏輯都應當隔離在模型之外。
將模型的代碼和視圖的代碼混在一起,是違反MVC架構原則的。模型是最應該從你的應用中解耦出來的部分。
當控制器從服務器抓取數據或創建新的記錄時,它就將數據包裝成模型實例。也就是說,我們的數據是面向對象的,任何定義在這個數據模型上的函數或邏輯都可以直接被調用。
視圖層是呈現給用戶的,用戶與之產生交互。在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變量中,這是一個命名空間,可以防止沖突並保持代碼清晰、可擴展。
控制器是模型和視圖之間的紐帶。控制器從視圖獲取事件和輸入,對它們(很可能包含模型)進行處理,並相應地更新視圖。當頁面加載時,控制器會給視圖添加事件監聽,比如監聽表單提交或按鈕點擊。然后,當用戶和你的應用產生交互時,控制器中的事件觸發器就開始工作了。
我們用簡單的jQuery代碼來實現控制器——var Controller = {}; (Controller.users = function($) { var nameClick = function() { /* ... */ }; // 在頁面加載時綁定事件監聽 $(function() { $('#view .name').click(nameClick); }); })(jQuery);






<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>
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(); });
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>
會發現,模板代碼添加了一些晦澀的屬性標簽。對於Ember.js的使用,我們需要創建一個Ember應用程序實例(app.js文件中)——
window.Todos = Ember.Application.create();

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); } });
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'), /* ... */ });
會發現上面的這個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(); } } });
對這些方法的調用,看一看對應的模板文件就知道了——
<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(); })();
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 };
會發現,視圖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的設計模式具備一定的優勢。