使用Maypole快速部署Web应用程序

您有一个数据库。您有一个Web服务器。您有一个截止日期。

无论是为一家新企业开设电子商务店面,实现HR员工数据库的新前端,还是提供一种跟踪美式英语俚语术语引用的便捷方式,故事总是相同的——而且截止日期总是昨天。

在这个4月,我正在为Perl基金会发起一项赞助,以开发我的一个名为Maypole的项目,该项目使Perl程序员能够快速搭建数据库前端以及复杂的基于Web的应用程序。

非常快速,并且几乎不需要编写任何Perl代码。我使用Maypole搭建了一个内部网站门户、一个选择菜单和菜谱的数据库和显示系统、歌词和弦谱投影软件、开源社交网络站点,以及一个啤酒品鉴笔记的Web数据库;这仅仅是过去两周内的事情。

Maypole的灵活性源于三个基本原则

为了展示这三个原则,我们将查看一个典型的Web应用程序——在线商店的产品目录——并看看我们如何能够快速使用Maypole将其构建出来。

关注点的分离

Maypole最初被称为Apache::MVC,反映了其基于模型-视图-控制器设计模式的基础。(我首先必须更改它,因为Maypole不依赖于Apache,其次是因为Apache::MVC这个名字真的很无聊。)这与Java的Struts框架等其他语言中类似项目的基石相同。

这种设计模式主要存在于图形应用程序中;想法是您有一个表示和操作数据的模型类,一个负责向用户显示数据的视图类,以及一个根据用户触发的事件控制其他类的控制器类。这种类比与基于Web的应用程序并不完全对应,但我们可以从中提取一个重要的原则。正如Andy Wardley解释的那样

Web MVC群体真正试图实现的是关注点的清晰分离。将您的数据库代码放在一个地方,将您的应用程序代码放在另一个地方,将您的展示代码放在第三个地方。这样,您可以随意切割和更换不同的元素,希望不会影响其他部分(当然,这取决于您的关注点如何分离)。这是常识和良好实践。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,我们需要一个所谓的驱动模块。这是一个非常短的Perl模块,它定义了我们正在工作的应用程序。我说这是一个Perl模块,这可能会让你认为这是关于编写代码,但说实话,大部分实际上是伪装成配置。以下是我们的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;

这是我们的应用程序名称,这是我们告诉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::MVCDBIx::SearchBuilderHTML::Mason,但我们没有。

基于Class::DBI的Maypole类使用Class::DBI::Loader调查数据库结构,然后将product表映射到ISellIt::Product类,依此类推。你可以阅读更多关于Class::DBI的表类映射是如何工作的,在Tony关于它的文章中。

    ISellIt->config->{uri_base} = "http://localhost/isellit";

ISellIt有时需要知道它的位置,以便它可以正确地产生指向应用程序内其他页面的链接。

    ISellIt->config->{rows_per_page} = 10;

这表示我们不想在一页上显示整个产品列表;在得到列表的页面视图之前,每页最多有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;我们应该看到这个

这只用了10行代码。但这还不够,因为如果我们点击产品列表,我们会得到这样的屏幕

这是我们可能无需进一步更改即可提供给销售团队的,他们可以愉快地添加、编辑和删除产品。

同样,如果我们接着点击产品表中的一款制造商,我们会看到一个关于制造商、他们的产品等内容的实用页面。

我认为我们已经在10行代码中得到了一些收获。接下来,我们将模板交给网页设计师。Maypole在三个不同的地方寻找模板:首先,它寻找特定于一个类的模板;然后,它寻找整个应用的自定义模板;最后,它查看factory目录以使用通用的、正确的模板。

因此,为了制作更好的制造商视图,我们告诉他们把factory/view模板复制到manufacturer/view并自定义它。我们把factory/list复制到product/list并自定义它作为产品列表;我们把factory/headerfactory/footer复制到custom/目录中,并将它们变成围绕每个页面的模板,等等。

现在,我并不是很擅长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> &pound; [% product.cost %] </TD>
    </TR>
    <TR>
        <TD class="field"> Part number  </TD>
        <TD> [% product.part_number %] </TD>
    </TR>
    </TABLE>

    [% button(product, "order") %]

产生了以下截图。它可能看起来并不更好,但至少它证明事情可以被制作成不同的样子。

我们编写了一个Template Toolkit模板;被[% ... %]包围的部分是模板指令。如果你对Template Toolkit不太熟悉,Maypole手册的视图文档中有关于TT在Maypole上下文中的良好介绍。

Maypole提供了一些默认的模板宏,如maybe_link_view,它将对象链接到一个查看该对象的页面,尽管所有这些都可以被覆盖。它还传递了对象product,它知道这是我们正在谈论的对象。

实际上,这正是Maypole的真正目的:我们用将其描述为将数据库的前端添加到数据库中的方式,但本质上,它负责使用URL /product/view/210来加载ID为210product对象,在其类上调用view方法,并将其传递给view模板。同样,/product/list调用产品类的list方法,它用一页的产品填充模板。

这个模板的有趣之处在于最后一行

    [% button(product, "order") %]

这产生了一个按钮,它将向URL /product/order/210发送POST请求,它与view相同,只是这次调用的是order方法。但是Maypole还不知道如何order一个产品。这没问题,因为我们可以告诉它。

易于扩展

Maypole的第三个原则是易于扩展。也就是说,Maypole使从简单的数据库前端到完整的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的默认authenticate允许每个人访问一切,因此我们需要重写这个方法。

    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;
    }

authenticate类方法传递一个Maypole请求对象;这就像一个Apache请求对象,但处于一个非常高的层次 - 它包含有关网络请求的信息、将要用于满足请求的类、我们需要在类上调用的方法、将要处理的模板、任何对象、表单参数和查询参数等。

在此阶段,Maypole已经解析了URI的组件数据库表、操作和附加参数,因此我们首先检查操作是否是普遍允许的其中之一。

如果不是,我们从Maypole对象中提取存储在其中的Apache::Request对象,并请求远程IP地址。如果它在私有范围内,我们可以做任何事情。如果不是,我们什么也不能做。很简单。

当设计人员告诉你,他们非常希望将图片放置在产品描述旁边时,几乎可以立即上线。没问题。

有两种方法可以实现这一点;一种看起来非常简单,使用文件系统来存储图片,并在模板中放置类似以下内容

    <IMG SRC="/static/product_pictures/[% product.id %].png">

但尽管这样做查看图片非常简单,并制作出很好的原型,但上传图片并不那么简单。所以你决定将图片放在数据库中。你在产品表中添加一个“图片”二进制列,然后查阅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转换到对象,而不需要我们自己的任何代码。

当我们在下一篇文章中实现排序时,我们将添加更多类似的行为,将产品放入用户的购物车,结账,验证他的信用卡等等。但这对现在来说应该足够了:一个有模板的、可网络编辑的产品数据库,带有图片,没有压力,代码不多,并在截止日期内。嗯,几乎是这样。

总结

Maypole正在快速发展,这主要归功于Perl基金会,它使我能够在这个月专注于它;它使我能够写出成千上万字的文章、示例应用程序和与Maypole相关的代码,这有助于Maypole成为开发Web应用程序的极其有用的框架。

标签

反馈

这篇文章有问题吗?请在GitHub上打开一个问题或拉取请求来帮助我们。