当前位置: 首页 > 科技观察

前端MVC蜕变

时间:2023-03-13 14:05:18 科技观察

背景:MVC是一种架构设计模式,鼓励通过关注点分离改进应用程序组织。过去,MVC被广泛用于构建桌面和服务器端应用程序。如今,Web应用的开发越来越接近于传统的应用软件开发,Web与应用的界限进一步模糊。传统编程语言中的设计模式也慢慢融入到Web前端开发中。由于前端开发的环境特点,从经典的MVC模式衍生出很多MV*模式,在各种Javascript框架中实现了多少演变。在研究MV*模式和各种框架的过程中,是“剪不断,推理依旧乱”:为什么不同地方说的MVC不一样?MVP和MVVM的出现是为了解决什么问题?据说“Web前端开发中根本不能用MVC”?翻了一大堆资料,十万个理由,不过貌似view,model,controller,decoupling,monitor,notification,active,passive,registration,bindingdefinition,rendering等各种名词的排列组合好像汪峰的歌词。本文希望以通俗易懂的方式理清一些关系。因接触时间有限,英文阅读能力有限,难免会有误会。欢迎讨论和指正。MVC蜕变MVC历史MVC最初是在研究Smalltalk-80(1979)期间设计的。恐怕没有一本书能回到计算机石器时代来介绍Smalltalk代码是如何实现MVC的。当时的应用场景很难知道,这一切都要追溯到80后出生之前。但当时的图形界面非常少,施乐正在开发一种用户友好的图形界面,以取代远在千里之外的电脑屏幕上的命令行和DOS提示符。那时候的计算机世界是混乱的、综合的,然后出现了造物主,他把现实世界从模型中抽象出来形成模型,把人机交互从应用逻辑中分离出来形成视图,然后就有了空气、水、鸡肉、鸡蛋什么的。《设计模式:可复用面向对象软件的基础》发表于1995年,对MVC进行了深入的讲解,对其使用起到了重要的推广作用。MVC包括三种类型的对象,将它们分开以提高灵活性和可重用性。模型模型用于封装与应用程序业务逻辑相关的数据和数据的处理方法。将有一个或多个视图收听此模型。一旦模型的数据发生变化,模型会通知相关的视图。视图是描述模型当前状态的屏幕表示。当模型的数据发生变化时,视图有机会相应地刷新自己。控制器定义了用户界面如何响应用户输入,在不同层次之间起到组织作用,并用于控制应用程序的流程。它处理用户行为和数据模型的变化。经典MVC模式实线:方法调用虚线:事件通知涉及到两个设计模式:view和model之间的观察者模式,view观察model,提前注册到这个model上,让view知道datamodel上发生了什么变化.视图和控制器之间的策略模式策略是表示算法的对象。MVC允许在不改变视图外观的情况下改变视图响应用户输入的方式。例如,您可能希望更改视图对键盘的响应方式,或者使用弹出菜单而不是命令键。MVC将响应机制封装在控制器对象中。有一个控制器类层次结构,可以通过对现有控制器进行适当更改来轻松创建新控制器。视图使用控制器子类的实例来实现特定的响应策略。要实现不同的响应策略,只需将它们替换为不同类型的控制器实例即可。您甚至可以通过更改视图的控制器来更改视图在运行时响应用户输入的方式。例如,可以通过简单地为视图提供一个忽略输入事件的控制器来禁止视图接受任何输入。好了,如果你被以上言论搞糊涂了,请继续往下看《设计模式:可复用面向对象软件的基础》。MVCforJAVASCRIPT我们回顾了经典的MVC,接下来要说的MVC主要是用Javascript实现的。javascriptMVC模式的源码图如图所示。视图承担了控制器的部分功能,负责处理用户输入,但不需要知道下一步要做什么。它依靠控制器为她做出决定或处理用户事件。事实上,前端视图已经具备独立处理用户事件的能力。如果每个事件都要流经控制器,势必会增加复杂度。同时,视图也可以委托控制器处理模型的变化。模型数据发生变化后,通知视图更新显示给用户。这个过程是一个循环,一个循环的过程。这种从经典MVC到JavascriptMVC的1对1转换让控制器的角色有点尴尬。MVC这种结构的正确性在于,任何界面都需要面向用户,而控制器“是连接用户和系统的纽带”。在经典的MVC中,控制器要做的大部分事情是将用户输入分发到不同的视图,并在必要时从视图中获取用户输入以更改模型。在Web和目前的大部分UI系统中,控制器的职责已经由系统来实现。由于某种原因,控制器和视图之间的分界线越来越模糊。也有人认为视图启动action时应该将视图归于控制器。例如在Backbone中,Backbone.View和Backbone.Router一起承担了控制器的职责。这为MVC中控制器的发展铺平了道路。MVPMVP(model-view-Presenter)是经典MVC设计模式的衍生,由Taligent在1990年代创立,是C++CommonPoint的典范。后台不再研究,看上图就知道和MVC的区别了。在MVP模式经典MVC中,捆绑了一对控制器视图来表示一个ui组件。controller直接接受用户输入并将输入转化为相应的命令来调用模型的接口,修改模型的状态,最后通过观察者模式重新渲染视图。进化成MVP的切入点是修改controller-view的绑定关系。为了解决controller-view的绑定关系,会进行改造,让view既有UI组件的结构,又有处理用户事件的能力,让controller可以独立出来。为了统一管理用户事件,view只负责将用户产生的事件传递给controller,由controller统一处理。这样做的好处是多个视图可以共享同一个控制器。这时候controller也已经从组件级别提升到应用级别,但是更新view的方式还是和经典的MVC一样:通过Presenter更新model,通过observer模式更新view.还有一个明显的区别就是MVC是一个循环,一个循环的过程,而MVP不是,依靠Presenter为核心,负责从模型中取出数据填充到视图中。MVP常见的实现方式是被动视图(passiveview)。Presenter观察模型而不是视图观察模型。一旦模型改变,视图将被更新。Presenter有效地将模型绑定到视图。视图公开setter接口,以便Presenter可以设置数据。使用这种被动视图结构,没有直接数据绑定的概念。但它的好处是直接在视图和模型之间提供更清晰的分离。但是缺少数据绑定支持意味着您必须单独专注于一项任务。在MVP中,应用的逻辑主要在Presenter中实现,其中view是一个很薄的层。MVVMMVVM,Model-View-ViewModel,最初是微软在使用WindowsPresentationFoundation和SilverLight时定义的。2005年,JohnGrossman在一篇关于Avalon(WPF的代号)的博文中正式宣布了它的存在。如果你用过VisualStudio,新建一个WPF应用程序,然后在“设计”中拖入一个控件,双击后在“代码”中编写一个事件处理程序,或者绑定一个数据源。我对这个MVVM有点感觉。比如下面VS自动生成的代码:"/>stackPanel1.DataContext=newStudent(){StudentID=20130501,Name="张三",EntryDate=DateTime.Parse("2013-09-01"),Credit=0.0};最重要的功能之一是数据绑定,Data-binding。没有前后端分离,一个开发人员可以搞定一切。一只手抓业务逻辑,一只手抓数据访问,顺便拖拽几个UI控件,绑定数据源到某个对象或某个表,一步到位。背景介绍完了,我们来看看MVVM模型的理论图。首先,视图和模型不知道彼此的存在。和MVP一样,视图和模型是明确分开的。其次,视图是viewmodel的对外展示,与viewmodel保持同步,viewmodel对象可以看作是view的context。视图绑定到视图模型的属性。如果viewmodel中的属性值发生变化,这些新值会通过数据绑定自动传递给视图。视图模型反过来将模型中的数据和特定状态暴露给视图。因此,view并不知道model的存在,viewmodel和model也不知道view。实际上,model完全忽略了viewmodel和view的存在。这是一个非常松耦合的设计。流行的MV*框架:每个框架都有自己的特点,这里主要讨论MVC的三个角色的职责。简要浏览一下每个框架的代码结构和风格。BackboneJSBackbone通过提供模型Model、集合Collection和视图View,赋予Web应用层次结构,其中模型包含领域数据和自定义事件;CollectionCollection是有序或无序的模型集合,具有丰富的可枚举API;视图可以声明事件处理程序。最后,将模型、集合和视图与服务器端的RESTfulJSON接口连接起来。在Backbone的升级过程中,去掉了controller,controller被view和router代替。视图专注于处理用户事件(如点击、按键等)、呈现HTML模板以及与模型数据交互。Backbone的模型不绑定UI视图数据,而是需要在视图中操作DOM来更新或读取UI数据。Router提供了很多客户端路由的方法,可以连接指定的动作(actions)和事件(events)。Backbone是一个小巧灵活的库,只是为了帮你实现一个MVC模型框架,更多的需要自己去实现。适合有一定web基础,喜欢用原生JS操作DOM的开发者(因为没有数据绑定)。为什么叫库,而不是框架,不仅仅是因为只有4KB的代码,更重要的是,有了库,你就有了控制权。如果你用了框架,控制就反过来了,框架在控制你。库可以提供灵活性和自由度,但框架强制执行某种方式来减少代码重复。这是Backbone和Angular的区别之一。至于MV*中的Backbone属于哪种模式,有人认为不是MVC,也有人认为更接近MVP。事实上,它从多种架构模式中借鉴了一些好的概念来创建一个灵活的框架,并且运行良好。你不必坚持某种模式。//view:varAppview=Backbone.View.extend({//每个view都需要引用一个DOM元素,就像ER中的main属性一样。el:'#container',//view不包含html标签,有是对template.template:_.template("

Hello<%=who%>

")的引用,//初始化方法initialize:function(){this.render();},//$el是一个缓存的jQuery对象render:function(){this.$el.html("HelloWorld");},//事件绑定events:{'keypress#new-todo':'createTodoOnEnter'}});varappview=newAppview();//model://每个应用的核心,包括交互数据和逻辑//比如数据校验、getter、setter、默认值、数据初始化、数据转换varapp={};app.Todo=Backbone.model.extend({defaults:{title:'',completed:false}});//创建模型实例vartodo=newapp.Todo({title:'LearnBackbone.js',completed:false});todo.get('title');//"LearnBackbone.js"todo.get('completed');//falsettodo.get('created_at');//undefinedtodo.set('created_at',Date());todo.get('created_at');//"我们dSep12201212:51:17GMT-0400(EDT)"//collection://一个有序的模型集合,可以设置或获取model//监听集合中的数据变化,从后端获取模型数据并持久化.app.TodoList=Backbone.Collection.extend({model:app.Todo,localStorage:newStore("backbone-todo")});//集合实例vartodoList=newapp.TodoList()todoList.create({title:'LearnBackbone\'sCollection'});//模型实例varmodel=newapp.Todo({title:'Learnmodels',completed:true});todoList.add(model);todoList.pluck('title');todoList.pluck('完全的');KnockoutJSnockoutJS是一个合法的MVVM框架,它通过简洁易读的数据绑定语法将DOM元素与viewmodel关联起来。当模型(viewmodel)的状态更新时,UI界面也会自动更新。viewmodel是模型和视图上的操作之间的连接,是一个纯Javascript对象。它不是UI,没有控件和样式的概念,也不是持久化的模型数据。它只是保存一些用户正在编辑的数据,然后公开操作(添加或删除)这些数据的方法。视图是视图模型中数据的可视化显示。视图观察视图模型。操作视图时,它向视图模型发送命令,并在视图模型发生变化时进行更新。视图和模型不知道彼此的存在。Newitem:0">添加

你的项目:

//viewmodelvarSimpleListmodel=function(items){this.items=ko.observableArray(items);this.itemToAdd=ko.observable("");this.addItem=function(){if(this.itemToAdd()!=""){//将input中的值添加到items中,select控件会自动更新this.items.push(this.itemToAdd());//清除input中的值this.itemToAdd("");}//确保这里一直是viewmodel}.bind(this);};ko.applyBindings(newSimpleListmodel(["Alpha","Beta","Gamma"]));AngularJSAngularJS试图成为Web应用程序中的端到端解决方案。这意味着它不仅仅是您Web应用程序的一小部分,而是一个完整的端到端解决方案。这使得AngularJS在构建CRUD应用程序时显得呆板和不灵活。AngularJS旨在克服HTML在构建应用程序方面的缺点。它使用不同的方法,试图填补HTML本身在构建应用程序中的空白。通过使用标识符(指令)结构,浏览器可以识别新语法。例如,使用双花括号语法进行数据绑定;使用ng-controller指定每个控制器负责监控视图的哪一部分;使用ng-model将输入数据绑定到模型中的部分属性。双向数据绑定是AngularJS的另一个特性。对UI控件的任何更改都会立即反映在模型变量中(一个方向),对模型变量的任何更改都会立即反映在问候语文本中(另一个方向)。AngularJS使用范围来保持数据模型和视图界面UI之间的双向同步。一旦模型的状态发生变化,AngularJS会立即刷新并反映在视图界面中,反之亦然。AngularJS本来是偏向MVC的,但是随着项目重构和版本升级,现在更接近MVVM。和Knockout视图中的样式类似,都是从WPF派生出来的,只是Knockout使用自定义属性data-bind作为绑定入口,而AngularJS对HTML进行了更彻底的改造,扩展了HTML的语法,引入了一系列指令。在AngularJS中,视图是模型在通过HTML模板呈现后的映射。这意味着每当模型发生变化时,AngularJS将实时更新连接点并相应地更新视图。例如,视图组件由AngularJS使用以下模板构建:
    {{phone.name}}{{phone.snippet}}

li标签内的ng-repeat语句是一个AngularJS迭代器。phone.name和phone.snippet周围的花括号标识数据绑定,它是对应用程序数据模型的引用。页面加载时,AngularJS会根据模板中的属性值,将其绑定到数据模型中的同名变量,保证两者的同步。数据模型在PhoneListCtrl控制器中初始化://controller:functionPhoneListCtrl($scope){//数组中存储的对象是手机数据列表$scope.phones=[{"name":"NexusS","snippet":"FastjustgotfasterwithNexusS."},{"name":"MotorolaXOOM?withWi-Fi","snippet":"TheNext,NextGenerationtablet."},{"name":"MOTOROLAXOOM?","snippet":"TheNext,NextGenerationtablet."}];}虽然controller看起来没有任何控制功能,但它在这里的重要性在于它允许通过给定数据模型的作用域$scope来建立模型和视图之间的数据绑定.方法名称PhoneListCtrl与body标记中的ngcontroller指令的值相匹配。当应用程序启动时,会创建一个根作用域,而控制器作用域是根作用域的典型继承者。此控制器范围对标记内的所有数据绑定有效。AngularJS的作用域理论非常重要:作用域可以看作是模板、模型和控制器协同工作的粘合剂。AngularJS使用范围,以及模板、数据模型和控制器中的信息。这些可以帮助保持模型和视图分离,但它们都是同步的!对模型的任何更改都会立即反映在视图中;对视图的任何更改都会立即反映在模型中。实践中的思考我们使用的MVC框架是ER,它适合并且可以很容易的构建一个全站式的AJAXWeb应用。提供精简的核心动作、模型和视图抽象,使构建RIA应用程序变得简单可行。在使用过程中,我可以近距离体验到很多优秀的设计理念。也让我开始思考每个角色的转变。让view走到前线,我开始思考action(controller)的角色。我觉得从纯解耦的角度来说,view和model不应该知道对方的存在,所有的事件流和数据、UI的处理都应该流过action。但这是极不现实的。当用户操作一个UI,需要更新模型的一条数据时,需要触发action,通过action调用模型的set方法。这个有点麻烦,因为在视图中有模型的应用,数据设置一行代码就搞定了。因此,我自己定了一个规则:如果是简单的模型数据读写,可以直接在视图中操作;如果要进行复杂的数据处理,则必须通过操作进行。于是,遇到了偷懒不得的情况(必须通过action):比如有一个主动作main,两个子动作listandselect,用户在viewin中选择一条数据列表并将其添加到右侧的选择中。经历的过程是这样的:实践中的思考sub-action中的listView接收UI事件,触发给listAction,listAction继续触发事件给mainView,mainaction处理其他的sub-action。mainView接收到事件,调用sub-Action的selectAction方法。selectAction继续调用selectView方法完成UI更新。涉及车型的变化暂不考虑。我在想,既然在经典的MVC中view接管了controller的角色来接受用户事件,那么如果我们借鉴Backbone的思路,我们可以把view作为controller的一个实现,推到战场的最前沿.保存两次转移动作是不是更简单?模型驱动开发在实际开发中,视图往往是核心,需要在页面上显示数据源,而数据源是在模型中设置的。当用户事件发生时,我会在动作中更新模型,然后刷新视图。有时漏更新模型,等到需要数据时才发现没有保存在模型中。模型本身是独立的,自控的,不依赖视图,可以支持多个视图同时显示。就像linux上的应用程序通常提供两种操作方式,图形界面和命令行。那么如果以模型为核心,模型驱动发展,数据在手,世界在我,通过模型验证来保证数据的完整性和正确性。实施数据绑定,对模型的任何更改都将反映在界面上。那么我们只需要预先写好view和model的关系映射(类似viewmodel),然后只关注model数据,就OK了。