使用Maypole进行快速Web应用开发
翻 译:Joe Jiang 出 处:中国Perl协会 FPC(Foundation of Perlchina) 原 名:Rapid Web Application Deployment with Maypole 作 者:Simon Cozens 原 文:http://www.perl.com/pub/a/2004/04/15/maypole.html 发 表:April 22, 2004 Perlchina提醒您:请保护作者的著作权,维护作者劳动的结晶。 |
你有个数据库,你有个Web服务器,你有个最后期限。
无论是将一个为一个新的机遇设计e-commerce的橱窗,或者为人事部门的数据库设计一个新的界面,或甚至是为美国英语俚语用法提供一个跟踪的方法,这都是一回事,而且最后期限总是昨天。
今年4月份我在为perl基金会工作来开发一个名叫Maypole的项目,这个项目是的perl程序员可以为数据库提供web界面,也就是一个基于web的应用程序,上手和运行都很快。
快的不得了,也只需要些很少的perl代码,我就能够用maypole来建立一个内网的入口,一个数据库和显示系统用于选择菜单和菜谱,歌词琴谱投影软件,一个开源社会网络的站点,一个啤酒品味web数据库,所有这些只在两周以内完成。
Maypole的灵活性来源于三个基础:
- 清晰的概念区分
- (敏感的)人性化的默认值
- 容易扩展
清晰的概念区分
开始Maypole叫做Apache::MVC,这反应了它是基于M-V-C的模式的。(我必须改掉这个名字首先是因为Maypole不是捆绑在Apache上的,而且Apache::MVC也实在是个土名字。)也就是这个模式在很多类似的语言中被很多项目使用,例如Java的Structs框架。这个设计模式在很多图形软件中被广泛使用,这个主意的核心是用一个模板来表达和处理数据,用一个视图来负责显示数据给用户看,用一个控制类来保证用户事件正确驱动其他类的活动。这个比喻不是完全准确的映射到web应用,但是我们可以得到一个重要的概念。Andy Wardley这么解释:
M-V-C web的各种形式真正力图实现的是概念上的清晰分离。把数据库代码放在一处,应用代码在另一处,表示相关的代码在第三处。这样你可以任意的调整每个元素,不用担心影响到其他部分(当然还要取决于你如何有效的分解问题)。这通常是一个好的尝试。通过有效的分格输入和输出(控制和视图),MVC的副产品就是达到概念分离的目标。这就是Maypole所做的,他有很多的数据库驱动,很多前端驱动和很多模板表示的驱动。通常情况下,Maypole总是能够在这些领域精确的表达你的要求,你也可以专心编写应用程序的业务逻辑。这就是为什么Maypole能够让你快速开发的原因(大多数情况下你都不需要做任何开发)。
那么让我们开始吧,首先是挑选我们要用来实现产品数据库的技术元素。我们实际上会用目前来说最常用的模型,视图,控制的配置。Maypole提供了一个基于Class::DBI的模型,基于Template::Toolkit的视图和基于Apache mod_perl的控制。我们会稍后介绍这些东西的具体含义,但是现在因为这个配置是如此通用,所以他是默认的配置,也不需要为了达到这个而写任何代码。
我们还需要一个数据库。我们的假想客户名字叫做iSellIt,一个计算机设备和软件供应商。我们需要有产品表,供应商表,产品目录,次分类目录。这就是数据库看起来的样子。
CREATE TABLE product ( id int NOT NULL auto_increment primary key, category int, subcategory int, manufacturer int, part_number varchar(50), name varchar(50), cost decimal(6,2), description text ); CREATE TABLE manufacturer ( id int NOT NULL auto_increment primary key, name varchar(50), url varchar(255), notes text ); CREATE TABLE category ( id int NOT NULL auto_increment primary key, name varchar(50) ); CREATE TABLE subcategory ( id int NOT NULL auto_increment primary key, name varchar(50), category integer );我们假定数据已经被加到数据库中了,但是我们还需要销售人员可以用一个web界面来更改数据库内容。
为了使用Maypole,我们需要一个驱动模块。这是一段非常短的用来定义我们的应用程序的模块。我说模块可能会让你们误认为是和写代码想关的,但是实际上主要的是隐性的配置。这就是我们的ISellIt应用程序的驱动模块。(这个客户端可以叫做iSellIt,但是多年和perl模块打交道使我习惯于用小写字母打头命名)
package ISellIt; use base 'Apache::MVC'; use Class::DBI::Loader::Relationship; ISellIt->setup("dbi:mysql:isellit"); ISellIt->config->{uri_base} = "http://localhost/isellit"; ISellIt->config->{rows_per_page} = 10; ISellIt->config->{loader}->relationship($_) for ("a manufacturer has products", "a category has products", "a subcategory has products", "a category has subcategories"); 1;十行代码,这是你可以盼望的Maypole应用的大小。下面我们一行行地解释:
package ISellIt;这是我们的应用程序的名字,也是我们要告诉我们web服务器上的Apache使用的Perl服务的名字。
use base 'Apache::MVC';这告诉大家我们用Apache作为Maypole的前端,这样我们就可以开始写一个mod_perl的程序了。
use Class::DBI::Loader::Relationship;现在我们用一个我编写的Perl模块来把Maypole的驱动类协调在一起。这是我们可以相当直白的描述我个的数据库中表的关系。
ISellIt->setup("dbi:mysql:isellit");我们让ISellIt连接数据库而且自己分析出表和字段的情况。并且因为我们还没有改变类的默认配置,就可以肯定的说我们会使用Class::DBI和Template Toolkit。我们也可以说要是用Apache::MVC和DBIx::SearchBuilder和HTML::Mason,但是没说。
Maypole的基于Class::DBI的类使用Class::DBI::Loader来调查数据库的结构,并且把里面的产品表映射到ISellIt::Product类,其他表的也一样。你可以在Tony的文章里面读到更多关于Class::DBI的表和类的映射的信息。
ISellIt->config->{uri_base} = "http://localhost/isellit";有时ISellIt也需要知道他在那个URL地址运行的,这样可以正确的在应用内部产生链接。
ISellIt->config->{rows_per_page} = 10;这行说我们不打算在一页里显示所有的产品,而是最多每页十个。
ISellIt->config->{loader}->relationship($_) for ("a manufacturer has products", "a category has products", "a subcategory has products", "a category has subcategories");现在我们用理想的自然语言定义关系中的约束,一个供应商有多个产品,一个分类目录有多个产品,如此类推。
十行代码,这产生什么结果?
敏感的默认行为
Maypole的第二个根基是使用了敏感的默认行为。他用一个通用模板系统来很好的支持浏览和编辑库里的数据。大多数情况下,web应用都不需要改变它的默认行为。绝大多数情况下,可以稍微改掉模板的中的几个就可以。精华在于程序员可以说这是web设计组的事情然后就不用做任何工作了。好,既然我们已经把应用和默认的模板安装上去,就可以用http://localhost/isellit来登录网站。就会看到:
这区区十行代码就做到这么多,而且还有更精彩的,如果我们在产品列表上点击,就会去到这个屏幕:
现在我们已经可以把这些给销售人员使用了,他们不用做更多的工作就可以很愉快的增添,修改,删除产品了。
类似的,如果我们在产品列表中点击供应商的链接就会看到一个很乘手的关于供应商的页面。里面有他们所有的产品。还可以依此类推。
我想写这十行代码应该值了。然后我们让web设计人员来处理模板。Maypole在三个不同的地方搜索模板,首先看看有没有和类相关的模板,然后看看有没有应用相关的模板,最后就用模板工厂的默认的模板。
这样的话,我们可以让他们把factory/view模板拷贝成供应商模板而且定制修改一下。我们把factory/list拷贝到product/list定制修改一下,也把factory/header和factory/footer拷贝到custom/目录,然后把他们做成每个HTML页面的框架,如此类推。
目前我还不怎么擅长HTML设计,这也是我喜欢Maypole的原因,它让别人来帮你解决问题,但这也意味着我没法有效的展示可以用模板做到的境界。但是有个例外,我创建了一个带有下面模板的product/view。
[% INCLUDE header %]
[% PROCESS macros %]
<DIV class="nav"> You are in: [% maybe_link_view(product.category) %] >
[% maybe_link_view(product.subcategory) %] </DIV>
<h2> [% product.name %]</h2>
<DIV class="manufacturer"> By [% maybe_link_view(product.manufacturer) %]
</DIV>
<DIV class="description"> [% product.description %] </DIV>
<TABLE class="view">
<TR>
<TD class="field"> Price (ex. VAT) </TD>
<TD> £ [% product.cost %] </TD>
</TR>
<TR>
<TD class="field"> Part number </TD>
<TD> [% product.part_number %] </TD>
</TR>
</TABLE>
[% button(product, "order") %]
从下面的屏幕截图可以看到结果。他可能说不上更好,但起码证明了template可以使视觉效果有所不同。

我们已经写了一个Template Toolkit的模板,在[% ... %]之间的部分是模板的打头标致。如果你不熟悉Template Toolkit,Maypole手册的视图部分是个好的简介(在Maypole中用TT)。
Maypole支持一些默认的Template宏,比如maybe_link_view,这个宏把对象和页面链接起来,当然所有这些也都可以覆盖。这个宏还参与到对象的产生过程,也就是它知道我们正在输出的信息是普通信息还是对象(若是对象就产生链接)。
实际上,这就是Maypole的核心,我们虽然主要说的是为数据库产生web界面,但是实际上他也可以为/product/view/210这样的URL提供读出产品编号为210的服务(调用相应的显示方法并套上模板)。以此类推,/product/list调用产品类的list方法,并为所产生的一页产品列表套用模板。
这个模板有趣的地方就是最后一行:
[% button(product, "order") %]这就产生了一个链接到/product/order/210这个URL的调用,这和视图所做的类似,区别只在于调用的方法是order。Maypole虽然不知道如何订购一个产品,但是我们可以告诉它。
易于扩展
Maypole的第三个原则是易于扩展。也就是说Maypole使得为数据库提供一个羽翼丰满的web界面非常容易。但是有可能在web设计人员做好了模板以后,需求从产品数据库变成了网上商店,你又回到了最后期限的烦恼中。但是在我们开始把这个产品目录应用程序扩展以完成新的需求之前(我们会有另外一篇文章描述这个),我们看看目前为止我们已经做到了什么和需要立即增加什么。
我们有个为数据库中产品,供应商,分类和子类提供列表的方法,也可以增加,修改,删除所有的东西。还可以搜索供应商提供的产品的价格等等。阻挠我们把这个应用安装在web上或者Intranet上的东西是什么呢?
最明显的问题就是安全性了,我们可以增/删/改所有产品,可是其他的任何人也可以。我们需要允许外网的人自能看和查找,对内网受限的IP地址范围的人才提供所有的服务。(现在因为需要购物车,我们有了用户的概念,特权用户的概念也很接近了)
不幸的是,现在我们有了一个用户定制的行为,所以必须开始写代码了,好在不会很多代码。我们在驱动类里面增加几行,首先是为了设置授权用户的IP地址范围,这可以用NetAddr::IP来实现。这个类提供了判断地址是否位于某个范围的乘手的方法。
use constant INTERNAL_RANGE => "10.0.0.0/8"; use NetAddr::IP; my $range = NetAddr::IP->new(INTERNAL_RANGE);下面我们来编写客户鉴权方法,Maypole的默认鉴权方法允许每个人使用每样功能,我们需要重新定义。
use Maypole::Constants; sub authenticate { my ($self, $r) = @_; # Everyone can view, search, list return OK if $r->action =~ /^(view|search|list)$/; # Else they have to be in the internal network my $ip = NetAddr->IP->new($r->{ar}->connection->remote_ip); return OK if $ip->within($range); return DECLINED; }这个鉴权方法过滤一个Maypole请求对象,有点像Apache的request对象,但是更加高级点(包含一个web请求的类所提供的信息和可调用方法,还有待处理的模板和form参数,以及查询参数等等)。
在此Maypole已经把URI转化成行为,相关的表,和额外的参数。这样我们就可以判断这个行为是不是每个人都许可的。
若不是人人许可的行为,我们就拿出内嵌的Apache::Request对象并且看看他的远端地址。如果是在许可范围的,就允许他所有的操作,否则拒绝,就是这么简单。
就在万事具备的时候,设计人员让你在产品描述里增加一个图片信息,没问题。
有两个方法可用,看上去比较容易的方案是用文件系统来存储图片,并用模板来增加图片链接。
<IMG SRC="/static/product_pictures/[% product.id %].png">
这虽然使得看图片变得非常容易,但也有把事情弄糟的时候,因为上传图片并不容易。这就使你决定要用数据库在存储图片。在产品表里面增加一个picture的二进制列,然后你就开始看Maypole的手册。
Maypole项目是Perl基金会赞助的这件事的好处在于我可以写很好的手册,这里面有使用Maypole的每个技巧。Request这个章节有不少上传和显示图片的秘笈。
目前我们需要几个动作,一是上传,一是显示。我们下面只做这些因为你不但可以看手册,而且这些就足够你很简单的了解如何更进一步的扩展Maypole。
最好是先让大家看到我们要做的结果再反过来解释如何做。我们的/product/view_picture/210这个URL会产生一个带有image/png的页面来展示产品。这也使我们可以在模板中加入如下的:
<IMG SRC="/product/view_picture/[% product.id %]/">
这就可以实现显示产品图片的功能。实际上我们可能更倾向于这样写:
[% IF product.picture %]
<IMG SRC="/product/view_picture/[% product.id %]/">
[% ELSE %]
<IMG SRC="/static/no_picture.png">
[% END %]
这样我们就向大家解释了如何使Maypole把URL转换成对象的方法调用,接着我们就要对产品类增加view_picture方法,类的名字是ISellIt::Product,我们用下面的代码开始:
package ISellIt::Product; sub view_picture { my ($self, $r) = @_; # ... }这还有个大问题,我们不希望外网的任何人都可以调用类的任何方法,那不太好。让Maypole来拒绝这个,为了告诉Maypole我们有权远程调用这个方法,我们用属性来修饰它:
sub view_picture :Exported { my ($self, $r) = @_; }在此我们既然希望允许从外网调用view_picture,就需要用合适的数据来填充Maypole请求。
sub view_picture :Exported { my ($self, $r, $product) = @_; if ($product) { $r->{content_type} = "image/png"; $r->{content} = $product->picture; } }这是一个不太寻常的Maypole方法,因为我们跳过了类的处理和套用模板的过程,直接手工产生结果。但这也有效的显明一件事:Maypole为方法调用而维护相应的对象,帮助我们直接从URL跳到对象,不用写程序。
在下篇文章我们将实现订单,我们得为产品增加更多的行为例如加到购物车,结帐,检查信用卡有效等等。但目前这就足够了:一个基于模板的,网上编辑产品数据库,还带图片的系统。没什么压力也没几行代码,还在期限内就完成了。几乎完美了。