高级HTML::Template:过滤器

CPAN模块HTML::Template是一个非常简单但非常有用的模块,可以在编写CGI脚本时实现真正的前端与逻辑的分离。基本思想是,不是在你的代码中散布print语句(“经典”CGI方法),也不是将逻辑与HTML混合(如在JSP、ASP和Perl Mason中),你将维护两个文件。一个是包含业务逻辑的实际Perl脚本,另一个是包含仅包含表示层语句的模板文件。

模板是纯HTML,增加了一小套特殊标签。模板系统将这些标签替换为动态内容。动态内容来自你在代码中构建的Perl数据结构。

以下是一个最小模板和相应的脚本

<html>
  <head></head>
  <body>
    <h1><TMPL_VAR NAME=title></h1>

    <ul>
      <TMPL_LOOP NAME=rows>
        <li><TMPL_VAR NAME=item>
      </TMPL_LOOP>
    </ul>
  </body>
</html>

#!/usr/bin/perl -w
use HTML::Template;

# Create the template object
my $tpl = HTML::Template->new( filename => 'tmpl1.tpl' );

# Set the parameter values
$tpl->param( title => "Useful Books" );
$tpl->param( rows => [ { item => "Learning Perl" },
                       { item => "Programming Perl" },
                       { item => "Perl Cookbook" } ] );

# Print
print "Content-Type: text/html\n\n", $tpl->output;

这就是全部内容。

尽可能简单,但不简单?

HTML::Template的一个优点是它并不试图做太多。它是一个HTML模板系统。这使它保持简单,避免了可能会干扰的功能。

在这种精神下,该模块仅提供一个非常、非常有限的标签集,并且没有明显的扩展选择的方法。以下是完整的列表

  • <TMPL_VAR>
  • <TMPL_LOOP>
  • <TMPL_IF>
  • <TMPL_ELSE>
  • <TMPL_UNLESS>
  • <TMPL_INLCUDE>

所有这些标签基本上都符合你的预期:`<TMPL_VAR>`扩展为动态文本;`<TMPL_LOOP>`遍历一个数组;`<TMPL_IF>`、`<TMPL_ELSE>`和`<TMPL_UNLESS>`提供了条件显示的机制;`<TMPL_INLCUDE>`允许你包含其他文档的片段(如共享页面标题)。

大多数时候,这正是你想要的。所有这些功能都可以控制显示,但不会将通用代码与HTML模板混合的风险。

然而,有时事情可能会变得过于笨拙——特别是在信息条件显示方面。一个典型的例子是有关可选信息的。假设你想要显示图书馆书籍及其到期日期的列表,但应仅当书籍当前被借出时显示到期日期。(这是一个出人意料常见的模式。想想带有可选销售价格的物品、带有可选不良账户状态的账户、带有可选延误信息的航班时间表等。)

有时你可以简单地留出相应的变量空白

<ul>
  <TMPL_LOOP NAME=books>
    <li><TMPL_VAR NAME=title>   <TMPL_VAR NAME=duedate>
  </TMPL_LOOP>
</ul>

并使用以下代码

$tpl->param( books => [ { title => 'Learning Perl', duedate => '' },
                        { title => 'Programming Perl', duedate => '29. Feb. 2008' } ] );

这将有效。如果没有到期日期,则不会输出到期日期。然而,如果你还想有其他一些文本,除了实际的到期日期呢?这样的模板可能看起来像

<ul>
  <TMPL_LOOP NAME=books>
    <li><TMPL_VAR NAME=title>
        <TMPL_IF NAME=duedate>
          Date due: <TMPL_VAR NAME=duedate>
        </TMPL_IF>
  </TMPL_LOOP>
</ul>

这样做很多次,你将开始寻找更好的解决方案!(这里的真正问题是模板本身变得越来越难以管理,其结构也越来越难以追踪。)

一种解决方案是将附加文本放在代码中

$tpl->param( books => [ { title => 'Learning Perl', duedate => '' },
                        { title => 'Programming Perl', duedate => 'Date due: 29. Feb. 2008' } ] );

然而,这很笨拙:它打破了表示和行为之间的分离,因为现在在Perl代码中严格存在一些表示材料,如“到期日期:”,并且它使得参数不那么通用。如果你想在模板的其他地方使用纯日期,现在它将捆绑一个你可能不想要的字符串。

如果能够将每个列表条目中的整个可选部分(“封装它”)捆绑起来,并通过名称调用它,那会更好吗?换句话说,如果你有能力定义自定义标签呢?

使用过滤器启用自定义标签

这就是过滤器发挥作用的地方。过滤器是一个在标签替换之前调用模板文本的子程序。过滤器可以做任何事情。特别是,它可以以编程方式修改模板。

过滤器正是进入模板处理的钩子,它是启用自定义标签所必需的。用标准模板标签和自己的自定义标签编写你的模板。然后提供过滤器(或多个过滤器)来替换这些自定义标签为适当的组合标准标签。然后,《HTML::Template》"引擎"进行最终的替换并渲染最终输出。

对于库示例,假设你选择使用自定义标签 <CSTM_DUEDATE> 来显示可选的到期日期。这使得模板看起来非常干净简单。

<ul>
  <TMPL_LOOP NAME=books>
    <li><TMPL_VAR NAME=title>   <CSTM_DUEDATE>
  </TMPL_LOOP>
</ul>

设置模板参数值的代码没有变化,但现在你需要定义过滤器(注意需要在 </TMPL_IF> 中转义斜杠)

sub duedate_filter {
  my $text_ref = shift;
  $$text_ref =~ s/<CSTM_DUEDATE>/<TMPL_IF NAME=duedate>Date due: <TMPL_VAR NAME=duedate><\/TMPL_IF>/g;
}

最后,你需要注册这个过滤器,这样模块就会在标签替换之前调用它。过滤器是模板对象的选项,因此注册发生在你构建模板时。

$tpl = HTML::Template->new( filename => 'books.tpl',
                            filter => \&duedate_filter );

这就是基本思路。如果你想,你可以注册一系列的过滤器(每个自定义标签一个过滤器?)或者你可以将所有必需的替换放入一个单一例程中。我确实可以想象开发可重用的“标签库”作为具有过滤器定义的Perl模块。有趣的是(与JSP相比),模板文件本身不需要知道“自定义标签”的定义和行为。我认为这正是正确的:模板是普通的(“愚笨的”)文本文件,它定义了表示层。所有行为都在Perl代码中,这是它应该所在的地方。

数据敏感显示和数据过滤器

考虑另一个示例应用程序。而不是处理可选信息,这个使用的是条件格式化

假设你有一份账户列表,每个账户都有各种状态:“良好”、“不良”和“已取消”。你希望根据状态以不同颜色显示账户号:“良好”为绿色,“不良”为黄色,“已取消”为红色。

如果你试图使用 <TMPL_IF> 标签来做这件事,这将会一团糟,有两个原因:你有超过两个选择,所以我们不能使用 <TMPL_IF>...<TMPL_ELSE>...</TMPL_IF> 结构;你将不得不使用一系列单独的条件:<TMPL_IF>...</TMPL_IF><TMPL_IF>...</TMPL_IF><TMPL_IF>...</TMPL_IF>。此外,<TMPL_IF> 不允许使用任意的条件表达式(如 $status eq 'good')。它所做的只是测试一个布尔变量。经典的方法是引入三个布尔哑变量:$isGood$isBad$isCancelled,用于这些测试。肯定有更好的方法!

相反,引入一个允许配置格式的自定义标签

<CSTM_SPAN NAME=...>

这是相关的过滤器

sub span_filter {
  my $text_ref = shift;
  $$text_ref =~ s/<CSTM_SPAN\s+NAME=(.*?)\s*>
                 /<span class='<TMPL_VAR NAME=$1_class>'>
                    <TMPL_VAR NAME=$1>
                  <\/span>
                 /gx;
}

如果你在模板中使用这个自定义标签作为 <CSTM_SPAN NAME=account>,那么在过滤器应用后、模板参数替换前的中间状态下的模板将看起来像

<span class='<TMPL_VAR NAME=account_class>'>
  <TMPL_VAR NAME=account>
</span>

原始的单个自定义标签已经扩展成HTML <span> 标签,包围着实际的动态内容。这个 <span> 定义了一个CSS类来提供对动态内容的格式化。这个CSS类的 名称 也是动态的——这样你就可以根据账户状态选择 .bad.good 类。CSS类的名称来自显示的参数名称。如果这个类在样式表中或在HTML文档内部定义,那么支持CSS的客户端将按照预期将其应用于动态文本。

换句话说,看似无害的<CSTM_SPAN>标签实际上需要两个参数:一个包含要显示的文本,另一个指定要应用的CSS类。您需要做的是在Perl代码中将account_class参数设置成适当的值(当然,您还需要在样式表中定义CSS类)。

这里有保持控制的一个巧妙方法。您可以使用param()函数,要么使用单独的名称/值对(就像我之前展示的那样),要么使用哈希引用:$tpl->param( { key1 => value1, key2 => value2 } )。换句话说,您不需要在代码中多次调用param()来设置单个参数,而是可以构建一个包含所有参数的大哈希,然后在一个调用中将其传递给模板。

现在您可以对参数应用与模板本身一样有效的过滤器技巧!换句话说,在调用$tpl->param( \%parameter_hash)之前,将哈希传递给一个子程序,该子程序对参数执行数据过滤操作。在这种情况下,它会为数据结构中的每个账户添加适当的CSS类。

这种方法将所有与数据相关的显示决策集中在一个子程序中。如果您添加更多的状态代码,或者您想要更改显示类,只有一个地方需要编辑。

以下是一个演示这些概念的Perl程序

#!/usr/bin/perl -w
use HTML::Template;

# Create the template object
my $tpl = HTML::Template->new( filename => 'tmpl4.tpl',
                               filter => \&span_filter );

# Build a data structure containing all the parameters
my %params = ( title => "Accounts",
               accounts => [ { account => 'Good' }, { account => 'Bad' } ] );

apply_display_logic( \%params );

# Set all parameters for the template at once
$tpl->param( \%params );

# Print
print "Content-Type: text/html\n\n", $tpl->output;

# ---

sub span_filter {
  my $text_ref = shift;
  $$text_ref =~ s/<CSTM_SPAN\s+NAME=(.*?)\s*>
                 /<span class='<TMPL_VAR NAME=$1_class>'>
                    <TMPL_VAR NAME=$1>
                  <\/span>
                 /gx;
}

sub apply_display_logic {
  my $hash_ref = shift;

  my %account_classes = ( Good => 'good', Bad => 'bad' );

  foreach my $acc ( @{ $hash_ref->{ accounts } } ) {
    $acc->{account_class} = $account_classes{ $acc->{account} };
  }
}

#%@$! 发生

当我第一次向同事解释这个想法时,他的反应是:“很好。你走这条路会变成JSP。这是你想要的吗?”我的回答是:“嗯,肥料发生了。”

GUI开发总是很痛苦。总是。您无法避免GUI开发中与展示和逻辑分离的所有问题。在我看来,因此挑战不是找到理想的Web开发框架,而是找到适合当前任务的最优框架。

HTML::Template非常适合相对简单、麻烦最小的直接网站(这类网站的数量比任何人愿意承认的都要多)。它特别适合报告和统计数据。在这个应用领域,能够定义自定义标签以处理格式化和特殊展示问题是一个明显的优势。

结论

在本系列的第二部分,我将进一步探讨这些想法,并探索如何使用HTML::Template在Web/CGI环境中创建类似GUI的小部件

标签

反馈

这篇文章有什么问题吗?请通过在GitHub上打开一个issue或pull request来帮助我们。