OpenACS 模板

作者:Reuven M. Lerner

在过去的几个月里,我们研究了 Open Architecture Community System(开放架构社区系统),这是一个用于创建社区网站的开源工具包。上个月,我们甚至研究了如何使用 ArsDigita Package Manager (APM) 创建一个简单的应用程序包。

但从本质上讲,Web 开发就是接收 HTTP GET 和 POST 请求中的输入,并生成 HTML 页面以响应这些请求。每个 Web 开发工具包都有自己的一组模板,并且每种类型的模板都有其自身的特性和怪癖。

本月,我们将更仔细地研究 OpenACS 模板系统,它在某些方面类似于 Zope Page Templates (ZPT)。OpenACS 模板在收集和返回数据的方式上相当复杂,并且可以执行许多类型的自动错误检查,否则这些检查将是繁琐或被简单地忽略。

OpenACS 模板

旧版本的 OpenACS 使用一种称为 AOLserver Dynamic Pages (ADP) 的模板系统,类似于 JSP、ASP 或 PHP。由于在 AOLserver(为 OpenACS 提供动力的 HTTP 服务器)内部运行的多线程 Tcl 解释器,ADP 可以包含 Tcl 代码。然而,ADP 有许多问题。每个 Tcl 代码块都是独立评估的,这使得在模板内部编写条件代码变得困难。没有标准方法来确保 ADP 传递了必需的参数,以便为参数提供可选值。当然,当开发人员和设计人员需要在同一个文件上工作时,问题才真正开始出现。此外,OpenACS 的大部分代码是用简单的 .tcl 文件编写的,这对于非程序员来说可能相当令人生畏。

ArsDigita 的程序员(他们的代码在 OpenACS 项目中得以延续)决定有必要进行范式转变。页面不再使用熟悉的 .html 和 .adp 后缀调用;相反,它们将完全不带后缀地调用。

这成为可能,是因为 AOLserver 愿意按顺序搜索合适的页面。给定 URL /foo,AOLserver 将首先查找 /foo.tcl,然后查找 /foo.adp,最后查找 /foo.html。(此配置设置可以在通常位于 /usr/local/aolserver 中的 nsd.tcl 配置文件中更改。)

OpenACS 模板系统依赖于这一事实,将工作分配到两个不同的文件中。一般来说,HTTP 请求生成的输出必须经过两个不同的文件。.tcl 文件首先执行,执行数据库查询并设置变量。它的最后一行通常是 ad_return_template,这是一个 Tcl 过程,然后调用配套的 .adp 页面。ADP 可以将这些变量作为数据源检索。

由于 .tcl 和 .adp 文件应该由不同的人开发,因此很自然地会预期它们会逐渐脱节或出现兼容性问题。ArsDigita 工程师通过设置“页面契约”来避免这个问题,这意味着 .tcl 文件期望接收的 HTTP 参数列表,以及将在 .adp 页面显示中可用的 Tcl 变量(称为数据源)列表。

数据源

数据源只是 Tcl 变量的另一种名称。在 .adp 页面内部,您可以使用 @ 符号将数据源指定为 @this@。如果数据源未定义,模板将退出并显示 Tcl 堆栈跟踪,抱怨未知的变量。

数据源可以放置在文件中的任何位置。在 HTML 发送到用户浏览器之前,它们的值会替换 HTML 页面上的值,这意味着数据源不仅可以定义文本,还可以定义图像名称和样式表属性。

例如,这是一个简单的 OpenACS 模板,它在一级标题中显示用户的名字

<master>
    <h2>@first_name@</h2>
    <p>That's you, isn't it?</p>
</master>

master 标签表明,所讨论的页面不是完整的 HTML 输出,而是要插入到本地 master 中(即,名为 master.tcl/master.adp 的模板对)。本地 master 又被包装在站点的默认 master 中(通常在名为 default-master.tcl 和 default-master.adp 的模板对中,但这可以使用参数配置)。因此,生成的页面由以下部分组成

default master top
    local master top
        our page
    local master bottom
default master bottom
通过这种方式,您可以创建具有统一外观和感觉的站点,并带有通用的页眉和页脚,例如菜单栏和联系信息。master 和默认 master 的概念在许多方面类似于基于 Perl 的 HTML::Mason 模板系统中的 autohandler。

这一切都很好,但是 @first_name@ 在哪里定义的呢?它在配套的 .tcl 文件中定义。但是 .tcl 文件设置 first_name 变量是不够的。它还必须通过显式命名来将该变量标记为要导出为数据源。

.tcl 页面在 ad_page_contract 过程的参数中命名其输入和输出,该过程通常在页面顶部执行。ad_page_contract 是一种强大的机制,使我们能够创建期望接收某些输入的页面,这些页面反过来承诺生成某些输出。根据 .adp 模板页面的需要,对 ad_page_contract 的调用可以从非常简单到非常复杂不等。例如,一个 .tcl 页面,它将用户的名字设置为一个虚拟值,然后将其导出到 .adp 页面,可能看起来像

ad_page_contract {
    Comments and CVS information go here.
} {
} -properties {
    first_name:onevalue
}
set first_name "Dummy first name"
ad_return_template

对 ad_page_contract 的调用告诉模板系统,变量 first_name 将被导出。然后我们设置变量的值,并最终使用 ad_return_template 将这些值传递给模板。(由于不幸的历史原因,数据源是在 -properties 参数中传递的,而不是使用更具信息量的名称。)

我们可以通过在 ad_page_contract 中命名其他变量来将多个变量传递给模板

ad_page_contract {
    Comments and CVS information go here.
} {
} -properties {
    first_name:onevalue
    last_name:onevalue
}
set first_name "FirstName"
set last_name "LastName"
ad_return_template
列表和多行

如您所见,ad_page_contract 使将单个文本字符串传递到模板变得容易。但在许多情况下,特别是从数据库检索结果时,我们希望传递值列表。OpenACS 模板可以毫无问题地处理这个问题。例如,以下 .tcl 页面检索系统上的用户列表,并将其放置在“列表”数据源中

ad_page_contract {
    Comments and CVS information go here.
} {
    users:onelist
}

set users [db_list get_all_users {
    SELECT PE.first_names || ' ' ||
           PE.last_name as users
      FROM Parties PA, Persons PE
     WHERE PA.party_id = PE.person_id
  ORDER BY PE.last_name, PE.first_Names }]
 ad_return_template

如您所见,我们的 SQL 查询返回一个名为 users 的单列。OpenACS 数据库 API 将其转换为 Tcl 列表 users,然后我们将其导出为 users 的数据源。但是,由于我们使用 :onelist 描述符导出它,因此 .adp 页面可以遍历每个单独的元素

<master>
     <list name="users">
         <li> @users:item@
     </list>
</master>
我们的迭代器是 <list>,其内容对于列表中的每个元素执行一次。当前元素可用作 @users:item@;当前迭代的编号可用作 @users:rownum@,当前迭代可用作 @users。

如果您想遍历多个数据库行,您的 .tcl 页面可以导出 multirow。multirow 包含返回的所有行,以及列名。以下是 Tcl 方面的内容

ad_page_contract {
} {
} -properties {
    users:multirow
}
db_multirow users get_info "
    SELECT PE.first_names || ' ' ||
           PE.last_name as name,
           PA.email FROM Parties PA, Persons PE
     WHERE PA.party_id = PE.person_id
  ORDER BY PE.last_name, PE.first_names"
ad_return_template

db_multirow 过程接受三个参数:将填充(并导出为数据源)的数组的名称、查询的名称(与独立于数据库的 .xql 文件结合使用)以及在未找到 .xql 文件时使用的回退查询。在 .adp 模板中,我们可以执行以下操作

<master>
  <ul>
  <multiple name="users">
    <li>
    <a href="mailto:@users.email@"
    @users.name@</a>
   </multiple>
   </ul>
</master>
这里有两点需要注意的技巧。首先,即使数据源导出为 multirow,迭代标签也是 multiple。在错误的位置使用错误的名称可能会导致难以理解的错误。更微妙的是,<multiple> 标签内的元素选择器是句点 (@users.email@),而在 <list> 标签中是冒号 (@users:item@)。我发现自己经常在这方面犯错误,并检查以前正常工作的代码页,以确保我在正确的页面上使用正确的语法。
输入

到目前为止,我们只研究了 ad_page_contract 允许我们从 .tcl 页面导出数据到 .adp 页面的方式。但是 .tcl 页面也可以通过 GET 或 POST 请求接受输入。ad_page_contract 允许我们指定我们期望接收哪些输入,根据需要分配默认值,并检查输入是否为特定格式

ad_page_contract {
} {
    foo
} -properties {
    foo2:onevalue
}
set foo2 "$foo$foo"
ad_return_template

在上面的示例中,.tcl 页面期望接收一个名为 foo 的参数(通过 GET 或 POST)。然后,参数的值用于创建新的数据源 foo2,其中包含 foo 的加倍版本。

如果有人在没有传递 foo 参数的情况下调用上述页面,OpenACS 会自动生成一个如下所示的错误消息

We had a problem processing your entry:
* You must supply a value for foo
Please back up using your browser, correct it,
and resubmit your entry.
Thank you.

如果未传递 foo,我们可以为其提供默认值,方法是将其作为 Tcl 列表的第一个元素,并将默认值作为第二个元素

ad_page_contract {
} {
    {foo "blah"}
} -properties {
    foo2:onevalue
}
set foo2 "$foo$foo"
ad_return_template
因此,使用 foo=abc 参数调用此页面将产生“abcabc”输出,而没有参数调用它将产生“blahblah”输出。

我们可以为每个参数添加一个或多个选项,以限制我们接收的信息类型。例如,我们可以从输入参数中修剪任何前导或尾随空格,或者确保我们只接收大于零的整数

ad_page_contract {
} {
    sometext:trim
    anumber:naturalnum
}

您可以通过用逗号分隔参数来使用多个选项

ad_page_contract {
} {
    sometext:trim,nohtml
    {anumber:naturalnum 50}
}
上面的页面契约表明,anumber 必须是一个自然数,如果未指定任何内容,则默认值为 50。sometext 将被修剪前导和尾随空格,但可能不包含任何 HTML 标签。有相关的 html 和 allhtml 选项,分别允许安全的 HTML 标签和任何 HTML 标签。

页面契约甚至可以更复杂。例如,您可以使用 ad_dateentrywidget 函数创建一个日期选择小部件。因此,您可以想象一个 .tcl 页面,如下所示

ad_page_contract {
} {
} -properties {
    datewidget:onevalue
}
set datewidget [ad_dateentrywidget datewidget]
ad_return_template

随附的 .adp 页面,将显示此日期小部件,然后看起来像

<master>
    <form method="POST" action="date-2">
    @datewidget@
    <input type="submit" value="Send the
date">
    </form>
</master>
换句话说,我们的 HTML 表单会将日期小部件的内容发送到 date-2,这是一个 .tcl 页面,它将在 .adp 页面中显示其结果。date-2.tcl 可以告诉 ad_page_contract,传入的 datewidget 参数将包含一个简单的数组。但是,我们还可以将 datewidget 声明为 date 类型的参数,这将自动为我们提供四个数组元素
ad_page_contract {
} {
  datewidget:array,date
} -properties {
  date_month:onevalue
  date_day:onevalue
  date_year:onevalue
  date_full:onevalue
}
  set date_month $datewidget(month)
  set date_day $datewidget(day)
  set date_year $datewidget(year)
  set date_full $datewidget(date)
  ad_return_template
我们的 .adp 页面 date-2 现在可以以各种格式显示日期信息
<master>
    <p>Month: @date_month@</p>
    <p>Day: @date_day@</p>
    <p>Year: @date_year@</p>
    <p>Full text: @date_full@</p>
</master>
请注意,完整版本的日期小部件对于 SQL 查询是完全可以接受的。这在输入日期或在比较查询中使用它们时非常方便。
其他功能

我们只是简单介绍了 OpenACS 模板系统。ad_page_contract 还支持验证例程,允许您检查多个参数或根据数据库中找到的信息发出错误信号。您实际上可以定义自己的自定义错误消息,这些消息可能会在出现问题时显示。即使在 ad_page_contract 之外,.tcl 页面也可以调用通用的 ad_return_complaint 函数,该函数会在格式良好的 HTML 页面中生成错误消息。

此外,OpenACS 模板系统拥有一整套表单构建例程,允许程序员使用 Tcl 过程指定 HTML 表单。然后,可以使用数据源将表单的内容导出到 .adp 页面。表单构建器不仅减少了您必须编写的 HTML 量,而且还使得创建两阶段表单提交过程变得容易,用户可以在提交之前有机会预览他们的工作。

最后,OpenACS 模板包含许多附加标签,例如 <if>,这些标签允许您根据其他数据源的值有条件地包含文本和图像。

结论

虽然 OpenACS 通常因其精细的数据模型和高级应用程序而被誉为卓越的系统,但我发现模板是 OpenACS 更引人注目的部分之一。与我合作的图形设计师喜欢 .tcl 和 .adp 页面之间的分离,我喜欢我可以检查错误并传递多个值,而无需记住它们在 HTTP 级别上没有明显的连接。

虽然 OpenACS 的学习曲线可能相当陡峭,但学习模板的工作方式是开始使用此系统的一种令人满意且有趣的方式。鉴于 OpenACS 附带的许多应用程序包,在您下载系统后,代码中也有许多模板示例。

资源

电子邮件:reuven@lerner.co.il

Reuven M. Lerner 是一位专门从事 Web/数据库应用程序和开源软件的顾问。他的著作《Core Perl》于 2002 年 1 月由 Prentice Hall 出版。Reuven 与他的妻子和女儿住在以色列的莫迪因。

加载 Disqus 评论