MVVM(Model View ViewModel)是一種基于MVC和MVP的架構(gòu)模式,它試圖將用戶界面(UI)從業(yè)務(wù)邏輯和行為中更加清晰地分離出來。為了這個目的,很多例子使用聲明變量綁定來把View層的工作從其他層分離出來。
這促進(jìn)了UI和開發(fā)工作在同一代碼庫中的同步進(jìn)行。UI開發(fā)者用他們的文檔標(biāo)記(HTML)綁定到ViewModel,在這個地方Model和ViewModel由負(fù)責(zé)邏輯的開發(fā)人員維護(hù)。
MVVM(如其大名)最初是由微軟定義,用于Windows Presentation Foundation(WPF)和Silverlight,在John Grossman2005年的一篇關(guān)于Avalon(WPF的代號)的博文中被官方推出。它也作為方便使用MVC的一種可選方案,為Adobe Flex社區(qū)積累了一些用戶量。
先于微軟采用的MVVM名稱,在社區(qū)中已經(jīng)有了一場由MVC像MVPM遷移的運(yùn)動:模型-視圖-展現(xiàn)模型。Marton Fowler在2004年為那些對此感興趣的人寫了一篇關(guān)于展現(xiàn)模型的文章。展現(xiàn)模型的理念的內(nèi)容要遠(yuǎn)遠(yuǎn)長于這篇文章,然而這篇文章被認(rèn)為是這一理念的重大突破,并且極大的捧紅了它。
在微軟推出作為MVPM的可選方案的MVVM后,就出現(xiàn)了許多沸沸揚(yáng)揚(yáng)的“alt.net”圈子。其中許多聲稱這個公司在GUI世界的霸主地位給與了它們將社區(qū)統(tǒng)一為整體的機(jī)會,出于市場營銷的目的,按照它們所高興的方式對已有的概念重新命名。一個進(jìn)步的群體也承認(rèn)MVVM和MVPM其實(shí)實(shí)在是同樣的概念,只是展現(xiàn)出來的是不同的包而已。
在近幾年,MVVM已經(jīng)在Javascript中得到了實(shí)現(xiàn),其構(gòu)造框架的形式諸如KnockoutJS,Kendo MVVM和Knockback.js,獲得了整個社區(qū)的積極響應(yīng)。
現(xiàn)在就讓我們來看看組成了MVVM的這三個組件。
和其它MV*家族成員一樣,MVVM中的模型代表我們的應(yīng)用用到的領(lǐng)域相關(guān)的數(shù)據(jù)或者信息。一個領(lǐng)域相關(guān)的數(shù)據(jù)的典型例子是用戶賬號(例如名字,頭像,電子郵件)或者音樂唱片(例如唱片名,年代,專輯)。
模型持有信息,但是通常沒有操作行為。它們不會格式化信息,也不會影響數(shù)據(jù)在瀏覽器中的表現(xiàn),因?yàn)檫@些不是模型的責(zé)任。相反,數(shù)據(jù)格式化是由視圖層處理的,盡管這種行為被認(rèn)為是業(yè)務(wù)邏輯,這個邏輯應(yīng)該被另外一個層封裝,這個層和模型交互,這個曾就是視圖模型。
這個規(guī)則唯一的例外是驗(yàn)證,由模型進(jìn)行數(shù)據(jù)驗(yàn)證是被認(rèn)為可以接受的,這些數(shù)據(jù)用于定義或者更新現(xiàn)存的模型(例如輸入的電子郵件地址是否滿足特定的正則表達(dá)式要求?)。
在KnockoutJS中,模型遵從上面的定義,但是通常對服務(wù)端服務(wù)的Ajax調(diào)用被做成即可以讀取也可以寫入模型數(shù)據(jù)。
如果我們正在構(gòu)建一個簡單的Todo應(yīng)用,使用KnockoutJS模型來表示一個Todo條目,看起來像下面這個樣子:
var Todo = function ( content, done ) {
this.content = ko.observable(content);
this.done = ko.observable(done);
this.editing = ko.observable(false);
};
注意:在上面小段代碼里面,你可能發(fā)現(xiàn)了,我們在KnockoutJS的名字空間里面調(diào)用observable()方法。在KnockoutJS中,觀察者是一類特殊的JavaScript對象,可以將變化通知給訂閱者,并且自動檢測依賴關(guān)系。這個特性使我們在模型值修改之后,可以同步模型和視圖模型。
使用MVC,視圖是應(yīng)用程序中用戶真正與之打交道的唯一一個部分.它們是展現(xiàn)一個視圖模型狀態(tài)的一個可交互UI.此種意義而言,視圖是主動的而不是被動的,而這也是真正的MVC和MVP的觀點(diǎn).在MVC,MVP和MVVM中視圖也可以是被動的,而這又是什么意思呢?
被動視圖僅僅只輸出要展示的東西,而不去接受任何用戶的輸入。
這樣一個視圖在我們的應(yīng)用程序中可能也沒有真正的模型的概念,而可以被一個代理控制.MVVM的主動視圖包含數(shù)據(jù)綁定,事件和需要能夠理解視圖模型的行為.盡管這些行為能夠被映射到屬性,視圖仍然處理這來自視圖模型的事件。
記住視圖在這里并不負(fù)責(zé)處理狀態(tài)時很重要的——它使得其與視圖模型得以同步。
KnockoutJS視圖是簡單的一個帶有聲明鏈接到視圖模型的HTML文檔。KnockoutJS視圖展示來自視圖模型的信息,并且傳遞命令給他(比如,用戶在一個元素上面點(diǎn)擊),并且針對視圖模型的變化更新狀態(tài)。而使用來自視圖模型的數(shù)據(jù)來生成標(biāo)記的模板也能夠被用在這個目的上。
未來給出一個簡單的初始示例,我們可以看看Javascritpt的MVVM框架KnockoutJS,看它如何允許一個視圖模型的定義,還有它在標(biāo)記中的相關(guān)綁定。
視圖模型:
var aViewModel = {
contactName: ko.observable("John")
};
ko.applyBindings(aViewModel);
視圖:
<p><input id="source" data-bind="value: contactName, valueUpdate: 'keyup'" /></p>
<div data-bind="visible: contactName().length > 10">
You have a really long name!
</div>
<p>Contact name: <strong data-bind="text: contactName"></strong></p>
我們的text-box輸入(源)從contactName獲取它的初始值,無論何時contactName發(fā)生了改變都會自動更新這個值.由于數(shù)據(jù)綁定是雙向的,像text-box中輸入也將據(jù)此更新contactName,以此保持值總是同步的。
盡管這個實(shí)現(xiàn)特定于KnockoutJS,但是包含著"You have a really long name!"文本的
標(biāo)簽包含有簡單的驗(yàn)證(同樣是以數(shù)據(jù)綁定的形式呈現(xiàn))。如果輸入超過10個字符,這個標(biāo)簽就會顯示,否則保持隱藏。
讓我們看看一個更高級的例子,我們可以看看我們的Todo應(yīng)用。一個用于這個應(yīng)用的裁剪后的KnockoutJS的視圖,包含有所有必要的數(shù)據(jù)綁定,這個視圖看起來是下面這個樣子。
<div id="todoapp">
<header>
<h1>Todos</h1>
<input id="new-todo" type="text" data-bind="value: current, valueUpdate: 'afterkeydown', enterKey: add"
placeholder="What needs to be done?"/>
</header>
<section id="main" data-bind="block: todos().length">
<input id="toggle-all" type="checkbox" data-bind="checked: allCompleted">
<label for="toggle-all">Mark all as complete</label>
<ul id="todo-list" data-bind="foreach: todos">
<!-- item -->
<li data-bind="css: { done: done, editing: editing }">
<div class="view" data-bind="event: { dblclick: $root.editItem }">
<input class="toggle" type="checkbox" data-bind="checked: done">
<label data-bind="text: content"></label>
<a class="destroy" href="#" data-bind="click: $root.remove"></a>
</div>
<input class="edit' type="text"
data-bind="value: content, valueUpdate: 'afterkeydown', enterKey: $root.stopEditing, selectAndFocus: editing, event: { blur: $root.stopEditing }"/>
</li>
</ul>
</section>
</div>
請注意,這個標(biāo)記的基本布局是相對直觀的,包含有一個輸入文本框(新的todo)用于增加新條目,用于標(biāo)記條目完成的開關(guān),以及一個擁有模板的列表(todo列表),這個模板以anli的形式展現(xiàn)Todo條目。
上面標(biāo)記中綁定的數(shù)據(jù)可以分成下面幾塊:
視圖模型被認(rèn)為是一個專門進(jìn)行數(shù)據(jù)轉(zhuǎn)換的控制器。它可以把對象信息轉(zhuǎn)換到視圖信息,將命令從視圖攜帶到對象。
例如,我們想象我們有一個對象的日期屬性是unix格式的(e.g 1333832407),而不是用戶視圖的所需要的日期格式(e.g 04/07/2012 @ 5:00pm),這時就有必要把unix的日期格式轉(zhuǎn)換為視圖需要的格式。我們的對象只簡單保存原始的unix數(shù)據(jù)格式日期,視圖模型作為一個中間人角色會格式化原始的unix數(shù)據(jù)格式轉(zhuǎn)換為視圖需要的日期格式。
在這個場景下,視圖模型可以被看做一個對象,它處理很多視圖顯示邏輯。視圖模型也對外提供更新視圖狀態(tài)的方法,并通過視圖方法和觸發(fā)事件更新對象。
簡單來說,視圖模型位于我們UI層后面層。它通過視圖發(fā)布對象的公共數(shù)據(jù),同時它作為視圖源提供數(shù)據(jù)和方法。
KnockoutJS描述視圖模型作為數(shù)據(jù)的表現(xiàn)和操作可以在UI上訪問和執(zhí)行。視圖模型并不是一個UI對象,也不是數(shù)據(jù)持久化對象,而是一個能夠?yàn)橛脩籼峁﹥Υ鏍顟B(tài)及操作的層次對象。Knockout的視圖模型實(shí)現(xiàn)了JavaScript對象與HTML語言無關(guān)性。通過這個實(shí)現(xiàn)使開發(fā)保持了簡單,意味著我們可以在視圖層更加簡單的管理更多的組合方法。
對于我們的ToDo應(yīng)用程序的一部分KnockoutJS視圖模型可以是像下面這樣:
// our main ViewModel
var ViewModel = function ( todos ) {
var self = this;
// map array of passed in todos to an observableArray of Todo objects
self.todos = ko.observableArray(
ko.utils.arrayMap( todos, function ( todo ) {
return new Todo( todo.content, todo.done );
}));
// store the new todo value being entered
self.current = ko.observable();
// add a new todo, when enter key is pressed
self.add = function ( data, event ) {
var newTodo, current = self.current().trim();
if ( current ) {
newTodo = new Todo( current );
self.todos.push( newTodo );
self.current("");
}
};
// remove a single todo
self.remove = function ( todo ) {
self.todos.remove( todo );
};
// remove all completed todos
self.removeCompleted = function () {
self.todos.remove(function (todo) {
return todo.done();
});
};
// writeable computed observable to handle marking all complete/incomplete
self.allCompleted = ko.computed({
// always return true/false based on the done flag of all todos
read:function () {
return !self.remainingCount();
},
// set all todos to the written value (true/false)
write:function ( newValue ) {
ko.utils.arrayForEach( self.todos(), function ( todo ) {
//set even if value is the same, as subscribers are not notified in that case
todo.done( newValue );
});
}
});
// edit an item
self.editItem = function( item ) {
item.editing( true );
};
..
上面我們基本上提供了必需的加入、編輯或者移除記錄的方法,還有標(biāo)記所有現(xiàn)存的記錄已經(jīng)被完成的邏輯。注意:唯一真正需要關(guān)注的同前面我們的視圖模型的示例的不同之處就是觀察數(shù)組.在KnockoutJS中,如果我們希望監(jiān)測到并且去回應(yīng)一個單獨(dú)的對象發(fā)生的改變,我們可以使用觀察.然而如果我們希望檢測并且去回應(yīng)一個集合的事物所發(fā)生的改變,我們可以換用一個觀察數(shù)組.如何使用觀察數(shù)組的一個簡單示例就像下面這樣:
// Define an initially an empty array
var myObservableArray = ko.observableArray();
// Add a value to the array and notify our observers
myObservableArray.push( 'A new todo item' );
注意:感興趣的話,我們在前面所提到的完整的KnockoutJS Todo應(yīng)用程序可以從 TodoMVC 獲取到。
視圖和視圖模型使用數(shù)據(jù)綁定和事件進(jìn)行通信。正如我們之前的視圖模型例子所見,視圖模型不僅僅發(fā)布對象屬性,它還提供其他的方法和特性,諸如驗(yàn)證。
我們的視圖處理自己的用戶接口事件,并會把相關(guān)事件映射到視圖模型。對象和它屬性與視圖模型是同步的,且通過雙向數(shù)據(jù)綁定進(jìn)行更新。
觸發(fā)器(數(shù)據(jù)觸發(fā)器)允許我們進(jìn)一步在視圖狀態(tài)變化后改變我們的對象屬性。
雖然可能會出現(xiàn)在MVVM中視圖模型完全對模型負(fù)責(zé)的情況,這些關(guān)系確實(shí)有一些值得關(guān)注的微妙之處.處于數(shù)據(jù)綁定的目的,視圖模型可以暴露出來一個模型或者模型屬性,而且也能夠包含獲取和操作視圖中暴露出來的屬性。
現(xiàn)在,我們完全對MVVM是什么,以及它是如何工作的,有了一個更好的了解.現(xiàn)在就讓我們來看看使用這種模式的優(yōu)點(diǎn)和缺點(diǎn)吧:
優(yōu)點(diǎn):
缺點(diǎn):
常見到有著MVC或者M(jìn)VP開發(fā)經(jīng)驗(yàn)的JavaScript程序員評論MVVM的時候在抱怨它會分散他們的關(guān)注點(diǎn)。也就是說,他們習(xí)慣在一個視圖中有相當(dāng)數(shù)量的數(shù)據(jù)被耦合在了HTML標(biāo)簽中。
我必須承認(rèn)當(dāng)我第一次體驗(yàn)實(shí)現(xiàn)了MVVM的JavaScript框架后(例如 KnockoutJS, Knockback),我很驚訝很多程序員都想要回到一個難以維護(hù)的混淆了邏輯(JavaScript代碼)和HTML標(biāo)簽做法的過去。然而現(xiàn)實(shí)是使用MVVM會有很多好處(我們之前說過),包括設(shè)計(jì)師能更容易的通過他們的標(biāo)記去綁定相關(guān)邏輯。
在我們中間的傳統(tǒng)程序員,你會很開心知道現(xiàn)在我們能夠通過數(shù)據(jù)綁定這個特性大量減少程序代碼的耦合程度,且KnockoutJS從1.3這個版本就開始提供自定義綁定功能。
KnockoutJS 默認(rèn)有一個數(shù)據(jù)綁定提供者,這個提供者搜索所有的附屬有數(shù)據(jù)綁定屬性的元素,如下面的例子:
<input id="new-todo" type="text" data-bind="value: current, valueUpdate: 'afterkeydown', enterKey: add" placeholder="What needs to be done?"/>
當(dāng)這個提供者定位一個到包含有該屬性的元素時,這個工具將會分析該元素,使用當(dāng)前的數(shù)據(jù)上下文來將其轉(zhuǎn)化成一個綁定對象。這種方式是 KnockoutJS百分百可以工作的方式,通過這種方式,我們可以采用聲明式的方法對元素增加綁定,KnockoutJS之后會在該層上將數(shù)據(jù)綁定到元素上。
當(dāng)我們開始構(gòu)建復(fù)雜的視圖的時候,我們最終就可能得到大量的元素和屬性在標(biāo)記中綁定數(shù)據(jù),這種方式將會變得很難管理。通過自定義的綁定提供者,這就不算個問題。
一個綁定提供者主要關(guān)心兩件事:
綁定提供者實(shí)現(xiàn)了兩個功能:
一個框架綁定提供者看起來如下:
var ourBindingProvider = {
nodeHasBindings: function( node ) {
// returns true/false
},
getBindings: function( node, bindingContext ) {
// returns a binding object
}
};
在我們充實(shí)這個提供者之前,讓我們先簡要的討論一下數(shù)據(jù)綁定屬性中的邏輯。
當(dāng)使用Knockout的MVVM,我們會對將應(yīng)用邏輯過度綁定到視圖上的這種方法不滿。我們可以實(shí)現(xiàn)像CSS類一樣的東西,將綁定根據(jù)名字賦值給元素。Ryan Niemeyer(knockmeout.net上的)之前提出使用數(shù)據(jù)類用于這個目的,來避免將展示類和數(shù)據(jù)類混淆,讓我們改造我們的nodeHasBindings 函數(shù),來支持這個概念:
// does an element have any bindings?
function nodeHasBindings( node ) {
return node.getAttribute ? node.getAttribute("data-class") : false;
};
接下來,我們需要一個敏感的getBindings()函數(shù)。既然我們堅(jiān)持使用CSS類的概念,為什么不考慮一下支持空格分割類呢,這樣可以使我們在不同元素之間共享綁定標(biāo)準(zhǔn)。
讓我們首先看一下我們的綁定長什么樣子。我們建立一個對象用于持有它們,在這些綁定處,我們的屬性名需要和我們數(shù)據(jù)類中使用的關(guān)鍵字相匹配。
注意:對于將使用傳統(tǒng)數(shù)據(jù)綁定方式的KnockoutJS應(yīng)用轉(zhuǎn)化成一個使用自定義綁定提供者的不引人矚目的綁定方式。我們簡單的拉取我們所有的數(shù)據(jù)綁定屬性,使用數(shù)據(jù)類屬性來替換它們,并且像之前做的一樣,將我們的綁定放到綁定對象中去。
var viewModel = new ViewModel( todos || [] ),
bindings = {
newTodo: {
value: viewModel.current,
valueUpdate: "afterkeydown",
enterKey: viewModel.add
},
taskTooltip : {
visible: viewModel.showTooltip
},
checkAllContainer : {
visible: viewModel.todos().length
},
checkAll: {
checked: viewModel.allCompleted
},
todos: {
foreach: viewModel.todos
},
todoListItem: function() {
return {
css: {
editing: this.editing
}
};
},
todoListItemWrapper: function() {
return {
css: {
done: this.done
}
};
},
todoCheckBox: function() {
return {
checked: this.done
};
},
todoContent: function() {
return {
text: this.content,
event: {
dblclick: this.edit
}
};
},
todoDestroy: function() {
return {
click: viewModel.remove
};
},
todoEdit: function() {
return {
value: this.content,
valueUpdate: "afterkeydown",
enterKey: this.stopEditing,
event: {
blur: this.stopEditing
}
};
},
todoCount: {
visible: viewModel.remainingCount
},
remainingCount: {
text: viewModel.remainingCount
},
remainingCountWord: function() {
return {
text: viewModel.getLabel(viewModel.remainingCount)
};
},
todoClear: {
visible: viewModel.completedCount
},
todoClearAll: {
click: viewModel.removeCompleted
},
completedCount: {
text: viewModel.completedCount
},
completedCountWord: function() {
return {
text: viewModel.getLabel(viewModel.completedCount)
};
},
todoInstructions: {
visible: viewModel.todos().length
}
};
....
上面代碼中,我們丟掉了兩行,我們?nèi)匀恍枰猤etBindings函數(shù),這個函數(shù)遍歷數(shù)據(jù)類屬性中每一個關(guān)鍵字,并從中構(gòu)建最終對象。如果我們檢測到綁定對象是個函數(shù),我們使用當(dāng)前的數(shù)據(jù)調(diào)用它。我們的完成版自定義綁定提供中,如下:
// We can now create a bindingProvider that uses
// something different than data-bind attributes
ko.customBindingProvider = function( bindingObject ) {
this.bindingObject = bindingObject;
// determine if an element has any bindings
this.nodeHasBindings = function( node ) {
return node.getAttribute ? node.getAttribute( "data-class" ) : false;
};
};
// return the bindings given a node and the bindingContext
this.getBindings = function( node, bindingContext ) {
var result = {},
classes = node.getAttribute( "data-class" );
if ( classes ) {
classes = classes.split( "" );
//evaluate each class, build a single object to return
for ( var i = 0, j = classes.length; i < j; i++ ) {
var bindingAccessor = this.bindingObject[classes[i]];
if ( bindingAccessor ) {
var binding = typeof bindingAccessor === "function" ? bindingAccessor.call(bindingContext.$data) : bindingAccessor;
ko.utils.extend(result, binding);
}
}
}
return result;
};
};
我們綁定對象最后的幾行,定義如下:
// set ko's current bindingProvider equal to our new binding provider
ko.bindingProvider.instance = new ko.customBindingProvider( bindings );
// bind a new instance of our ViewModel to the page
ko.applyBindings( viewModel );
})();
我們在這里所做的是為我們的綁定處理器有效的定義構(gòu)造器,綁定處理器接受一個我們用來查找綁定的對象(綁定)。然后我們可以使用數(shù)據(jù)類為我們應(yīng)用程序視圖的重寫標(biāo)記,像下面這樣做:
<div id="create-todo">
<input id="new-todo" data-class="newTodo" placeholder="What needs to be done?" />
<span class="ui-tooltip-top" data-class="taskTooltip" style="display: none;">Press Enter to save this task</span>
</div>
<div id="todos">
<div data-class="checkAllContainer" >
<input id="check-all" class="check" type="checkbox" data-class="checkAll" />
<label for="check-all">Mark all as complete</label>
</div>
<ul id="todo-list" data-class="todos" >
<li data-class="todoListItem" >
<div class="todo" data-class="todoListItemWrapper" >
<div class="display">
<input class="check" type="checkbox" data-class="todoCheckBox" />
<div class="todo-content" data-class="todoContent" style="cursor: pointer;"></div>
<span class="todo-destroy" data-class="todoDestroy"></span>
</div>
<div class="edit'>
<input class="todo-input" data-class="todoEdit'/>
</div>
</div>
</li>
</ul>
</div>
Nei Kerkin 已經(jīng)使用上面的方式組合成了一個完整的TodoMVC示例,它可以 從 這里獲取到。 雖然上面的解釋看起來像是有許多的工作要做,現(xiàn)在我們就有一個一般的getBindingmethod方法要寫。比起為了編寫我們的KnockoutJS應(yīng)用程序而嚴(yán)格使用數(shù)據(jù)綁定,簡單的重用和使用數(shù)據(jù)類更加的瑣碎。最終的結(jié)果是希望得到一個干凈的標(biāo)記,其中我們的數(shù)據(jù)綁定會從視圖切換到一個綁定對象。
MVP和MVVM都是MVC的衍生物。它和它的衍生物之間關(guān)鍵的不同之處在于每一層對于其它層的依賴,以及它們相互之間是如何緊密結(jié)合在一起的。
在MVC中,視圖位于我們架構(gòu)的頂部,其背后是控制器。模型在控制器后面,而因此我們的視圖了解得到我們的控制器,而控制器了解得到模型。這里,我們的視圖有對模型的直接訪問。然而將整個模型完全暴露給視圖可能會有安全和性能損失,這取決于我們應(yīng)用程序的復(fù)雜性。MVVM則嘗試去避免這些問題。
在MVP中,控制器的角色被代理器所取代,代理器和視圖處于同樣的地位,視圖和模型的事件都被它偵聽著并且接受它的調(diào)解。不同于MVVM,沒有一個將視圖綁定到視圖模型的機(jī)制,因此我們轉(zhuǎn)而依賴于每一個視圖都實(shí)現(xiàn)一個允許代理器同視圖去交互的接口。
MVVM進(jìn)一步允許我們創(chuàng)建一個模型的特定視圖子集,包含了狀態(tài)和邏輯信息,避免了將模型完全暴露給視圖的必要。不同于MVP的代理器,視圖模型并不需要去引用一個視圖。視圖可以綁定到視圖模型的屬性上面,視圖模型則去將包含在模型中的數(shù)據(jù)暴露給視圖。像我們所提到過的,對視圖的抽象意味著其背后的代碼需要較少的邏輯。
對此的副作用之一就是視圖模型和視圖層之間新增的的用于翻譯解釋的一層會有性能損失。這種解釋層的復(fù)雜度根據(jù)情況也會有所差異——它可能像復(fù)制數(shù)據(jù)一樣簡單,也可能會像我們希望用視圖理解的一種形式去操作它們,那樣復(fù)雜。由于整個模型是現(xiàn)成可用的,從而這種操作可以被避免掉,所以MVC沒有這種問題。
了解MVC,MVP和MVVM之間的細(xì)微差別是很重要的,然而基于我們已經(jīng)了解到的東西,開發(fā)者最終會問到是否它們應(yīng)該考慮使用KnockoutJS而不是Backbone這個問題。下面的一些相關(guān)事項(xiàng)對此可能有些幫助:
總結(jié)下來,我個人發(fā)覺KnockoutJS更適合于小型的應(yīng)用,而Backbone的特性在任何東西都是無序的場景下面才會是亮點(diǎn)。那就是說,許多開發(fā)者兩個框架都已經(jīng)使用過來編寫不同復(fù)雜度的應(yīng)用程序,而我建議在一個小范圍內(nèi)兩種都嘗試一下,在你決定哪一種能更好的為你工作之前。
更多建議: