使用 Mason 构建网站

作者:Reuven M. Lerner

当我们谈论动态生成的 Web 内容时,大多数人立即想到 CGI,即“通用网关接口”。CGI 可以在 Web 服务器、语言和操作系统之间移植,但这种可移植性是以效率为代价的。例如,每次调用用 Perl 编写的 CGI 程序都会创建一个新进程,在该进程中必须编译然后解释该程序。

程序员愿意为了可移植性而牺牲速度,他们有很多选择。许多 Perl 程序员选择使用 mod_perl,它可以使用 Perl 模块修改 Apache 的行为。由 mod_perl 调用的 Perl 模块在 Apache 进程内部运行,从而消除了启动单独的 CGI 进程的开销。mod_perl 还缓存了已调用模块的编译版本,从而无需每次运行时都对其进行编译。结果是速度的显着提高,以及使用 Perl 快速轻松地修改 Apache 的灵活性。

尽管 mod_perl 功能强大,但我从未被它吸引来创建大型动态网站。诚然,速度的提高非常令人印象深刻,而且使用起来并不难。正如我们在本专栏之前的几期中看到的那样,编写 mod_perl 模块可能非常容易,并且它可以平滑地集成到更大的网站中。

当一个网站需要创建大量动态输出时,其中大部分是由非程序员编写和设计的,mod_perl 的强大功能受到限制,因为它需要使用数十个或数百个模块,每个模块仅服务于单个 URL 或目录。解决此问题的方法是将 mod_perl 与模板集成,模板将 HTML 格式的文本与 Perl 代码穿插在一起。我们在之前的几个场合中研究过模板,并看到了它们的功能和灵活性。

本月,我们将研究 Mason,这是一个由 Jonathan Swartz 编写的 mod_perl 模块,它试图解决许多这些问题。它使用模板并鼓励使用单独的“组件”,这些组件可以构建起来以创建一个大型的动态生成的站点。由于这些组件存在于单独的文件中,Mason 提供了额外的优势

  • 它缓存组件,提供的速度提升比简单的模板更大。

  • 它提供了一个完整的调试和预览环境。

  • 它生成输出文件。

我大约在两年前第一次听说 Mason,并一直告诉自己有一天我会看看它。几个月前,我终于认真地研究了 Mason,并且对我所看到的印象非常深刻——以至于我期望在未来的大部分开发工作中使用它。

公开分发的 Mason 组件数量仍然相对较少,这使得它看起来比 Zope 和商业模板解决方案的开发环境要差。但是,这种情况似乎已经在改变,并且组件的数量很可能会在未来几个月和几年内显着增加。

安装和配置 Mason

虽然 Mason 可以作为 CGI 程序工作,但它最好和最容易与 mod_perl 一起使用。(有关获取和安装 Apache 和 mod_perl 的信息,请参阅“资源”。)从 CPAN 检索最新版本的 HTML::Mason,并按照与其他任何模块相同的步骤进行操作,详细信息请参阅随附的 INSTALL 文件。

但是,使用 Mason 比简单地说“use HTML::Mason”要复杂得多。因为它通过 mod_perl 工作,而 mod_perl 是 Apache 的一部分,所以 Mason 配置必须在 Apache 启动时执行。这可以通过在 Apache 配置文件(通常称为 httpd.conf)中使用 PerlRequire 语句来最容易地完成

PerlRequire /usr/local/apache/conf/mason.pl

上面的语句告诉 Apache 在首次启动时执行 /usr/local/apache/conf/mason.pl 中的 Perl 语句。在 mason.pl 中导入的任何 Perl 模块或声明的变量都放置在所有 Apache 进程共享的内存区域中。这可能意味着大量的内存节省,因为 Perl 和 mod_perl 消耗大量内存,并且大多数 Web 服务器运行预分叉子 Apache 进程。

至少,mason.pl 必须创建三个不同的对象

  • Mason 解析器 ($parser),它将每个 Mason 组件转换为 Perl 子例程。

  • Mason 解释器 ($interp),它执行由解析器创建的子例程。在创建解释器 (HTML::Mason::Interp) 时,我们命名了解释器读取和写入信息的两个目录:Mason“组件根目录”(Mason 组件所在的位置)和 Mason“数据目录”(缓存和调试信息存储在其中)。我通常将组件根目录设置为 /usr/local/apache/mason,将数据目录设置为 /usr/local/apache/masondata。

  • Mason ApacheHandler ($ah),它处理 HTTP 请求并生成响应。

警告:由于一些尚不完全清楚的原因,Mason 无法处理符号链接。将符号链接指定为目录名称将导致神秘的“文件未找到”错误。在我的系统上,/usr/local 是 /quam/local 的符号链接;虽然 Mason 文档确实提到了这个怪癖,但它不够明确,无法为我节省超过半小时的安装时间。

列表 1

列表 1 中显示了一个最基本的 mason.pl。请注意 mason.pl 如何定义子例程 HTML::Mason::handler,该子例程对于每个传入的 HTTP 请求调用一次。通过这种方式,Mason 能够处理每个 HTTP 请求;ApacheHandler 接收请求并将其传递给解释器,然后解释器从缓存中读取已编译的组件,或者在必要时解析它。

更高级的 Mason 安装使用 mason.pl 来定义各种额外的行为。例如,Mason 附带一个预览器/调试器组件,可以跟踪组件及其子组件的执行过程。还可以为每种浏览器或请求类型定义不同的 ApacheHandler 对象。

一旦我们的 mason.pl 文件安装完成,我们必须告诉 Apache 让 HTML::Mason 处理一些传入的请求,而不是使用默认的 Apache 处理程序。这就是我们将组件根目录连接到 Apache Handler 的地方。例如,如果组件根目录是 /usr/local/apache/mason,我们可以说以下内容

Alias /mason /usr/local/apache/mason
<Location /mason>
    SetHandler perl-script
    PerlHandler HTML::Mason
</Location>

Alias 指令告诉 Apache 将每个以 /mason 开头的 URL 转换为 Mason 组件根目录 /usr/local/apache/mason。 部分告诉 Apache,每个以 /mason 开头的 URL 都应由 mod_perl 处理程序 HTML::Mason 处理。

Mason 组件

在 Mason 世界中,“组件”可以返回 HTML 或值。前者通常由 HTML 模板或模板片段组成,而后者由子例程和其他代码组成,这些子例程和其他代码由模板调用。所有组件共享相同的语法,对于任何使用过模板系统的人来说,这种语法都应该很熟悉。

Perl 代码可以放在组件内部,用 <%%> 括起来。任何返回的值都会插入到组件中,替换创建它的 Perl 代码。例如,以下组件 (output.html) 将在每次调用时显示当前时间

<HTML>
<Head><Title>Current time</Title></Head>
<Body>
<H1>Current time</H1>
<P>The current time is: <% scalar localtime %></P>
</Body>
</HTML>

我将以上内容放入 time.html 文件中,并将其放置在组件根目录中。这样做之后,我立即能够访问 URL /mason/time.html 并获取当前时间。

Mason 支持另外两种 Perl 部分,它们在不同的上下文中可能很有用。Mason 组件第一列中的 % 强制将整行解释为 Perl,而不是字面意义上的文本。这最适合用于生成文本字符串的控制结构(例如循环和 if-then 语句),如下所示

<HTML>
<Head><Title>Current time</Title></Head>
<Body>
<H1>Months</H1>
% foreach my $month (qw(Jan Feb Mar Apr May Jun
% Jul Aug Sep Oct Nov Dec))
% {
<P><% $month %></P>
% }
</Body>
</HTML>

正如您所看到的,<% %> 构造在所有上下文中都有效。此外,在一个 Perl 段的顶层声明的词法变量可以在任何其他 Perl 段中使用。

最后,长时间运行的 Perl 代码可以放在 %perl 块中。这最适合用于执行繁重的计算,而不是简单地检索变量值。例如

<HTML>
<Head><Title>Current time</Title></Head>
<Body>
<H1>Months</H1>
<%perl>
my @months = qw(January February March April May
June July August September October November December);
</%perl>
<P>The current month is <% $months[(localtime)[4]]
%>.</P>
</Body>
</HTML>

再次注意,在 <%perl> 部分中声明的词法 (my) 变量在下面的 <% %> 部分中可用。

调用组件

有经验的 Text::Template 和其他 Perl 模板模块的用户可能对此并不感到印象深刻。毕竟,有很多方法可以创建这种模板,并且许多模板可以与 mod_perl 一起使用以获得额外的速度。

但是,Mason 的模板语法包括调用其他组件的规定,就像一个子例程可能调用另一个子例程一样。(实际上,由于 Mason 解析器将每个组件转换为子例程,因此这种类比并非不正确。)在某些方面,这就像拥有一个重型的服务器端包含系统,允许您标准化页眉和页脚。但是,由于组件可以返回值以及 HTML 输出,并且由于 Mason 可以将参数传递给组件,因此事情可能会变得更加有趣。

一个组件可以使用特殊的 <& &> 语法调用另一个组件。例如,以下代码调用组件 subcomp

<& subcomp &>

subcomp 生成的任何 HTML 都放置在调用它的位置,很像服务器端包含。由 Mason 站点生成的每个 HTML 页面可以由一个、五个、十个、二十个或更多个组件组成。通过这种方式,可以从单个元素组装页面——从页眉和页脚开始,然后转到表格和下拉菜单。例如,这是一个页眉组件

<!-- begin component: header.comp -->
<Body bgcolor="#FFFFFF">
<H1>This is a header</H1>
<!-- end component: header.comp -->
这是一个页脚组件
<!-- begin component: footer.comp -->
<address>
<a href="mailto:reuven@lerner.co.il">
reuven@lerner.co.il</a>
</address>
<!-- end component: footer.comp -->
最后,这是一个顶级组件,其中的页眉和页脚来自上述组件
<HTML>
<Head><Title>Title</Title></Head>
<& header.comp &>
<P>This is the body</P>
<& footer.comp &>
</HTML>
请注意,我为页眉和页脚提供了“comp”而不是“html”的文件扩展名。这仅仅是一种约定,使我能够区分顶级组件(具有 .html 扩展名)和较低级别的片段。

另外,请注意我是如何在每个较低级别的组件的开头和结尾使用 HTML 注释来指示其开始和结束位置的。这提供了一种原始类型的调试(由 Mason 预览器/调试器组件扩展),使我可以通过查看组件的 HTML 源代码来了解发生了什么。

参数

上面的页眉和页脚组件示例适用于简单的站点。但是,如果我们的页眉和页脚组件可以接受参数,允许我们根据需要修改其内容的某些部分,那将更有用。

Mason 确实允许组件发送和接收参数,从而提供额外的灵活性。要将参数传递给调用的组件,请在组件的名称和名称、值对列表之间放置一个逗号。例如

<& header, "address" => 'president@whitehouse.gov' &>

组件可以在特殊的 <%args> 部分中接收传递的参数,传统上,<%args> 部分放置在组件文件的底部。<%args> 部分声明组件的参数,如果未将参数传递给组件,则可以使用可选的默认值。例如,以下 <%args> 部分声明了 $name$address 变量。没有默认变量的参数是必需的。$name 没有默认值,而 $address 的默认值为 reuven@lerner.co.il

<%args>
$name
$address => 'reuven@lerner.co.il'
</%args>
我们可以用这种方式重写 footer.comp
<!-- begin component: footer.comp -->
<address>
<a href="<% $address %>"><% $name ? $name : $address %></a>
</address>
<%args>
$name => ""
$address => 'reuven@lerner.co.il'
</%args>
<!-- end component: footer.comp -->
最后,我们可以重写 output.html 以发送必需的参数,而无需可选参数
<HTML>
<Head><Title>Title</Title></Head>
<& header.comp &>
<P>This is the body</P>
<& footer.comp, "name" => 'Reuven' &>
</HTML>
$m 和 $r

有经验的 mod_perl 程序员可能会喜欢 Mason 提供的组件的想法。但是,有时最容易通过深入 Apache 的内部并使用 mod_perl 请求对象(传统上称为 $r)来完成某些操作。

Mason 为每个组件提供 $r 的副本,因此我们可以使用服务器的内部结构。例如,我们可以使用 content_type 方法发送“text/html”的 HTTP Content-type

$r->content_type("text/html");

由于 <%perl> 部分在返回实际的 HTTP 标头之前被调用,因此 Mason 组件可以通过这种方式修改所有响应标头,包括使用 HTTP cookie。

一个类似的对象,称为 $m,是 Mason 特有的,它允许我们调用与 Mason 组件和开发相关的方法。例如,我们可以使用 $m->scomp 方法检索组件的内容。HTML::Mason::Devel 的手册页列出了更多可以在 $m 上调用的方法。

初始化

Mason 为我们提供了两个部分,<%init><%once>,用于在组件执行开始时运行 Perl 代码。

<%init> 部分在任何 <%perl> 部分以及组件中的任何其他 Perl 代码之前进行评估。这使组件有机会定义变量并检索有关其环境的信息。实际上,<%init><%perl> 相同,只是它可以放置在组件中的任何位置,而不是顶部。传统上,<%init> 部分与其他特殊部分一起放置在底部附近。

每次调用组件时都会评估 <%init> 部分。但是,有些项目只需要在第一次调用组件时创建,而不是每次都创建。这些项目可以放在 <%once> 部分中。在 <%once> 部分中声明的词法变量和子例程在组件的整个生命周期内都存在,这使得它们对于初始化组件的状态特别有用。但是,<%once> 部分不在 Mason 请求的上下文中进行评估,这意味着它们无法调用其他组件。

使用 Perl 的 DBI 连接到关系数据库的 Mason 组件通常使用 <%once><%init>$m 的组合来重用数据库句柄。例如,我们可以按照 Mason 文档中的建议执行以下操作

<%once>
my $dbh;    # Declare $dbh only once
</%once>
<%init>
# If this is the first time we're running,
# connect to the database
if ($m->current_comp->first_time)
{
$dbh = DBI->connect("DBI:mysql:$database:localhost",
                 $username, $password) ||
die qq{DBI error from connect: "$DBI::errstr"};
}
</init>
autohandler 和 dhandler

虽然 Mason 组件可以使用我们上面看到的 <& &> 语法创建页眉和页脚,但在我们创建的每个顶级组件中放入这些部分会变得很麻烦。因此,Mason 支持两种特殊类型的组件,一种称为 autohandler,另一种称为 dhandler

如果存在 autohandler 组件,则在目录中的每个组件之前调用它。也就是说,在检索实际请求的组件之前,先调用 autohandler,并且它可以生成自己的 HTML 输出,使用 $m->call_next。例如,以下 autohandler 将在其目录中的每个文档上放置统一的标题和页脚

<HTML>
<Head><Title>Welcome to our
site!</Title></Head>
<Body>
<% $m->call_next %>
<hr>
<address>webmaster@example.com</address>
</Body>
</HTML>

相比之下,如果组件不存在,则调用 dhandler。在某些方面,这使我们能够重写网站经常生成的“404--文件未找到”错误消息。

虽然 autohandler 通常只影响它们自己的目录,但 dhandler 会影响所有子目录。因此,/foo 中的 dhandler 将影响 /foo/bar 中的所有文档,但不影响 /bar 中的文档。但是,/foo 中的 autohandler 不会影响 /foo/bar 或 /bar 中的项目。

幻灯片放映

现在我们已经了解了 Mason 如何处理一些简单的任务,让我们看看我编写的一些用于创建幻灯片放映的组件。这种演示文稿不会像 Microsoft PowerPoint 那样具有精美的擦除效果和图形,但对于大多数技术导向的群体来说已经足够了。

幻灯片放映组件由一个 autohandler、一个 dhandler 和一个或多个用 HTML 编写的幻灯片(文本文件)组成。每个幻灯片都包含一段 HTML,它将被粘贴到 <Body> 中。例如,以下可能是一个幻灯片

<H1>Short Presentation</H1>
<P>This is my short presentation.</P>

在 autohandler 内部(列表 2;请参阅“资源”),我们有一个 <%once> 部分,其中定义了几个我们将重复使用的常量,以及 @slides,一个包含幻灯片列表的数组。例如,这是我最近一次演讲中 @slides 的值

my @slides = qw(start whoami free-software just-in-time
  databases mysql postgresql
  cgi mod_perl
  templates text::template minivend minivend-example
  mason mason-example mason-autohandler
  php jsp zope acs xml
  conclusion);
通过重新排序 @slides 中的文件名,我更改了演示文稿的顺序,通过从 @slides 中删除或添加元素,我可以更改演示文稿的长度。

autohandler 使用前面描述的 $m->scomp 来检索与幻灯片关联的 HTML。它使用它来检索它可能在幻灯片中找到的任何标题(在 <H1> 标签中),并将该标题用于 <Title> 标签中。

此外,autohandler 生成“上一张”和“下一张”幻灯片的链接。我们通过获取当前幻灯片的索引并从数组中检索名称来完成此操作

my $previous_slide = $slides[$current_slide_index - 1] || $slides[0];
my $next_slide = $slides[$current_slide_index + 1] || $slides[0];

一旦我们获得了上一张和下一张幻灯片的名称,我们就可以检索它们的标题,从而创建有吸引力的“上一张”和“下一张”链接

# Grab the headline from the next component
my $next_headline = $next_slide;
my $next_contents = ($m->scomp($next_slide));
if ($next_contents =~ m|<H1>(.+)</H1>|igs)
{
    $next_headline = $1;
}
为幻灯片使用此 autohandler 的好处之一是,它允许我通过移动文件名来重新排序或修改演讲稿。

除了 autohandler 之外,我还安装了一个 dhandler 来处理错误的文件名

<HTML>
<Head><Title>Error: No such
page</Title></Head>
<Body BGCOLOR="#FFFFFF">
<P>Sorry, but the page <i><% $r->filename() %></i> does not exist.</P>
</Body>
</HTML>
结论

Mason 提供了一个在简单易用的模板和 mod_perl 复杂而强大的底层结构之间取得良好平衡的环境。如果您曾经考虑在您的网站上使用 mod_perl,但被其复杂性吓退,请考虑研究 Mason。Mason 不仅是免费软件——由于多种原因,这是一件好事——而且它是一个经过验证的工具,与许多同类工具相比,它可以使 Web 开发变得更加容易。我希望在未来几个月内在 Mason 中进行大量开发,并希望随着我越来越喜欢这个新工具,分享我的许多经验和代码。

资源

Building Sites with Mason
Reuven M. Lerner 是一位互联网和 Web 顾问,最近与 Shira Friedman-Lerner 结婚后搬到了以色列的莫迪因。他的著作《Core Perl》将于春季由 Prentice-Hall 出版。可以通过 reuven@lerner.co.il 联系 Reuven。ATF 主页,包括存档和讨论论坛,位于 http://www.lerner.co.il/atf/
加载 Disqus 评论