1. 程式人生 > >JavaScript MVC js也mvc

JavaScript MVC js也mvc

JavaScript MVC

中文:http://blog.youmila.com/?p=423 —from [email protected]
英文:http://www.alistapart.com/articles/javascript-mvc/ —from Jonathan Snook

mvc

javascript 已經從一個“小演員”發展成為舞臺的中心”人物“。它的足跡已經遍佈我們的伺服器和發展計劃的一覽表中,並且正在持續增長中。因此我們必須思考怎樣才能提高我們的javascript程式碼的重用性和更容易維護性呢?或許,MVC能夠給我們一些好的提示。
MVC對於後端應用程式開發及其開發人員來說是一個熟悉的術語。
正在使用的類似框架比如:Struts, Ruby on Rails, 和CakePHP。 MVC 起源於使用者介面的發展。藉助於它佈局客戶端應用程式的結構。讓我們一起來看下MVC是什麼。看看我們如何在一個專案中用mvc重寫它。並且思考一些現在已經存在的MVC框架。
What is MVC?(MVC是什麼?)
這個縮寫詞已經在前面提到了6次,如果你從來沒有聽說過,那一定迫不及待的逍遙知道MVC代表什麼,MVC代表

Model-View-Controller. 它是一個將應用程式分成3個部分的設計模式:model層是資料層,view層是資料對使用者的表現形式,controller層是使用者互動採取的行為動作。

追溯到1978年在Xeroc PARC, Trygve Reenskau發表了 recalled the origin of the MVC concept (PDF):(這篇文章成為了MVC的起源)

這部分原文我就不翻譯了哈(保留原味的好哈):

There are four roles in this user interaction paradigm. The human User has a mental model of the information he is currently working with. The object playing the Model role is the computer’s internal and invisible representation of this information. The computer presents different aspects of the information through objects playing the View role, several Views of the same model objects can be visible on the computer screen simultaneously. Objects playing the Controller role translate User commands into suitable messages for the View or Model objects as needed.

換句話說,使用者在做某件事情時,這件事情被轉到controller這邊,並且controller知道下一步去做什麼,一般來說 controller會從model層這邊請求資料,並且把獲取到的資料放到view層並且顯示給使用者。但是這樣的劃分層結構,對於一個網站或者是web 應用程式來說意味著什麼呢?

Foundations(基礎)

靜態文件時web頁面的基礎,給我們服務的每個頁面都反映了他們在伺服器那一刻的資訊狀態。但是我們得到不止是原始資料,而是包含原始資料的xhtml或者html資料,並且通過已經預定義的css渲染後的漂亮的頁面。

多年前,如果你想去修改原始資料,服務端必須提供一個可以文字輸入的頁面去做改動。那時候我們把改動後的資訊傳送給服務端,並且等待服務端返回ok 的反饋後才能搞定。每次都是完整的請求一個新的頁面,然後在等待伺服器反饋,這樣讓我們使用者感到乏味不堪,甚至當你出現錯誤的時候,還需要重新鍵入原來輸入的資訊。

The knight in shining armor (鎧甲騎士)

後來,web早期的黑暗迎來了他們的拯救者,鎧甲騎士 –javascript和ajax。

他們結束以前的整個頁面請求的方式,可以單個元素髮送使用者請求到後端伺服器。

並且也允許使用者頁面傳送請求的時候,繼續響應使用者的其他操作。

現在我們需要在javascript和ajax的發展和運用中採用MVC的模式分離程式碼:

比如說:在某些情況下,分離可能是不需要的,甚至某些情況下,分離會造成很多不必要的程式冗長。當我們的應用程式便得越來越複雜,需要 javascript在網站的多數部分的互動操作的時候。我們把javascript分離進入MVC模式能夠產生出更多元化,更重複利用的程式碼。

Structuring our code(構造我們的程式碼)

javscript是個傻瓜,他不會明白html將要告訴使用者什麼或者使用者想在這個頁面完成什麼。所以我們作為開發者,就必須告訴我們的javascript,使用者的輸入意味著什麼。

思考下面的例子,如果我們需要驗證表單中的資料,我們可以設定一個事件來處理這個任務,在這個任務中,事件處理函式去遍歷表單中的欄位列表,並且確定怎樣去反饋出錯誤的結果。

function validateForm(){
var errorMessage = ‘The following errors were found:
‘;
if (document.getElementById(‘email’).value.length == 0) {
errorMessage += ‘You must supply an email address
‘;
}
document.getElementById(‘message’).innerHTML = errorMessage;
}

上面得這個方法能工作,但是不夠靈活,比如我們如果想要增加個欄位驗證,或者另一個頁面有不同表單驗證。那我們就不得不拷貝這個函式的大部分程式碼為我們每次新增加欄位驗證。

Toward modularity(奔向模組化)

第一步是奔向模組化,並且分離就是在表單的欄位中新增語義化的東西。

比如對於email驗證的表單欄位我們可以這樣做:

<input type="text" class="required email">

這樣我們的javascript會遍歷所有的表單欄位從class中拖出這個屬性從那執行相應的程式。(這裡的class屬性保留了雙重含義,一個是css的樣式設定另一個就是js的目標物件。多麼便捷哈!~)

上面這種方式和頁面的結構已經語義化標記緊密纏繞在一起,但是這種方式也有一定限制條件,比如沒有條件判定式,而且在html標記中不能構造條件邏輯。比如:我們說如果一個欄位完成,需要另一個唯一的欄位。(可能你要說能但是很笨的方法。)


<input name="other" type="checkbox" /> Other
<textarea class="dependson-other"></textarea>

在上面得這個例子當中我們用了字首 dependson 指出 textarea是依靠 checkbox才出現的。為了避免這種拙劣的方法,我們可以在javascript中定義這塊業務邏輯。

Using JavaScript to describe things(用javascript去描述事物)

當我們在html中增加語義化標記以及元資料的時候,我們最終的目的是獲取資訊給javascript。但是以javascript方式描述資料是相對比較方便的。
這裡就是一個例子:(其實就是用json方式描述)


var fields = {
'other': {
required:true
},
'additional: {
'required': {
'other':{checked:true},
'total':{between:[1,5]}
},
'only-show-if': {
'other': {checked:true}
}
}
};

在這個例子中附加欄位會有幾個依賴關係。依賴關係中的每一個都被描述了。並且有各種各樣的資訊定義在其中。在這種情況下,附加欄位需要兩個欄位滿足條件,並且附加欄位只有在使用者選擇other的checkbox時才會顯示出來。
在這時候,javascript被用於如何驗證發生後欄位和業務邏輯的描述定義,在這一層中我們已經分離了一部分資料進入他們自己的物件中。但是這些驗證仍然期望在有效的變數資料中應用。希望在頁面中能夠展示出相應的錯誤摘要。
儘管這個例子已經有一點分離。但是這裡驗證過程仍然有很多的依賴性,比如:資料的驗證和驗證的後的報錯結果顯示仍然存在緊密的耦合。
那就讓我們思考一下怎樣才能用mvc模式構建我們的程式碼。並且然後構建我們表單驗證的例子。

The Model

既然mvc有三個主要組成部分,那麼我們的程式也要相應的劃分成至少3個主要物件。

分離model層進入它自己的物件是比較容易的,正如我們早期看到那個表單驗證的例子,這個常常發生的很自然。

讓我們來看那下另外一個例子吧。假設我們有一個日曆事件,這個事件的資料將會儲存在它自身的物件中,增加到物件的中的方法是抽象了直接與資料互動的過程。這些方法經常被增刪改查的任務呼叫,比如:建立,讀取,更新,刪除等操作。

var Events = {
get: function (id) {
return this.data[id];
},
del: function (id) {
delete this.data[id];
AjaxRequest.send(‘/events/delete/’ + id);
},
data:{
’112′: { ‘name’: ‘Party time!’, ‘date’: ’2009-10-31′ },
’113′: { ‘name’: ‘Pressies!’, ‘date’: ’2009-12-25′ }
}
metadata: {
‘name’: { ‘type’:'text’, ‘maxlength’:20 },
‘date’: { ‘type’:'date’, ‘between’:['2008-01-01','2009-01-01'] }
}
}

我們需要一個方法去描述資料。所以我們增加這個metadata欄位來描述資料的型別以及資料的限定條件。
上面這個增刪改查的任務也在伺服器端儲存狀態改變資訊,在這個例子中,delete函式從本地的物件儲存中刪除了資料的登記之後就將刪除物件的id通過ajax傳送一個刪除命令請求到伺服器端,然後從伺服器端刪除。
在儲存資料的時候,使用命名鍵值的方法,這是最快最有效率從物件中取回資料的方法。
這種方式通常和資料庫主鍵一樣。(上面得例子用了number型別的id)。對於一個事件日曆來說,按照月份劃分儲存資料會更實用。這樣做就不用遍歷所有的事件去發現那個需要的在頁面中渲染的事件了。你會發現這樣做事最好的辦法。

The View

在mvc模式中,view負責接收資料並且決定資料如何顯示。view層可以用頁面已存在的html,也可以從伺服器端請求一個新的html元件,還可以自己通過dom建立新的html元素。合併提供的資料以檢視的形式顯示給使用者,有一點很重要,就是view層並不關心資料來自哪裡,或者怎麼獲取到,它只負責取走資料使用。

View.EventsDialog = function(CalendarEvent) {
var html = ‘

{name}

‘ +

{date}

‘;
html = html.replace(/\{[^\}]*\}/g, function(key){
return CalendarEvent[key.slice(1,-1)] || ”;
});
var el = document.getElementById(‘eventshell’);
el.innerHTML = html;
}

var Events.data = {
’112′: { ‘name’: ‘Party time!’, ‘date’: ’2009-10-31′ },
’113′: { ‘name’: ‘Pressies!’, ‘date’: ’2009-12-25′ }
}

View.EventsDialog(Events.data['112']); // edits item 112

觀察上面得程式我們能夠發現它包含三個部門:
EventsDialog 函式對CalendarEvent這個json物件的name和date屬性進行了解析顯示。Events的data屬性儲存了日曆的時間。 View.EventsDialog的呼叫使112顯示。
這裡Events Dialog的view能夠被擴充套件,加入一個附加函式能夠使它進行有效的互動。在下面得例子中,Events Dialog被給了一個open方法和一個close方法。通過這樣做能夠使view提高自己的感知能力,同時也能夠被controller層更好的使用 view層,並不需要知道它實現的細節。

View.EventsDialog = function(CalendarEvent){ … }
View.EventsDialog.prototype.open = function(){
document.getElementById(‘eventshell’).style.display = ‘block’;
}
View.EventsDialog.prototype.close = function(){
document.getElementById(‘eventshell’).style.display = ‘none’;
}

var dialog = new View.EventsDialog(eventObject);
dialog.open();
dialog.close();

Generalizing Views(概括觀點)

使視野變得覺察到資料模型和資料檢索的方法是一容易墜入的陷阱. 分離這些函式不過是想讓他們在其他方面能重新使用這個dialog。在這個例子當中,如果分離了事件的資料和dialog,那麼我們能總結dialog屬於view層中,dialog不只適用events類的模型,也能應用到其他模型。

View.Dialog = function(data) {
var html = ‘

‘ + data.name + ‘

‘;
delete data.name;
for(var key in data) {
html += ‘

‘ + data[key] + ‘

‘;
}
var el = document.getElementById(‘eventshell’);
el.innerHTML = html;
}

我們現在有一個共有的方法去訪問一個任意物件的元素,而不僅僅是事件物件。在下一個需要dialog的專案中,我們可以合併這部分程式碼並且使用它。
很多JavaScript框架都是以這資料不可知論來設計的。比如yui controller ,jQuery UI widgets, ExtJS, 和 Dojo Dijit 從頭到位都是把通用性(泛用性)放在第一位來建立的。

Handling View methods(處理檢視方法)

一般來說,view層不能執行他們自己的方法,舉個例子來說,dialog(對話方塊)不能自己控制開關,應該由controller(控制器)–控制層來控制它是否開關。
如果一個使用者點選了一個dialog中的儲存按鈕,這個點選動作由控制器來接收,控制器傳送一個動作來決定dialog應該做什麼。可能是關閉dialog也能是顯示正在處理..一旦資料儲存了,ajax完成後會觸發控制器發出另一個隱藏指令來關閉dialog

無論怎樣,在有些情況下view層也能夠執行它自己的方法。比如一個view頁面中有一個以slide形式展示的輸入框,並且允許使用者提取裡面內容的時候,view會自己處理互動操作的,讓slide的內容顯示出來,這個時候就不需要controller(控制器)來操作這個互動了。

The Controller

現在,從 model層到view層資料是怎樣獲取到得呢?這就是通過controller層做的。controller啟用是在事件發生以後,多半是在頁面載入或者使用者發起的行為事件。一個事件處理程式被分配到一個controller(控制器)層的方法是做使用者的競標。

Controllers.EventsEdit = function(event) {
/* event is the javascript event, not our calendar event */
// grab the event target id, which stores the id
var id = event.target.id.replace(/[^d]/g, ”);
var dialog = new View.Dialog( Events.get(id) );
dialog.open();
}

當資料在在各種情況下使用的時候這種模式確實很方便。舉個例子:
我們正在編輯的日曆上顯示的事件,我們點選刪除按鈕,現在需要去消除dialog(對話方塊)和日曆上的事件,然後從伺服器中刪除該事件。

Controller.EventsDelete = function(event) {
var id = event.target.id.replace(/[^d]/g, ”);
View.Calendar.remove(id);
Events.del(id);
dialog.close();
}

controller的行為就變得相對容易理解和簡單了。這是建立可維護應用程式的關鍵。

Break it up(分解它)

現在我們瞭解了怎樣去分解我們的程式碼到他們的構成部分。讓我們重新回來開始部分的表單驗證的例子,我們怎樣才能用MVC模式去設計它以達到最大靈活性。

Validating our Model(驗證我們的模型)

該模型確定資料是否正確或不使用的方法。它不關心如何呈現概要的檢視。它只是需要報告哪些欄位沒有達到水平。

以前我們做過的那個例子當中,有一個簡單的變數“fields”儲存了我們資料模型的元資料格式,我們能夠用一個定義理解和檢測被給資料的方法去擴充套件那個物件,該方法能夠遍歷所有的資料並且比對他們在內部元資料型別中定義的需求。

var MyModel = {
validate: function(data) {
var invalidFields = [];
for (var i = 0; i < data.length; i++) {
if (this.metadata[data.key].required && !data.value) {
invalidFields[invalidFields.length] = {
field: data.key,
message: data.key + ‘ is required.’
};
}
}
return invalidFields;
},
metadata: {
‘other’: {required:true}
}
}

為了使我們的資料有效,我們提供一個數組key /value (鍵/值)對。key就是名字,value就是使用者鍵入的欄位的內容。

var data = [
{'other':false}
];

var invalid = MyModel.validate(data);

我們的無效變數現在包含任何沒有驗證欄位列表,現在我們要傳遞這些資料到view層中在頁面中顯示這些錯誤。

Presenting the invalid fields(顯示無效的欄位)

在這種情況下,我們需要在頁面中顯示一條錯誤資訊。這個顯示工作由view層完成,顯示的資料來自controller提供。view層會用這些資料構建一個錯誤資訊顯示給使用者,我們已經寫好了,並且可以在許多情況下使用它。

View.Message = function(messageData, type){
var el = document.getElementById(‘message’);
el.className = type;
var message = ‘

We have something to bring to your »
attention

‘ +

  • ‘;
    for (var i=0; i < messageData.length; i++) {
    message += ‘
  • ‘ + messageData[i] + ‘

    ‘;
    }
    message += ‘

‘;
el.innerHTML = message;
}
View.Message.prototype.show() {
/* provide a slide-in animation */
}

這裡的type 是css的一個class name,給使用者自定義message的型別樣式入口,
這個函式遍歷所有的message資料並把他們顯示在頁面中。

Hooking it all together with a Controller(用控制層掛起這一切流程)

我們的model層儲存了我們的資料並且能夠告訴我們資料是否有效,view層給使用者顯示成功或者失敗的訊息,就剩下最後一步了,就是使用者表單提交的時候驗證表單資訊。

/* use the event binding of your friendly neighbourhood
JavaScript library or whatever you like to use
*/
addEvent(document.getElementById(‘myform’), ‘submit’, »
MyController.validateForm);

MyController.validateForm = function(event){
var data = [];
data['other'] = document.getElementById(‘other’).checked;
var invalidFields = MyModel.validate(data);

if (invalid.length) {
event.preventDefault();
// generate the view and show the message
var message = new View.Message(invalidFields, ‘error’);
message.show();
}
}

好了我們完成了,model層和view層的方法我們以後還能重複利用哈

Frameworks

javascript mvc正在流行起來,但是 深入的理解怎樣在你的工作中運用它會更有幫助。你可以自己做,也可以用已經存在的javascript mvc框架

下面是幾個javascript mvc 框架:

你的應用程式是否需要一個框架,這依賴於應用程式的複雜性。如果它是個簡單的應用程式,那麼使用框架來做就不值當了。

請記住,推出自己的MVC框架並不是一種死板,正如你想的那樣它可以更靈活。

Finally

和其他的開發一樣,你一定要決定取捨,這種分離是否值當。如果只是一個簡單程式,只有幾個函式,那這種型別的分離就是顯得有些過分了。如果是個複雜的大程式那這種MVC模式的分離將會獲益匪淺!