英文:http://www.alistapart.com/articles/javascript-mvc/ from Jonathan Snook
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的起源)
这部分原文我就不翻译了哈(保留原味的好哈):
(本文来源于图老师网站,更多请访问http://m.tulaoshi.com/webkaifa/)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.
(本文来源于图老师网站,更多请访问http://m.tulaoshi.com/webkaifa/)换句话说,用户在做某件事情时,这件事情被转到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模式构建我们的代码。并且然后构建我们表单验证的例子。
既然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)。对于一个事件日历来说,按照月份划分存储数据会更实用。这样做就不用遍历所有的事件去发现那个需要的在页面中渲染的事件了。你会发现这样做事最好的办法。
在mvc模式中,view负责接收数据并且决定数据如何显示。view层可以用页面已存在的html,也可以从服务器端请求一个新的html组件, 还可以自己通过dom创建新的html元素。合并提供的数据以视图的形式显示给用户,有一点很重要,就是view层并不关心数据来自哪里,或者怎么获取 到,它只负责取走数据使用。
View.EventsDialog = function(CalendarEvent) {
var html = ‘
‘ +
‘
{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();
使视野变得觉察到数据模型和数据检索的方法是一容易坠入的陷阱. 分离这些函数不过是想让他们在其他方面能重新使用这个dialog。在这个例子当中,如果分离了事件的数据和dialog,那么我们能总结dialog属 于view层中,dialog不只适用events类的模型,也能应用到其他模型。
View.Dialog = function(data) {
var html = ‘
‘;
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 从头到位都是把通用性(泛用性)放在第一位来建立的。
一般来说,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 = ‘
‘ +
‘
‘;
}
message += ‘
‘;
el.innerHTML = message;
}
View.Message.prototype.show() {
/* provide a slide-in animation */
}
这里的type 是css的一个class name,给用户自定义message的类型样式入口,
这个函数遍历所有的message数据并把他们显示在页面中。
我们的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层的方法我们以后还能重复利用哈
Frameworksjavascript mvc正在流行起来,但是 深入的理解怎样在你的工作中运用它会更有帮助。你可以自己做,也可以用已经存在的javascript mvc框架
下面是几个javascript mvc 框架:
JavaScriptMVCSproutCoreTrimJunction你的应用程序是否需要一个框架,这依赖于应用程序的复杂性。如果它是个简单的应用程序,那么使用框架来做就不值当了。
请记住,推出自己的MVC框架并不是一种死板,正如你想的那样它可以更灵活。
Finally和其他的开发一样,你一定要决定取舍,这种分离是否值当。如果只是一个简单程序,只有几个函数,那这种类型的分离就是显得有些过分了。如果是个复杂的大程序那这种MVC模式的分离将会获益匪浅!