前端MVC变形记

426 查看

背景:

MVC是一种架构设计模式,它通过关注点分离鼓励改进应用程序组织。在过去,MVC被大量用于构建桌面和服务器端应用程序,如今Web应用程序的开发已经越来越向传统应用软件开发靠拢,Web和应用之间的界限也进一步模糊。传统编程语言中的设计模式也在慢慢地融入Web前端开发。由于前端开发的环境特性,在经典MVC模式上也引申出了诸多MV*模式,被实现到各个Javascript框架中都有多少的衍变。在研究MV*模式和各框架的过程中,却是“剪不断、理还乱”:

  1. 为什么每个地方讲的MVC都不太一样?
  2. MVP、MVVM的出现是要解决什么问题?
  3. 为什么有人义正言辞的说“MVC在Web前端开发中根本无法使用”?

带着十万个为什么去翻阅很多资料,但是看起来像view、model、controller、解耦、监听、通知、主动、被动、注册、绑定、渲染等各种术语的排列组合,像汪峰的歌词似的。本篇希望用通俗易懂的方式阐述清楚一些关系,由于接触时间有限,英文阅读能力有限,可能会存在误解,欢迎讨论和纠正。

MVC变形记

MVC历史

MVC最初是在研究Smalltalk-80(1979年)期间设计出来的,恐怕没有一本书能够回到计算机石器时代介绍一下Smalltalk的代码是如何实现MVC的,不仅如此,连想搞清楚当时的应用场景都很难了,都要追溯到80后出生以前的事了。但是当时的图形界面少之又少,施乐公司正在研发友好的用户图形界面,以取代电脑屏幕上那些拒人于千里之外的命令行和DOS提示符。那时计算机世界天地混沌,浑然一体,然后出现了一个创世者,将现实世界抽象出模型形成model,将人机交互从应用逻辑中分离形成view,然后就有了空气、水、鸡啊、蛋什么的。在1995年出版的《设计模式:可复用面向对象软件的基础》对MVC进行了深入的阐述,在推广使用方面发挥了重要作用。

MVC包括三类对象,将他们分离以提高灵活性和复用性。

  • 模型model用于封装与应用程序的业务逻辑相关的数据以及对数据的处理方法,会有一个或多个视图监听此模型。一旦模型的数据发生变化,模型将通知有关的视图。
  • 视图view是它在屏幕上的表示,描绘的是model的当前状态。当模型的数据发生变化,视图相应地得到刷新自己的机会。
  • 控制器controller定义用户界面对用户输入的响应方式,起到不同层面间的组织作用,用于控制应用程序的流程,它处理用户的行为和数据model上的改变。

经典MVC模式

实线:方法调用 虚线:事件通知

其中涉及两种设计模式:

  • view和model之间的观察者模式,view观察model,事先在此model上注册,以便view可以了解在数据model上发生的改变。
  • view和controller之间的策略模式

一个策略是一个表述算法的对象,MVC允许在不改变视图外观的情况下改变视图对用户输入的响应方式。例如,你可能希望改变视图对键盘的响应方式,或希望使用弹出菜单而不是原来的命令键方式。MVC将响应机制封装在controller对象中。存在着一个controller的类层次结构,使得可以方便地对原有的controller做适当改变而创建新的controller。

view使用controller子类的实例来实现一个特定的响应策略。要实现不同的响应的策略只要用不同种类的controller实例替换即可。甚至可以在运行时刻通过改变view的controller来改变用户输入的响应方式。例如,一个view可以被禁止接受任何输入,只需给他一个忽略输入事件的controller。

好吧,如果被上述言论绕昏了,请继续研读《设计模式:可复用面向对象软件的基础》。

MVC for JAVASCRIPT

我们回顾了经典的MVC,接下来讲到的MVC主要是在Javascript上的实现。

javascript MVC模式

源图

如图所示,view承接了部分controller的功能,负责处理用户输入,但是不必了解下一步做什么。它依赖于一个controller为她做决定或处理用户事件。事实上,前端的view已经具备了独立处理用户事件的能力,如果每个事件都要流经controller,势必增加复杂性。同时,view也可以委托controller处理model的更改。model数据变化后通知view进行更新,显示给用户。这个过程是一个圆,一个循环的过程。

这种从经典MVC到Javascript MVC的1对1转化,导致控制器的角色有点尴尬。MVC这样的结构的正确性在于,任何界面都需要面对一个用户,而controller “是用户和系统之间的链接”。在经典MVC中,controller要做的事情多数是派发用户输入给不同的view,并且在必要的时候从view中获取用户输入来更改model,而Web以及绝大多数现在的UI系统中,controller的职责已经被系统实现了。由于某种原因,控制器和视图的分界线越来越模糊,也有认为,view启动了action理论上应该把view归属于controller。比如在Backbone中,Backbone.View和Backbone.Router一起承担了controller的责任。这就为MVC中controller的衍变埋下了伏笔。

MVP

MVP(model-view-Presenter)是经典MVC设计模式的一种衍生模式,是在1990年代Taligent公司创造的,一个用于C++ CommonPoint的模型。背景上不再考证,直接上图看一下与MVC的不同。

MVP模式

经典MVC中,一对controller-view捆绑起来表示一个ui组件,controller直接接受用户输入,并将输入转为相应命令来调用model的接口,对model的状态进行修改,最后通过观察者模式对view进行重新渲染。

进化为MVP的切入点是修改controller-view的捆绑关系,为了解决controller-view的捆绑关系,将进行改造,使view不仅拥有UI组件的结构,还拥有处理用户事件的能力,这样就能将controller独立出来。为了对用户事件进行统一管理,view只负责将用户产生的事件传递给controller,由controller来统一处理,这样的好处是多个view可共用同一个controller。此时的controller也由组件级别上升到了应用级别,然而更新view的方式仍然与经典MVC一样:通过Presenter更新model,通过观察者模式更新view。

另一个显而易见的不同在于,MVC是一个圆,一个循环的过程,但MVP不是,依赖Presenter作为核心,负责从model中拿数据,填充到view中。常见的MVP的实现是被动视图(passive view),Presenter观察model,不再是view观察model,一旦model发生变化,就会更新view。Presenter有效地绑定了model到view。view暴露了setters接口以便Presenter可以设置数据。对于这种被动视图的结构,没有直接数据绑定的概念。但是他的好处是在view和model直接提供更清晰的分离。但是由于缺乏数据绑定支持,意味着不得不单独关注某个任务。在MVP里,应用程序的逻辑主要在Presenter来实现,其中的view是很薄的一层。

MVVM

MVVM,Model-View-ViewModel,最初是由微软在使用Windows Presentation Foundation和SilverLight时定义的,2005年John Grossman在一篇关于Avalon(WPF 的代号)的博客文章中正式宣布了它的存在。如果你用过Visual Studio, 新建一个WPF Application,然后在“设计”中拖进去一个控件、双击后在“代码”中写事件处理函数、或者绑定数据源。就对这个MVVM有点感觉了。比如VS自动生成的如下代码:

 

其中最重要的特性之一就是数据绑定,Data-binding。没有前后端分离,一个开发人员全搞定,一只手抓业务逻辑、一只手抓数据访问,顺带手拖放几个UI控件,绑定数据源到某个对象或某张表,一步到位。

背景介绍完毕,再来看一下理论图

MVVM模式

首先,view和model是不知道彼此存在的,同MVP一样,将view和model清晰地分离开来。 其次,view是对viewmodel的外在显示,与viewmodel保持同步,viewmodel对象可以看作是view的上下文。view绑定到viewmodel的属性上,如果viewmodel中的属性值变化了,这些新值通过数据绑定会自动传递给view。反过来viewmodel会暴露model中的数据和特定状态给view。 所以,view不知道model的存在,viewmodel和model也觉察不到view。事实上,model也完全忽略viewmodel和view的存在。这是一个非常松散耦合的设计。

流行的MV*框架:

每个框架都有自己的特性,这里主要讨论MVC三个角色的责任。粗浅地过一遍每个框架的代码结构和风格。

BackboneJS

Backbone通过提供模型Model、集合Collection、视图View赋予了Web应用程序分层结构,其中模型包含领域数据和自定义事件;集合Colection是模型的有序或无序集合,带有丰富的可枚举API; 视图可以声明事件处理函数。最终将模型、集合、视图与服务端的RESTful JSON接口连接。

Backbone在升级的过程中,去掉了controller,由view和router代替controller,view集中处理了用户事件(如click,keypress等)、渲染HTML模板、与模型数据的交互。Backbone的model没有与UI视图数据绑定,而是需要在view中自行操作DOM来更新或读取UI数据。Router为客户端路由提供了许多方法,并能连接到指定的动作(actions)和事件(events)。

Backbone是一个小巧灵活的库,只是帮你实现一个MVC模式的框架,更多的还需要自己去实现。适合有一定Web基础,喜欢原生JS去操作DOM(因为没有数据绑定)的开发人员。为什么称它为库,而不是框架,不仅仅是由于仅4KB的代码,更重要的是 使用一个库,你有控制权。如果用一个框架,控制权就反转了,变成框架在控制你。库能够给予灵活和自由,但是框架强制使用某种方式,减少重复代码。这便是Backbone与Angular的区别之一了。

至于Backbone属于MV*中的哪种模式,有人认为不是MVC,有人觉得更接近于MVP,事实上,它借用多个架构模式中一些很好的概念,创建一个运行良好的灵活框架。不必拘泥于某种模式。