首页 新闻 论坛 群组 Blog 文档 下载 读书 Tag 网摘 搜索 开源 FAQ 第二书店 博文视点 程序员
频道: 研发 数据库 中间件 信息化 视频 .NET Java 游戏 移动 服务: 人才 外包 培训
    图书品种:235680
       
热门搜索: ASP.NET Ajax Spring Hibernate Java
MVC对于Web应用并不陌生,甚至是在本书中大力抨击的传统基于页面的Web应用中,它也是我们的老熟人了。分离视图和模型对于Web应用来说是很自然的,因为它们本来就分属不同的机器[9]。那么,Web应用是否天生就符合MVC模式呢?换句话说,难道还有可能编写一个将视图和模型混杂在一起的Web应用吗?

很不幸,完全有可能出现这种情况,因为这太容易做到了,可能大多数Web开发者以前都这样做过,当然我们也未能幸免。

大多数Web MVC的提倡者都将生成的HTML页面以及生成页面的代码看作是视图,而不是将用户实际看到的页面所呈现的东西看作是视图。在一个为JavaScript客户端提供数据的Ajax应用中[10],按照上述观点来看视图是通过HTTP响应返回给客户端的XML文档[11]。将生成文档的代码与执行业务逻辑的代码分离开,确实需要一些纪律才行。

3.4.1  不使用模式的Ajax Web服务器层

为了展示所讨论的内容,我们来为Ajax应用开发一个示例的Web服务器层。我们已经在第2章和3.1.4节看到了一些基础的客户端Ajax代码,第4章还会回到客户端的代码。现在,我们将注意力集中在Web服务器端发生的事情。我们开始以尽可能简单的方式来编码,随后逐渐重构到MVC模式,看看它能为应用提高应对变化的能力带来哪些好处。首先,我们来介绍一下这个应用。

我们有一个服装店的服装列表,这个列表存储在数据库中。我们想要查询这个数据库,然后将条目的列表展示给用户,每个条目[12]包括图片、标题、简短的描述以及价格。如果某种服装有几种颜色或尺寸,还允许用户来选择。图3-6展示了这个系统的主要组成部分,即数据库、表示产品的数据结构以及传输到Ajax客户端的XML文档,在这个文档中列出了匹配查询的所有产品。

3-6  在在线商店的例子中,用来生成XML格式的产品数据的主要组成部分。在生成视图的过程中,从数据库中提取出一个结果集,使用它来组装表示每种服装的数据结构,然后以XML数据流的形式传输给客户端

假设用户刚刚进入商店,我们为他提供了男装、女装和童装几个选项。每个产品都属于这三种类别中的一种,通过数据库Garments表中的Category字段来标识。用来获得男装类的所有相关条目的简单SQL语句可能是这样:

SELECT * FROM garments WHERE CATEGORY = 'Menswear';

我们需要获取这个查询的结果,然后将它以XML的格式发送给Ajax应用。我们看看如何来做。

1. 为客户端生成XML数据

代码清单3-5展示了实现这个特定需求的迅速而粗糙[13]的解决方案。这个例子使用PHPMySQL数据库,但是我们关注的重点是大体上的结构。如果换成ASPJSP或者Ruby脚本,可能会得到结构类似的代码。

代码清单3-5  迅速而随性地从数据库查询结果生成XML数据流

代码清单3-5中的PHP页面可以生成类似于代码清单3-6XML页面,在这个例子中,数据库里有两个匹配的产品。这里,代码进行了缩排以便于阅读。之所以选择XML作为客户端和服务器之间通信的媒介,是因为它通常都用于这个目的,并且第2章也已经提到如何使用XML- HttpRequest对象来处理服务器端生成的XML文档。第5章将会更加详细地探讨客户端和服务器通信的其他选项。

代码清单3-6  代码清单3-5输出的简单的XML

我们有了一个Web服务器端的应用,假设在前端有一个很好的Ajax应用来处理这个XML。来展望一下未来。假设随着产品范围的扩大,要添加子类别(例如时装、休闲装、户外运动装),还要添加“按照季节搜索”的功能、实现关键字搜索、以及清除条目的链接。所有这些特征都可以通过类似的XML数据流来很好地支持。我们来考察一下如何重用当前的代码以便实现这些目标,以及在这个过程中将会遇到什么阻碍。

2. 可重用性问题

要重用当前的脚本有几个阻碍。首先,将SQL查询硬编码在了页面中。如果要按照类别或者关键字来搜索,就必须要修改SQL语句。随着不断加入更多的查询选项,最终的代码中会堆积大量if语句而变得丑陋不堪。

还有一个更糟糕的替代方案,那就是简单地接受CGI参数作为WHERE子句,即:

  $sql="SELECT id,title,description,price,colors,sizes"

    ."FROM garments

WHERE ".$sqlWhere;

随后直接通过URL来调用,例如:

garments.php?sqlWhere=CATEGORY="Menswear"

这种方案进一步混淆了模型和视图,将原始的SQL暴露给表现层的代码。这为恶意的SQL注入攻击敞开了大门。虽然现代版本的PHP对于这类攻击有一些内建的防范措施,但是依赖它们是很愚蠢的。

其次,将XML的数据格式硬编码在了页面中,它被埋在了一大堆的printfecho语句之中。我们有很多理由需要改变这个XML的数据格式。例如,在产品的售价旁边显示它的原价,用来说服某个可怜的傻瓜将积压的高尔夫球袜全部买走。

第三,使用数据库的结果集本身来生成XML。一开始,这似乎是一个有效率的做法,但是它有两个潜在的问题。在生成XML的时候,需要保持数据库连接一直打开。这样在while()循环中,就不能做任何非常困难的事情,否则就有可能长时间占用连接,最终可能会成为系统的一个瓶颈。除此之外,只有当将数据库看作是一个扁平的数据结构时,这样做才是合理的。

3.4.2  重构领域模型

目前在Garments表中,使用逗号分隔的列表来保存颜色和尺寸,这是一种相当低效的方式。如果想要按照良好的关系模型来规范化数据,就需要使用一个独立的表来存储所有可用的颜色,并且使用一个关联表在服装和颜色之间建立连接(用数据库的术语来说,这叫做多对多的关系)。图3-7展示了这种多对多关系的用法。

3-7  数据库模型中的多对多关系。Colors表列出了服装的所有可用的颜色,而Garments表不再包含任何颜色信息

为了确定猎鹿帽(deerstalker)有哪些颜色,需要以garment_id作为外键来查找Garments_ to_Colors表。关联的color_id字段指向Colors表的主键。我们可以看到,这种帽子有鲜粉红色(shocking pink)和蓝莓色(blueberry),却没有蓝灰色(battleship gray)。反过来运行这个查询,可以用一种给定的颜色从Garments_to_Colors表中查找所有匹配的产品。

现在我们可以更好地使用数据库了,但是用来获取所有信息的SQL变得有些复杂。如果能将服装看作是包含颜色和尺寸的数组对象,而无需手工精心构造那些联接查询,那就太好了。

1. 对象关系映射工具

幸好我们已经有了做这件事的工具和库,就是“对象关系映射”(ORM)工具。ORM工具可以自动完成数据库中的数据和内存中的对象之间的转换,为开发人员卸掉了编写原始SQL的重担。PHP程序员可以看看PEAR DB_DataObjectEasy PHP Data ObjectEZPDO)或者MetastorageJava开发者得天独厚地拥有大量的选择,Hibernate(也移植到了.NET上)是时下流行的选择。ORM工具可是一个巨大的话题,我们现在还是将它略过吧,否则说上三天三夜也说不完。

MVC的角度来考察应用,我们会发现采用ORM带来了一个令人愉快的副作用,那就是我们开始有了一个真正的模型。现在我们能够编写生成XML的程序,与Garment对象通信,而让ORM工具去跟数据库打交道,不再与特定的数据库API(以及它的怪癖)绑定在一起了。代码清单3-7展示了使用ORM工具之后代码中发生的变化。

在这里,使用PHP来为商店例子定义业务对象(即模型),使用了Pear::DB_DataObject,这要求类扩展DB_DataObject这个基类。不同的ORM工具做事的方式有所不同,但这里的要点是创建了一组对象,可以像平常的代码一样与它们通信,SQL语句的复杂性被抽象到了ORM工具之中。

代码清单3-7  服装商店的对象模型

除了主要的Garment对象,我们还定义了Color对象,以及可以返回所有可用颜色的Garment对象的方法。Size对象也可以用类似的方式来实现,限于篇幅,在这里省略了。因为这个库不能直接支持多对多的关系,我们需要为连接表定义一个对象类型,并且在getColors()方法中遍历与这张表对应的对象模型以得到可用的颜色。虽然如此,这仍然是一个相当完整和易读的对象模型。我们来看看如何在页面中使用这个模型。

2. 使用修改后的模型

我们已经从更加清晰的数据结构中生成了一个数据模型,现在需要将这个模型用在PHP脚本中。代码清单3-8是使用基于ORM的对象修改主页面后的代码。

代码清单3-8  修改后的页面,使用ORM与数据库通信

我们包含了对象模型的定义,然后按照这个对象模型来与数据库通信。我们没有专门构造一些SQL,而是新建了一个空的Garment对象,然后使用搜索条件来部分组装它[14]。因为对象模型是从一个单独的文件包括进来的,所以可以在其他的搜索中重用它。XML视图现在根据对象模型来生成。我们下一步要做的重构是将XML的格式从生成它的进程中分离出来。

3.4.3  从表现中分离内容

视图代码仍然与对象模型纠缠在一起,这是因为XML的格式和对象解析的代码绑在了一起。如果我们正在维护多个页面,希望能够只在一个地方修改XML的格式,然后应用到所有的地方。在要维护多种格式的更加复杂情况下,例如,一种格式用于为消费者提供概要和详细的列表,而另一种格式用于存货盘点的应用,我们希望每一种格式只定义一次,然后为它们提供一个集中的映射。

1. 基于模板的系统

解决这个问题的常见方法是使用模板语言。模板系统接受一个包含一些特殊标记符号的文档,这些标记符号充当占位符,在执行过程中由真正的变量值替换。PHPASPJSP本身就是这类的模板语言,它们是在Web页面中嵌入代码,而不是像Java servlet和传统的CGI脚本,在代码中嵌入内容。然而,它们将脚本语言的全部能力都公开给了页面,使得很容易将业务逻辑和表现混杂在一起。

与之相反,特定用途(purpose-built)的模板语言,例如PHP SmartyApache Velocity(一种基于Java的系统,也移植到了.NET上,称作NVelocity),为代码提供了更加有限的能力,通常是有限的控制流程,例如简单的分支(例如if)和循环(例如forwhile)结构。代码清单3-9展示了一个生成XML输出的PHP Smarty模板。

代码清单3-9  生成XML输出的PHP Smarty模板

<?xml version="1.0" encoding="UTF-8" ?>

<garments>

{section name=garment loop=$garments}

  <garment id="{$garment.id}" title="{$garment.title}">

    <description>{$garment.description}</description>

    <price>{$garment.price}</price>

{if count($garment.getColors())>0}

    <colors>

{section name=color loop=$garment.getColors()}

      <color>$color->name</color>

{/section}

    </colors>

{/if}

  </garment>

{/section}

</garments>

模板期望得到的输入是名为garments的数组,它包含了一些Garment对象。模板中的大部分内容都会原封不动地输出到结果文档中,但是花括号之中的部分会解释为指令,它们要么被看作是需要替换的变量名,要么被看作是简单的branchloop语句。与代码清单3-7中的代码相比,在这个模板中,输出XML文档的结构显然更加易读。我们来看看如何在页面中使用这个模板。

2. 使用修改后的视图

我们已经将XML格式的定义从主页面中移到了Smarty模板中。这样的话,主页面只需要设置模板引擎然后传给它合适的数据就行了。代码清单3-10展示了需要做出的修改。

代码清单3-10  使用Smarty来生成XML

<?php

header("Content-type: application/xml");

include "garment_business_objects.inc";

include "smarty.class.php";

$garment=new DataObjects_Garment;

$garment->category = $_GET["cat"];

$number_of_rows = $garment->find();

$smarty=new Smarty;

$smarty->assign('garments',$garments);

$smarty->display('garments_xml.tpl');

?>

 


Smarty使用起来非常简单,只需要遵循三个步骤就可以了。首先,创建一个Smarty引擎。然后,使用变量来组装它,在这个例子只有一个变量,但是其实可以增加任意多个变量。例如,如果用户的详细信息保存在会话中,就可以将它们传递进去,然后通过模板来显示一个个性化的问候信息。最后,调用display()方法,将模板文件的名称传递进去。

现在,从搜索结果的页面中分离视图已经达到了一种令人愉快的状态。XML格式只需要定义一次,然后通过几行代码就可以使用这个格式。搜索结果的页面现在变得非常专注,只包含特定于它自身的信息,即组装搜索参数和定义输出格式。还记得前面所构思的“在线切换XML格式”的需求吗?使用Smarty来做这件事情是很简单的,只需要再定义一个额外的格式就行了。如果我们想要追求更高程度的结构化,即在文档之中创建更小的变化,Smarty甚至还支持在模板之中包括其他的模板。

回顾一下最初对模型—视图—控制器模式的讨论,可以看到现在的实现已经相当令人满意了。图3-8形象地概括了目前所采用的方法。

3-8  MVC应用在Web应用中一般方式。Web页面或servlet扮演控制器的角色,它首先查询模型以得到相关的数据,然后将这些数据传递给模板文件(视图),视图再生成发送给用户的内容。注意,这里是只读的情况,如果要修改模型,事件的流程会略微不同,但是角色是一样的

模型是领域对象的集合,使用ORM工具自动持久化到数据库中。视图是定义XML格式的模板。控制器是“按照类别查询”的页面,以及定义的其他页面,这些页面将模型和视图粘合在了一起。

这就是MVCWeb应用中的传统使用方式。这里,我们在Ajax应用的Web服务器层使用

这种方式来为客户端提供XML文档,可以很容易地看到,它同样适用于那些提供HTML页面的传统Web应用。

依赖于所使用的技术,你可能会碰到这个模式的不同变种,但是它们的原理都是一样的。J2EE的企业beans对模型和控制器加以抽象,使得它们可以位于不同的服务器上。.NET中的“代码隐藏”(code-behind)类将控制器的角色委派给了特定于页面的对象,然而类似Struts的框架定义了一个“前端控制器”,用来拦截和分发所有到应用的请求。类似Apache Struts的框架做这些事可谓是驾轻就熟,它将控制器的职责精练为仅仅负责在页面之间对于用户加以引导,我们也可以在单个页面的级别上实现相同的功能(在Ajax应用中,可以使用JavaScript来做这件事[15])。但是在所有的情况下,映射基本上都是相同的,这就是MVCWeb应用领域中通常被理解成的样子。

使用MVC来描述Web架构是一种非常有用的方法,随着从传统的Web应用过渡到Ajax风格的应用,MVC仍然会一如既往地支持我们。但是这里并不是在Ajax中用到MVC的唯一地方。在第4章中,我们将会考察这个模式的一个变种,可以使整个应用获得结构化设计的好处。在此之前,我们考察另外一种为Ajax应用引入秩序的方法。

除了对自己的代码进行重构,我们也经常使用第三方的框架和库来使代码更加合理。随着业界对于Ajax兴趣的增长,很多有用的框架浮现了出来,我们简要地介绍其中一些较为流行的框架以此结束本章。

 

查看所有评论(0)条】

最近评论



正在载入评论列表...
热点评论