Mason 新闻稿
上个月,我们初步了解了 Mason,这是一个模板系统,它位于 mod_perl 之上,使我们能够创建由小型组件构建的快速执行的动态网站。
本月,我们将研究一个用 Mason 构建的简单应用程序——一个在公司网站上显示最新新闻稿的系统。当然,这样的系统可以通过多种方式进行定制,包括在线报纸或其他定期更改信息的出版物。在创建这个小型网站时,我们将看到使用 Mason 的一些步骤。
我们新闻系统的核心要素将是一个关系数据库。在这些示例中,我将使用 MySQL,尽管任何关系数据库系统都应该可以正常工作。
我在我的 MySQL 服务器上创建了一个名为 “atfnews” 的新 MySQL 数据库,并分配了权限,以便用户 atfnews 可以使用密码 “atfpass” 连接。然后,我创建了以下两个表
CREATE TABLE Categories ( category_id SMALLINT UNSIGNED AUTO_INCREMENT, category_name VARCHAR(25) NOT NULL, PRIMARY KEY(category_id), UNIQUE(category_name) ); CREATE TABLE Articles ( article_id MEDIUMINT UNSIGNED AUTO_INCREMENT, category_id SMALLINT UNSIGNED NOT NULL, posting_date TIMESTAMP NOT NULL, headline VARCHAR(30) NOT NULL, body TEXT NOT NULL, PRIMARY KEY(article_id), UNIQUE(category_id, headline) );
正如您可能从它们的名称中看出的那样,Categories 表包含类别 ID 号和名称的列表。Articles 表包含更多信息,包括文章 ID、文章应放置的类别 ID、文章发布日期和时间、文章的标题和正文。我们通过在 CREATE TABLE 语句的末尾使用 UNIQUE 子句来确保给定类别中没有两篇文章具有相同的标题。
posting_date 列利用了 MySQL 的 TIMESTAMP 数据类型。此类型自动插入给定行的最新 INSERT 或 UPDATE 的时间和日期。这样,我们可以轻松确定新闻报道何时添加到数据库中,而无需自己输入或跟踪信息。
为了使我们的新闻系统正常工作,我们将需要创建至少两组不同的组件。一组将允许用户将新闻项目输入到数据库中(即,执行 INSERT 操作),另一组将使从数据库中检索项目成为可能(即,执行 SELECT 操作)。在生产环境中,我们可能希望将发布访问权限限制为选定的用户数量。这可以通过标准的 .htaccess 文件来实现,该文件允许用户限制对单个文件或目录的访问,或者使用更复杂的系统,将用户信息存储在数据库中。
Mason 的优点之一是它对组件的使用。组件实际上是 Perl 子例程,巧妙地伪装成 HTML 文件,其中包含一些 Perl 代码。(Mason 的解析器执行将组件转换为子例程的底层魔法。)这种结构意味着重复的功能可以打包到一个组件中,然后在其他组件中调用。
例如,列表 1 包含一个名为 “database-connect.comp” 的组件。此组件返回一个值,而不是生成 HTML 输出。它的目的是连接到数据库服务器并返回数据库句柄,通常称为 $dbh。通过集中此连接代码,我们可以轻松地将我们的站点从一台服务器移动到另一台服务器,只需根据需要更改相关的 $host、$user、$password 和 $database 变量即可。
一旦配置了 database-connect.comp,我们系统上的任何组件都可以使用以下代码接收有效的数据库句柄
<%init> my $dbh = $m->comp(database-connect.comp); </%init>
上面的代码利用了 Mason 的面向对象接口,使用预定义的 $m 对象来调用另一个组件。
通过将赋值放在 <%init> 中,我们确保组件将在组件内发生任何其他事情之前连接到数据库。但是,这也意味着我们在每次调用组件时都会创建一个新的词法变量 ($dbh)。
在 <%once> 部分中执行上述赋值会稍微更优雅,创建 $dbh 一次并保留该值。但是,<%once> 部分在 Mason 组件上下文之外执行,这意味着它们无法在 $m 上调用方法。此外,<%once> 部分在创建新的 Apache 子进程之前调用,$dbh 对象可能不喜欢这样。因此,通常在 <%once> 部分中定义 $dbh,但在 <%init> 中执行赋值
<%once> my $dbh; </%once> <%init> $dbh = $m->comp(database-connect.comp); </%init>
普通的 mason.pl(或 Mason 文档中描述的 “handler.pl”)配置文件几乎足以使此系统工作。我们只需要加载 Apache::DBI,这是一个包装模块,可在 mod_perl 环境中与 DBI 一起使用,确保仅在必要时创建和删除数据库连接。
为了加载 Apache::DBI,我们需要在 mason.pl 中放置一个 use Apache::DBI 语句,该语句通过 httpd.conf 中的 PerlRequire 语句加载。为了节省一些内存,我们在 httpd.conf 中插入一行 PerlModule Apache::DBI。这确保了模块在 Apache 分裂成多个子进程之前加载到内存中。该模块可能仍然需要相当多的内存,但至少该内存将在所有 Apache 进程之间共享,而不是要求每个进程都有自己的副本。
使我们的新闻系统工作的首要步骤是允许用户创建新类别。我们简单系统中的每个新闻报道都将精确地放置在一个类别中,就像报纸上的每个报道仅在一个版块中印刷一样。
如果我们要使用 CGI 模型来创建数据库编辑器,我们将需要创建一个 HTML 表单,将其 <Form> 操作指向 CGI 程序的 URL。然后,该 CGI 程序将需要检索 HTML 表单元素,连接到数据库并执行 INSERT 操作。
使用 Mason,由于 HTML 表单元素和变量之间的关系,所有这些都变得容易得多。我们仍然需要两个不同的组件,一个呈现表单,另一个处理表单的内容。第一个组件 add-category-form.html(参见列表 2)是一个普通的 HTML 表单,带有一个文本字段和一个 “提交” 按钮。此表单与其非 Mason 对应物之间的唯一区别是 <Form> 标记中的 action 属性。在 Mason 中,即使是带有 .html 后缀的文件也是一个程序,因此可以接收来自 HTML 表单的输入。
处理输入并将新行插入 Categories 表的组件称为 add-category.html(参见列表 3)。与 Mason 组件通常的情况一样,您必须首先查看组件的最后部分(<%once>、<%init> 和 <%args>),以便了解正在发生的事情。
在 add-category.html 的情况下,我们的 <%once> 部分仅定义 $dbh,如上所述。<%init> 部分执行两个操作。首先,它基于 “database-connect.comp” 返回的值定义 $dbh。建立数据库连接后,<%init> 部分继续将用户的输入 INSERT 到数据库中。请注意我们如何使用 DBI 的占位符,此处在 VALUES 列表中显示为问号,以避免 SQL 查询中带引号的字符串的潜在问题。
占位符用 $new_category_name 的值填充,$new_category_name 是在 <%args> 中定义的标量变量
<%args> $new_category_name </%args>
通过在此处定义它,我们表明 add-category.html 在调用时必须接收 HTML 表单元素 new_category_name。我们可以为 $new_category_name 提供默认值;但是,此值对于 add-category.html 的功能至关重要,并且必须是强制性的。
根据 SQL INSERT 是否成功,标量变量 $successful_insert 设置为 true 或 false。然后,此值用于大型 if-else 语句中,以生成 HTML,该 HTML 反映列表 3 开头显示的 INSERT 的成功或失败。请注意,$DBI::errstr(标准的 DBI 错误消息)可从我们的组件中获得。
一旦我们添加了一个或多个类别,我们就可以开始将新闻项目插入到系统中。与 add-category-form.html 不同,add-news-form.html(列表 4)将需要连接到数据库,并且不能是简单的 HTML 表单。这是因为我们希望向用户呈现当前类别的 <select> 列表。为了动态创建此列表,我们将需要连接到数据库并执行简单的 SELECT 操作。除此之外,HTML 表单相对简单。我们将使用表格来组织标题和表单元素,但它由三个基本元素组成:标题、正文文本和一个类别 <select> 列表。
我决定以相对低效(但易于理解)的方式执行此操作,使用 SQL ORDER BY 子句按字母顺序检索名称。为了跟踪两个不同的值(ID 和名称),我将它们放入 @categories 数组中
while ($row_ref = $sth->fetchrow_arrayref) { my ($id, $name) = @$row_ref; push @categories, {id => $id, name => $name}; }
然后我们可以遍历 @categories,将类别 ID 作为 “value” 属性(将提交给 add-news.html 组件),但显示类别名称
<select name="category_id"> % foreach my $category (@categories) { <option value="<% $category->{id} %>"> <% $category->{name} %> % } </select>列表 5
添加新闻的组件 add-news.html(列表 5)几乎与 add-category.html 相同,只是它插入三个值而不是一个:类别 ID、标题和文章正文。如果提交成功,我们会告诉用户文章现在已放入数据库中。
虽然我们可以将新闻直接检索到顶级组件中,但我们更容易创建一个通用组件,该组件可以从任何类别中检索任意数量的文章。通过这种方式,我们可以在许多不同的高级组件中使用此 “get-news.comp” 组件,检索我们感兴趣的文章的数量和类型。
列表 6 get-news.comp 非常简单,它向调用者返回文章列表。它构建文章列表的方式与我们在 add-news-form.html 中构建类别列表的方式非常相似,检索每篇文章
while ($row_ref = $sth->fetchrow_arrayref) { my ($headline, $body, $posting_date) = @$row_ref; push @articles, {headline => $headline, body => $body, posting_date => $posting_date}; } return @articles;
我们利用 MySQL 的 LIMIT 子句将检索限制为用户有兴趣接收的文章数量。此外,我们按文章到达的相反顺序检索文章,以便时间戳最新的文章排在第一位。这确保了每当我们检索最新的五篇文章时,它们确实是最新的
my $sql = "SELECT headline, body, posting_date "; $sql .= "FROM Articles "; $sql .= "WHERE category_id = ?"; $sql .= "ORDER BY posting_date DESC "; $sql .= "LIMIT ?";
get-news.comp 将最新新闻返回到一个数组中。但是,当然,用户有兴趣阅读新闻,而不是查看 Perl 数组。因此,我们将定义另外两个顶级组件:一个选择我们要阅读的类别和文章数量,另一个显示它们。
首先,我们将创建一个组件 view-stories-form.html(列表 7),它允许我们选择一个类别和要显示的最大故事数量。此组件重复了从 Perl 数组创建 <select> 列表的范例。然后,它调用 view-stories.html(列表 8),这是一个简单的组件,除了遍历 get-news.comp 返回的故事之外什么也不做,并将它们放在格式良好的 HTML 页面中。
如您所见,创建此站点所需的工作量和代码量都相当适中。虽然这是一个相对简单的站点,但它确实有效——并且它代表了一种将 Mason 和数据库结合使用以在最短时间内创建动态站点的方法。诚然,我们最终编写了许多组件;但至少其中两个组件是可重用的,如果我们决定将来扩展站点,因此将减少我们届时必须完成的工作量和调试量。
通过更多的工作,我们可以为该站点添加个性化功能,允许用户仅阅读对他们来说是新的新闻,并且仅阅读他们感兴趣的类别中的新闻。
正如我上个月指出的那样,Mason 已逐渐成为我制作此类网站的首选工具,因为我可以快速轻松地完成这项工作。我可以将任务分离到可重用组件中的事实,以及在 mod_perl 中工作带来的高速增益,这些奖励使 Mason 成为一个极具吸引力的 Web 开发环境。

Reuven M. Lerner 是一位互联网和 Web 顾问,在 11 月与 Shira Friedman-Lerner 结婚后移居以色列莫迪因。他写了一本书,《Core Perl》,由 Prentice-Hall 出版。可以通过 reuven@lerner.co.il 联系 Reuven。ATF 主页,包括档案、源代码和讨论论坛,可以在 http://www.lerner.co.il/atf/ 找到。