MVC是一個(gè)架構(gòu)設(shè)計(jì)模式,它通過分離關(guān)注點(diǎn)的方式來支持改進(jìn)應(yīng)用組織方式。它促成了業(yè)務(wù)數(shù)據(jù)(Models)從用戶界面(Views)中分離出來,還有第三個(gè)組成部分(Controllers)負(fù)責(zé)管理傳統(tǒng)意義上的業(yè)務(wù)邏輯和用戶輸入。該模式最初是由Trygve Reenskaug在研發(fā)Smalltalk-80 (1979)期間設(shè)計(jì)的,當(dāng)時(shí)它起初被稱作Model-View-Controller-Editor。在1995年的“設(shè)計(jì)模式: 面向?qū)ο筌浖械目蓮?fù)用元素” (著名的"GoF"的書)中,MVC被進(jìn)一步深入的描述,該書對MVC的流行使用起到了關(guān)鍵作用。
了解一下最初的MVC模式打算解決什么問題是很重要的,因?yàn)樽詮恼Q生之日起它已經(jīng)發(fā)生了很大的改變?;氐?0年代,圖形用戶界面還很稀少,一個(gè)被稱為分離展示的概念開始被用來清晰的劃分下面兩種對象:領(lǐng)域?qū)ο?,它對現(xiàn)實(shí)世界里的概念進(jìn)行建模(比如一張照片,一個(gè)人), 還有展示對象,它被渲染到用戶屏幕上進(jìn)行展示。
Smalltalk-80作為MVC的實(shí)現(xiàn),把這一概念進(jìn)一步發(fā)展,產(chǎn)生這樣一個(gè)觀點(diǎn),即把應(yīng)用邏輯從用戶界面中分離開來。這種想法使得應(yīng)用的各個(gè)部分之間得以解耦,也允許該應(yīng)用中的其它界面對模型進(jìn)行復(fù)用。關(guān)于Smalltalk-80的MVC架構(gòu),有幾點(diǎn)很有趣,值得注意一下:
開發(fā)者有時(shí)候會驚奇于他們了解到的觀察者模式(如今已經(jīng)被普遍的作為發(fā)布/訂閱的變異實(shí)現(xiàn)了)已經(jīng)在幾十年以前被作為MVC架構(gòu)的一部分包含進(jìn)去了.在Smalltalk-80的 MVC中,視圖觀察著模型.如上面要點(diǎn)中所提到的,模型在任何時(shí)候發(fā)生了改變,視圖就會做出響應(yīng).一個(gè)簡單的示例就是一個(gè)由股票市場數(shù)據(jù)支撐的應(yīng)用程序——為了應(yīng)用程序的實(shí)用性,任何對于我們模型中數(shù)據(jù)的改變都應(yīng)該導(dǎo)致視圖中的結(jié)果實(shí)時(shí)的刷新。
Martin Fowler在過去數(shù)年完成了對原生MVC有關(guān)問題進(jìn)行寫作的優(yōu)秀工作,如果對關(guān)于Smalltalk-80的MVC的更深入的歷史信息感興趣的話,我建議您讀一讀他的作品。
我們已經(jīng)回顧了70年代,讓我們回到當(dāng)下回到眼前?,F(xiàn)在,MVC模式已經(jīng)被應(yīng)用到大范圍的編程語言當(dāng)中,包括與我們關(guān)系最近的JavaScript。JavaScript領(lǐng)域現(xiàn)在有一些鼓勵(lì)支持MVC (或者是它的變種,我們稱之為MV* 家族)的框架,允許開發(fā)者不用付出太多的努力就可以往他們的應(yīng)用中添加新的結(jié)構(gòu)。
這些框架包括諸如Backbone, Ember.js和AngularJS??紤]到避免出現(xiàn)“意大利面條”式的代碼的重要性,該詞是指那些由于缺乏結(jié)構(gòu)設(shè)計(jì)而導(dǎo)致難于閱讀和維護(hù)的代碼,對現(xiàn)代JavaScript開發(fā)者來說,了解該模式能夠提供什么已經(jīng)是勢在必行。這使得我們可以有效的領(lǐng)會到,這些框架能讓我們以不同的方式做哪些事情。
我們知道MVC由三個(gè)核心部分組成:
Models管理一個(gè)業(yè)務(wù)應(yīng)用的數(shù)據(jù)。它們既與用戶界面無關(guān)也與表現(xiàn)層無關(guān),相反的它們代表了一個(gè)業(yè)務(wù)應(yīng)用所需要的形式唯一的數(shù)據(jù)。當(dāng)一個(gè)model改變時(shí)(比如當(dāng)它被更新時(shí)),它通常會通知它的觀察者(比如我們很快會介紹的views)一個(gè)改變已經(jīng)發(fā)生了,以便觀察者采取相應(yīng)的反應(yīng)。
為了更深的理解models,讓我們假設(shè)我們有一個(gè)JavaScript的相冊應(yīng)用。在一個(gè)相冊中,照片這個(gè)概念配得上擁有一個(gè)自己的model, 因?yàn)樗砹颂囟I(lǐng)域數(shù)據(jù)的一個(gè)獨(dú)特類型。這樣一個(gè)model可以包含一些相關(guān)的屬性,比如標(biāo)題,圖片來源和額外的元數(shù)據(jù)。一張?zhí)囟ǖ恼掌梢源鎯Φ絤odel的一個(gè)實(shí)例中,而且一個(gè)model也可以被復(fù)用。下面我們可以看到一個(gè)用Backbone實(shí)現(xiàn)的被簡化的model例子。
var Photo = Backbone.Model.extend({
// 照片的默認(rèn)屬性
defaults: {
src: "placeholder.jpg",
caption: "A default image",
viewed: false
},
// 確保每一個(gè)被創(chuàng)建的照片都有一個(gè)`src`.
initialize: function() {
this.set( { "src": this.defaults.src} );
}
});
不同的框架其內(nèi)置的模型的能力有所不同,然而他們對于屬性驗(yàn)證的支持還是相當(dāng)普遍的,屬性展現(xiàn)了模型的特征,比如一個(gè)模型標(biāo)識符.當(dāng)在一個(gè)真實(shí)的世界使用模型的時(shí)候,我們一般也希望模型能夠持久.持久化允許我們用最近的狀態(tài)對模型進(jìn)行編輯和更新,這一狀態(tài)會存儲在內(nèi)存、用戶的本地?cái)?shù)據(jù)存儲區(qū)或者一個(gè)同步的數(shù)據(jù)庫中。
另外,模型可能也會被多個(gè)視圖觀察著。如果說,我們的照片模型包含了一些元數(shù)據(jù),比如它的位置(經(jīng)緯度),照片中所展現(xiàn)的好友(一個(gè)標(biāo)識符的列表)和一個(gè)標(biāo)簽的列表,開發(fā)者也許會選擇為這三個(gè)方面的每一個(gè)提供一個(gè)單獨(dú)的視圖。
為現(xiàn)代MVC/MV*框架提供一種將模型組合到一起的方法(例如,在Backbone中,這些分組作為“集合”被引用)并不常見。管理分組中的模型允許我們基于來自分組中所包含的模型發(fā)生改變的通知,來編寫應(yīng)用程序邏輯.這避免了手動設(shè)置去觀察每一個(gè)單獨(dú)的模型實(shí)體的必要。
如下是一個(gè)將模型分組成一個(gè)簡化的Backbone集合的示例:
var PhotoGallery = Backbone.Collection.extend({
// Reference to this collection's model.
model: Photo,
// Filter down the list of all photos
// that have been viewed
viewed: function() {
return this.filter(function( photo ){
return photo.get( "viewed" );
});
},
// Filter down the list to only photos that
// have not yet been viewed
unviewed: function() {
return this.without.apply( this, this.viewed() );
}
});
MVC上舊的文本可能也包含了模型管理著應(yīng)用程序狀態(tài)的一種概念的引述.Javascript中的應(yīng)用程序狀態(tài)有一種不同的意義,通常指的是當(dāng)前的"狀態(tài)",即在一個(gè)固定點(diǎn)上的用戶屏幕上的視圖或者子視圖(帶有特定的數(shù)據(jù)).狀態(tài)是一個(gè)經(jīng)常被談?wù)摰降脑掝},看一看單頁面應(yīng)用程序,其中的狀態(tài)的概念需要被模擬。
總而言之,模型主要關(guān)注的是業(yè)務(wù)數(shù)據(jù)。
視圖是模型的可視化表示,提供了一個(gè)當(dāng)前狀態(tài)的經(jīng)過過濾的視圖。Smaltalk的視圖是關(guān)于繪制和操作位圖的,而JavaScript的視圖是關(guān)于構(gòu)建和操作DOM元素的。
一個(gè)視圖通常是模型的觀察者,當(dāng)模型改變的時(shí)候,視圖得到通知,因此使得視圖可以更新自身。用設(shè)計(jì)模式的語言可以稱視圖為“啞巴”,因?yàn)樵趹?yīng)用程序中是它們關(guān)于模型和控制器的了解是受到限制的。
用戶可以和視圖進(jìn)行交互,包括讀和編輯模型的能力(例如,獲取或者設(shè)置模型的屬性值)。因?yàn)橐晥D是表示層,我們通常以用戶友好的方式提供編輯和更新的能力。例如,在之前我們討論的照片庫應(yīng)用中,模型編輯可以通過“編輯”視圖來進(jìn)行,這個(gè)視圖里面,用戶可以選擇一個(gè)特定的圖片,接著編輯它的元數(shù)據(jù)。
而實(shí)際更新模型的任務(wù)落到了控制器上面(我們很快就會講這個(gè)東西)。
讓我們使用vanilla JavaScript 實(shí)現(xiàn)的例子來更深入的探索一下視圖。下面我們可以看到一個(gè)函數(shù)創(chuàng)建了一個(gè)照片視圖,使用了模型實(shí)例和控制器實(shí)例。
我們在視圖里定義了一個(gè)render()工具,使用一個(gè)JavaScript模板引擎來用于渲染照片模型的內(nèi)容(Underscore的模板),并且更新了我們視圖的內(nèi)容,供照片EI來參考。
照片模型接著將我們的render()函數(shù)作為一個(gè)其一個(gè)訂閱者的回調(diào)函數(shù),這樣通過觀察者模式,當(dāng)模型發(fā)生改變的時(shí)候,我們就能觸發(fā)視圖的更新。
人們可能會問用戶交互如何在這里起作用的。當(dāng)用戶點(diǎn)擊視圖中的任何元素,不是由視圖決定接下來怎么做。而是由控制器為視圖做決定。在我們的例子中,通過為photoEI增加一個(gè)事件監(jiān)聽器,來達(dá)到這個(gè)目的,photoEI將會代理處理送往控制器的點(diǎn)擊行為,在需要的時(shí)候?qū)⒛P托畔⒑褪录徊鬟f。
這個(gè)架構(gòu)的好處是每個(gè)組件在應(yīng)用工作的時(shí)候都扮演著必要的獨(dú)立的角色。
var buildPhotoView = function ( photoModel, photoController ) {
var base = document.createElement( "div" ),
photoEl = document.createElement( "div" );
base.appendChild(photoEl);
var render = function () {
// We use a templating library such as Underscore
// templating which generates the HTML for our
// photo entry
photoEl.innerHTML = _.template( "#photoTemplate" , {
src: photoModel.getSrc()
});
};
photoModel.addSubscriber( render );
photoEl.addEventListener( "click", function () {
photoController.handleEvent( "click", photoModel );
});
var show = function () {
photoEl.style.display = "";
};
var hide = function () {
photoEl.style.display = "none";
};
return {
showView: show,
hideView: hide
};
};
在支持MVC/MV*的JavaScript框架的下,有必要簡略的討論一下JavaScript的模板以及它們與視圖之間的關(guān)系,在上一小節(jié),我們已經(jīng)接觸到這種關(guān)系了。
歷史已經(jīng)證明在內(nèi)存中通過字符串拼接來構(gòu)建大塊的HTML標(biāo)記是一種糟糕的性能實(shí)踐。開發(fā)者這樣做,就會深受其害。遍歷數(shù)據(jù),將其封裝成嵌套的div,使用例如document.writeto 這樣過時(shí)的技術(shù)將"模板"注入到DOM中。這樣通常意味著校本化的標(biāo)記將會嵌套在我們標(biāo)準(zhǔn)的標(biāo)記中,很快就變得很難閱讀了,更重要的是,維護(hù)這樣的代碼將是一場災(zāi)難,尤其是在構(gòu)建大型應(yīng)用的時(shí)候。
JavaScript 模板解決方案(例如Handlebars.js 和Mustache)通常用于為視圖定義模板作為標(biāo)記(要么存儲在外部,要么存儲在腳本標(biāo)簽里面,使用自定義的類型例如text/template),標(biāo)記中包含有模板變量。變量可以使用變化的語法來分割(例如{{name}}),框架通常也足夠只能接受JSON格式的數(shù)據(jù)(模型可以轉(zhuǎn)化成JSOn格式),這樣我們只需要關(guān)心如何維護(hù)干凈的模型和干凈的模板。人們遭遇的絕大多數(shù)的苦差事都被框架本身所處理了。這樣做有大量的好處,尤其選擇是將模板存儲在外部的時(shí)候,這樣在構(gòu)建大型引應(yīng)用的時(shí)候可以是模板按照需要?jiǎng)討B(tài)加載。
下面我們可以看到兩個(gè)HTMP模板的例子。一個(gè)使用流行的Handlebar.js框架實(shí)現(xiàn),一個(gè)使用Underscore模板實(shí)現(xiàn)。
<li class="photo">
<h2>{{caption}}</h2>
<img class="source" src="{{src}}"/>
<div class="meta-data">
{{metadata}}
</div>
</li>
<li class="photo">
<h2><%= caption %></h2>
<img class="source" src="<%= src %>"/>
<div class="meta-data">
<%= metadata %>
</div>
</li><span style="line-height:1.5;font-family:'sans serif', tahoma, verdana, helvetica;font-size:10pt;"></span>
請注意模板并不是它們自身的視圖,來自于Struts Model 2 架構(gòu)的開發(fā)者可能會感覺模板就是一個(gè)視圖,但并不是這樣的。視圖是一個(gè)觀察著模型的對象,并且讓可視的展現(xiàn)保持最新。模板也許是用一種聲明的方式指定部分甚至所有的視圖對象,因此它可能是從模板定制文檔生成的。
在經(jīng)典的web開發(fā)中,在單獨(dú)的視圖之間進(jìn)行導(dǎo)航需要利用到頁面刷新,然而也并不值得這樣做。而在單頁面Javascript應(yīng)用程序中,一旦數(shù)據(jù)通過ajax從服務(wù)器端獲取到了,并不需要任何這樣必要的刷新,就可以簡單的在同一個(gè)頁面渲染出一個(gè)新的視圖。
這里導(dǎo)航就降級為了“路由”的角色,用來輔助管理應(yīng)用程序狀態(tài)(例如,允許用戶用書簽標(biāo)記它們已經(jīng)瀏覽到的視圖)。然而,路由既不是MVC的一部分,也不在每一個(gè)類MVC框架中展現(xiàn)出來,在這一節(jié)中我將不深入詳細(xì)的討論它們。
總而言之,視圖是對我們的數(shù)據(jù)的一種可視化展現(xiàn)。
控制器是模型和視圖之間的中介,典型的職責(zé)是當(dāng)用戶操作視圖的時(shí)候同步更新模型。
在我們的照片廊應(yīng)用程序中,控制器會負(fù)責(zé)處理用戶通過對一個(gè)特定照片的視圖進(jìn)行編輯所造成改變,當(dāng)用戶完成編輯后,就更新一個(gè)特定的照片模型。
請記住滿足了MVC中的一種角色:針對視圖的策略模式的基礎(chǔ)設(shè)施。在策略模式方面,視圖在視圖的自由載量權(quán)方面代表了控制器。因此,那就是測試模式是如何工作的,視圖可以代表針對控制器的用戶事件,當(dāng)視圖看起來合適的時(shí)候。視圖也可以代表針對控制器的模型變更事件處理,當(dāng)視圖看起來合適的時(shí)候,但這并不是控制器的傳統(tǒng)角色。
大多數(shù)的Javascript MVC框架都受到了對"MVC"通常認(rèn)知的影響,而這種認(rèn)知是和控制器綁定在一起的.出現(xiàn)這種情況的原因各異,但在我的真實(shí)想法中,那是由于框架的作者一開始就將從服務(wù)器端的角度看待MVC,意識到它并不在客戶端進(jìn)行1:1的翻譯,而對MVC中的C進(jìn)行重新詮釋意在他們感覺更加有意義的事情.與此同在的問題在于它是主觀的,增加了理解經(jīng)典MVC模式的復(fù)雜度,當(dāng)然還有控制器在現(xiàn)代框架中的角色。
作為示例,讓我們來簡要回顧一下當(dāng)前流行的一種構(gòu)造框架Backbone.js其架構(gòu).Backbone包含了模型和視圖(某些東西同我們前面看到的類似),然而它實(shí)際上并沒有真正的控制器.它的視圖和路由行為同控制器有一點(diǎn)點(diǎn)類似,但它們自身實(shí)際上都不是控制器。
在這一方面,同官方文檔或者博客文章中可能提到的相左,Backbone既不是一個(gè)真正的MVC/MVP框架,也不是一個(gè)MVVM框架.事實(shí)上把它看做是用它自身的方式架構(gòu)方法的MV*家族中的一員,更加合適.當(dāng)然這沒有任何錯(cuò)誤的地方,但區(qū)分經(jīng)典MVC和MV*是重要的,我們應(yīng)該依靠前者的經(jīng)典語法來幫助理解后者。
我們現(xiàn)在知道傳統(tǒng)的控制器負(fù)責(zé)當(dāng)用戶更新視圖是同步更新模型.值得注意的一個(gè)有趣的地方是大多數(shù)時(shí)下流行的Javascript MVC/MV*框架在編寫的時(shí)候(Backbone)都沒有屬于它們自己的明確的控制器的概念。
因此,這對于我們從另一個(gè)MVC框架中體會到控制器實(shí)現(xiàn)的差異,并更進(jìn)一步的展現(xiàn)出控制如何扮演著非傳統(tǒng)的角色是很有用處的.對于這一點(diǎn),讓我們來看看來自于Spine.js的示例控制器。
在這個(gè)示例中,我們會有一個(gè)叫做PhotosController的控制器,用來管理應(yīng)用程序中的個(gè)人照片.它將確保當(dāng)視圖更新(例如,一個(gè)用戶編輯了照片的元數(shù)據(jù))時(shí),對應(yīng)的模型也會更新。
注意:我們并不會花大力氣研究Spine.js,而只是對它的控制器能做什么進(jìn)行一定程度的了解:
// Controllers in Spine are created by inheriting from Spine.Controller
var PhotosController = Spine.Controller.sub({
init: function () {
this.item.bind( "update" , this.proxy( this.render ));
this.item.bind( "destroy", this.proxy( this.remove ));
},
render: function () {
// Handle templating
this.replace( $( "#photoTemplate" ).tmpl( this.item ) );
return this;
},
remove: function () {
this.el.remove();
this.release();
}
});
在Spine中,控制器被認(rèn)為是一個(gè)應(yīng)用程序的粘合劑,對DOM事件進(jìn)行添加和響應(yīng),渲染模板,還有確保視圖和模型保持同步(這在我們所知的控制器的上下文中起作用)。
我們在上面的example.js示例中所做的,是使用render()和remove()方法在更新和銷毀事件中設(shè)置偵聽器。當(dāng)一個(gè)照片條目獲得更新的時(shí)候,我們對視圖進(jìn)行重新渲染,以此反映對元數(shù)據(jù)的修改。類似的,如果照片從照片集中被刪除了,我們也會把它從視圖中移除。在render()函數(shù)中,我們使用Underscore微模板(通過_.template())來用ID #photoTemplate對一個(gè)Javascript模板進(jìn)行渲染。這樣會簡單的返回一個(gè)編輯了的HTML字符串用來填充photoEL的內(nèi)容。
這為我們提供了一個(gè)非常輕量級的,簡單的管理模型和視圖之間的變更的方法。
后面的章節(jié)我們將會對Backbone和傳統(tǒng)MVC之間的區(qū)別進(jìn)行一下重新審視,但現(xiàn)在還是讓我們專注于控制器吧。
在Backbone中,控制器的責(zé)任一分為二,由Backbone.View和Backbone.Router共享.前段時(shí)間Backbone確曾有其屬于自己的Backbone.Controller,但是對這一組件的命名對于它所被使用的上下文環(huán)境中并沒有什么意義,后來它就被重新命名為Router了。
Router比控制器要負(fù)擔(dān)處理著更多一點(diǎn)點(diǎn)的責(zé)任,因?yàn)樗沟脼槟P徒壎ㄊ录?以及讓我們的視圖對DOM事件和渲染產(chǎn)生響應(yīng),成為可能.如Tim Branyen(另外一名基于Bocoup的Backbone貢獻(xiàn)者)在以前所指出的,為此完全擺脫不使用Backbone.Router是有可能的,因此一種考慮讓它使用Router范式的做法可能像下面這樣:
var PhotoRouter = Backbone.Router.extend({
routes: { "photos/:id": "route" },
route: function( id ) {
var item = photoCollection.get( id );
var view = new PhotoView( { model: item } );
$('.content').html( view.render().el );
}
});
總之,本節(jié)的重點(diǎn)是控制器管理著應(yīng)用程序中模型和視圖之間的邏輯和協(xié)作。
MVC中關(guān)注分離的思想有利于對應(yīng)用程序中功能進(jìn)行更加簡單的模塊化,并且使得:
盡管當(dāng)今主流的JavaScript框架都嘗試引入MVC的模式,來更好地面對web應(yīng)用的開發(fā)。由Peter Michaux編寫的Maria.js ,是一個(gè)嘗試純正的Smalltalk-80的框架。其中,Model只是Model,View也只完成View應(yīng)該做的,controller則只負(fù)責(zé)控制。然后,一些開發(fā)人員認(rèn)為,MV*架構(gòu)更值得關(guān)注,如果你對純正的MVC架構(gòu)的JavaScript實(shí)現(xiàn)感興趣,這將是很好的參考。
在這本書的這一點(diǎn)上,我們應(yīng)該對MVC模式提供了些什么有了一個(gè)基礎(chǔ)的了解,然而仍然有一些值得去關(guān)注的非常美妙的信息。
GoF并不將MVC引述為一種設(shè)計(jì)模式,而是把它看做是構(gòu)建一個(gè)用戶界面的類的集合.按照他們的觀點(diǎn),它實(shí)際上是三種經(jīng)典設(shè)計(jì)模式的變異組合:觀察者模式,策略模式和組件模式.依賴于框架中的MVC如何實(shí)現(xiàn),它也可能會使用工廠和模板模式.GoF Book提到這些模式在使用MVC工作時(shí)是非常有用的附加功能。
如我們所討論的,模型代表應(yīng)用程序的數(shù)據(jù),而視圖則是用戶在屏幕上看到的被展現(xiàn)出來的東西.如此,MVC它的一些核心的通訊就要依賴于觀察者模式(令人驚奇的是,一些相關(guān)的內(nèi)容在許多關(guān)于MVC模式的書籍并沒有被涵蓋到).當(dāng)模型被改變時(shí),它會通知觀察者(視圖)一些東西已經(jīng)被更新了——這也許是MVC中最重要的關(guān)系。觀察者的這一特性也是實(shí)現(xiàn)將多個(gè)視圖連結(jié)到同一個(gè)模型的基礎(chǔ)。
對于那些對MVC解耦特性想了解更多的開發(fā)者(這再一次依賴于特定的實(shí)現(xiàn)),這一模式的目標(biāo)之一就是幫助去實(shí)現(xiàn)一個(gè)主體(數(shù)據(jù)對象)和它的觀察者之間的一對多關(guān)系的定義。當(dāng)一個(gè)主體發(fā)生改變的時(shí)候,它的觀察者也會被更新。視圖和控制器有一種稍微不同的關(guān)系.控制器協(xié)助視圖對不同的用戶輸入做出響應(yīng),這也是一個(gè)策略模式的例子。
回顧完經(jīng)典的MVC模式以后,我們現(xiàn)在應(yīng)該理解了它是如何允許我們對一個(gè)應(yīng)用程序中的各個(gè)關(guān)注點(diǎn)進(jìn)行清晰地的區(qū)分.我們現(xiàn)在也應(yīng)該感恩于Javascript MVC框架在它們對MVC模式的詮釋中是如何的不同,而其對變異也是相當(dāng)開放的,仍然分享著其原生模式已經(jīng)提供的其中一些基礎(chǔ)概念。
當(dāng)審視一個(gè)新的Javas MVC/MV*框架時(shí),請記住——回過頭去考察考察它如何選擇相近的架構(gòu)(特別的,它支持實(shí)現(xiàn)了模型,視圖,控制器或者其它的一些可選特性)可能會有些用處,因?yàn)檫@樣能夠更好的幫助我們深入了解這一框架預(yù)計(jì)需要被如何拿來使用。