模板:将程序与设计分离
如果您运营着一个小型网站,那么您可能需要负责所有事情——服务器管理、网站内容以及生成可变和动态内容的 CGI 程序。
如果您的网站规模较大,那么工作可能会在多人之间分配。实际上,大多数大型网站都会将其员工分为负责网站内容和设计的人员,以及负责网站基础设施和技术方面的人员。
这种分工无疑使网站管理更加容易。毕竟,找到一个撰写内容或编写 CGI 程序的人比两者都做要容易得多。此外,根据职能划分工作可以让每个人都做他们最擅长的事情。
与此同时,这种分工使得网站难以保持统一的呈现风格。CGI 程序生成 HTML,必须与网站其余部分的风格相匹配。这可能意味着在每个页面的顶部插入特定的页眉,使用特定的背景颜色或在每个页面的侧面插入图形。
换句话说,网站上的 HTML 内容有两个独立的来源。设计师创建的 HTML 页面,以及 CGI 程序生成的 HTML。如果网站不经常(或根本不)更改其风格,那么 HTML 来自两个来源的事实并不重要。设计师为网站建立一种风格,然后设计师和程序员在他们的工作中都采用这种风格。
然而,许多网站已经开始每隔几个月进行重新设计,部分原因是不断改进的技术使设计师能够在他们的网站上创造更具趣味性、更令人兴奋的体验。每次网站的设计发生变化时,所有现有内容都必须重写以适应新的设计。那些将内容分为程序员生成的 HTML 和 CGI 生成的 HTML 的网站会发现他们必须用两个不同的团队转换两种类型的文件。
例如,假设一个网站已经标准化了蓝底白字文本。每次设计师创建一个新页面时,他们都会确保包含一个如下形式的 <body> 标签
<body bgcolor="blue" fgcolor="white">
为了使网站具有统一的外观,该网站上的所有 CGI 程序都必须在其输出的顶部包含类似的 <body> 标签。这是一个基本的“hello, world”页面,演示了如何实现这一点
#!/usr/bin/perl -w use strict; use diagnostics; use CGI; # Available from https://perldotcom.perl5.cn/CPAN # Create an instance of CGI my $query = new CGI; # Send an appropriate MIME header print $query->header("text/html"); # Begin the HTML, with our colors indicated print $query->start_html( -title => "Hello, world!", -bgcolor => "blue", -fgcolor => "white"); # Send our message print "<P>Hello, world!</P>\n"; # End the HTML print $query->end_html;如果此程序在 Web 服务器中作为 CGI 程序运行,它将在我们的屏幕上生成一个简短的 HTML 页面,其中文本以白色文本显示在蓝色背景上。(是的,我们应该使用十六进制代码以确保跨平台颜色一致,但这只是为了提供一个简单的例子。)
在创建 CGI 的实例(一个可以从 CPAN 免费获取的对象模块,网址为 https://perldotcom.perl5.cn/CPAN)之后,程序发送一个 MIME 标头,指示它将向用户的浏览器发送 HTML 格式的文本。在 MIME 标头之后,它发送一个 <body> 标签,该标签在某种程度上被 start_html 方法隐藏,该方法为我们处理此类标签的生成。
最后,我们发送我们的简短消息,用 HTML 标记,并调用 end_html 方法,该方法发送一个 </body> 标签以结束 HTML 文本的主体,并发送一个 </html> 标签以指示 HTML 页面的结尾。
当设计师认为蓝底白字文本已经过时,并且他们更喜欢更现代的外观(类似于 Wired 杂志),采用绿底橙字文本时会发生什么?对于设计师来说,对网站上 HTML 文件中出现的 <body> 标签执行全局“搜索和替换”操作并不困难。但是,修改服务器上的每个 CGI 程序要棘手得多。
一种解决方案是将所有与设计相关的变量放在一个库模块中,我们可以将该模块导入到我们的程序中。这是一个名为 SiteDesign 的此类模块的示例
#!/usr/bin/perl -w package SiteDesign; $background_text = "white"; $foreground_text = "blue"; 1;
上述模块由 package 语句命名。在该语句之后,变量和函数被假定为以字符串 SiteDesign:: 开头。为了避免在将变量导入程序时出现包名称问题,我们关闭了通常有用的构造 use strict。
假设上面的代码放在名为 SiteDesign.pm 的文件中,并且该文件放在由特殊的 Perl 变量 @INC(Perl 模块所在的目录列表)命名的目录中。现在,我们的程序应该能够使用以下语句包含此库
use SiteDesign;
换句话说,我们可以将我们的“Hello, world”程序重写为
#!/usr/bin/perl -w use strict; use diagnostics; use CGI; # Available from https://perldotcom.perl5.cn/CPAN use SiteDesign; # Create an instance of CGI my $query = new CGI; # Send an appropriate MIME header print $query->header("text/html"); # Begin the HTML, with our colors indicated print $query->start_html( -title => "Hello, world!", -bgcolor => $SiteDesign::background_text, -fgcolor => $SiteDesign::foreground_text); # Send our message print "<P>Hello, world!</P>\n"; # End the HTML print $query->end_html;与我们程序的第一个版本相比,此代码无疑是一个改进,因为无需修改程序即可更改程序生成的 HTML。现有的 CGI 程序确实需要进行更改,以便它们使用 SiteDesign.pm——但是您只需要更改一次现有代码,而不是每次网站的设计发生变化时都更改。
这种方法在许多方面都很有用,但它并没有解决所有问题。虽然我们减少了网站程序员每次设计师改变主意时需要执行的工作量,但我们并没有完全消除它。设计师仍然需要在每次希望进行此类更改时与技术人员联系。
此外,我们可以通过设置变量来影响程序输出的方式数量存在实际限制。我们可以添加一个变量来指示在每个页面的顶部应显示哪个图像(如果有),另一个变量来指示是否应在页面的底部显示图像,另一个变量来指示字体大小,还有一个变量来指示是否应将第一段居中,等等,以此类推,永无止境。当然,更改这些变量仍然比更改每个 CGI 程序的输出容易,但是此解决方案对于大量变量来说无法很好地扩展。您是否愿意成为每次网站设计更改时被要求修改 30 个配置变量的程序员?
解决此问题的一种可能方案是将变量放在配置文件中,类似于我们在过去几个月中讨论过的测验文件。这样的文件,特别是如果它被由 CGI 程序和 HTML 表单组成的界面所掩盖,将允许设计师修改网站的设计,而无需打扰程序员。但是,设计师仍然必须处理大量的配置变量,并理解它们的含义。而程序员仍然必须编写代码来考虑各种样式设置的可能性。
换句话说,使用变量来指示样式设置比什么都没有要好,但远非完美的解决方案。我们想要的是一种创建 HTML 页面的方法,这些页面可以由设计师修改,并且还可以在这些 HTML 页面中执行代码。
幸运的是,我们可以使用 Mark-Jason Dominus 编写的 Text::Template Perl 模块创建这种混合的 Perl/HTML 页面。此模块可从 CPAN(https://perldotcom.perl5.cn/CPAN/)获得,它允许我们获取此类混合文件,评估 Perl 部分,保留纯 HTML 不变,并将结果发送到用户的 Web 浏览器。虽然 template 模块被标识为 beta 软件,并且不保证其稳定性,但我已经使用它一段时间了,并且没有遇到任何问题。(我希望我也可以对我使用的一些商业软件这么说。)虽然 template 模块并非专门用于 HTML 页面,但我发现在这方面它非常有用。
模板是包含零个或多个 Perl 代码片段的 HTML 页面。(因此,纯 HTML 文件也是模板,尽管此类文件不执行任何特殊操作。)Perl 代码包含在花括号内,Perl 使用花括号来标识程序中的代码块。例如,这是一个显示服务器上记录的当日时间的模板
<HTML> <Head> <Title>Welcome to our site</Title> </Head> <Body> <P>Welcome to our site!The time is now {localtime;} </>P </Body> </HTML>
乍一看,上面的模板似乎只是 HTML,仅此而已。如果您查看花括号 ({ }) 内,您会看到 Perl 代码隐藏在那里。在这个特定示例中,我们使用了 Perl 函数“localtime”,它使用标准的 Unix 格式打印时间和日期。
因为上面的文件看起来和行为都像 HTML——毕竟,除了 Perl 代码之外,它就是 HTML——我们可以将其交给我们的设计师,他们可以随意更改布局。如果他们希望在时间之前/之后插入图像,或者他们希望将当日时间居中,他们可以使用熟悉的 HTML 标签来实现。网站的程序员只需强调不要修改花括号内包含的文本的重要性,这些文本对他们来说应该是禁区。同样,网站的程序员应该只修改花括号内包含的代码,因为那是他们负责的部分。
通过使用模板,我们获得了两全其美的效果。页面可以包含程序,因此,可以根据情况修改其输出,而样式仍然由代码块周围的 HTML 决定。
诚然,编写模板需要花费一些时间才能掌握;但是,编写模板的原则很容易理解。如上所述,花括号内的任何内容都被视为 Perl 代码,并被其评估结果替换。因此,表达式
{ 2 + 2; }
返回 4,表达式
{ $browser = $ENV{"HTTP_USER_AGENT"}; $outputstring = "<P>You are using \"$browser\" as your browser.</P>\n"; }返回一个字符串,告诉用户他正在使用哪个浏览器,并用 HTML “段落”标签括起来。
也可以在一个 Perl 代码块中进行计算,并在后面的代码块中使用这些计算的结果。因此,我们可以创建以下内容
<HTML> <Head> <Title>Welcome to our site</Title> </Head> { $time = localtime; $browser = $ENV{"HTTP_USER_AGENT"}; } <Body> <P>Welcome to our site! The time is now { $time; } </P> <P>You are using {$browser;} to view our site.</P> </Body> </HTML>
在此代码中,我们使用第一个 Perl 代码块来分配模板其余部分所需的变量。这可能看起来有点牵强,但在创建大型、复杂的模板时,在第一个代码块中设置多个变量,然后在后续代码块中引用它们,可能会非常有帮助。
如果我们在编写代码块时不小心,我们可能会意外地在生成的 HTML 页面中插入一些无关的字符。在上面的示例中,第一个代码块将值分配给变量。代码块本身返回 $browser 的值,因为那是最后一个变量赋值。换句话说,我们的用户会看到两次浏览器名称——一次在第一个代码块所在的位置,第二次在我们期望看到它的位置,即在第三个 Perl 代码块中。
为了避免此类问题,我通常使用一个名为 $outputstring 的变量,该变量仅用于将输出发送到生成的 HTML 页面。在每个代码块的开头,我将 $outputstring 分配为空字符串 (""),以确保它不受先前代码块的值的污染。然后,每个代码块的最后一行设置为 $outputstring;,它评估为 $outputstring 的值并发送到用户的浏览器。在这两次使用 $outputstring 之间,我可以执行任何我想要的计算——并且任何我想发送给用户的内容都只是连接到 $outputstring 的当前值。
由于 CGI 变量实际上是环境变量,并且子进程从其父进程继承环境变量,因此我们也可以从模板中访问 CGI 变量。我们在上面的示例中已经看到了这一点,当我们检索 $ENV{"HTTP_USER_AGENT"} 时,它应该返回 Web 浏览器随文档请求一起发送到 Web 服务器的标识字符串。
因为模板内的代码是完整的 Perl,所以我们可以使用我们通常使用的所有技术和代码,包括使用数据库的库模块、代码的集中库以及几乎任何其他可用的东西。
当然,您需要确保您的代码在发布到不知情的公众之前已经调试过。创建一个模板并将其放在您网站的公共区域,结果却发现一个导致整个模板崩溃的错误,这非常令人尴尬。实际上,模板不会崩溃; template 模块足够智能,可以捕获问题并在生成的 HTML 页面上指出它们。调试模板可能很棘手,因此当您使用模板而不是直接使用 CGI 程序时,请务必分配额外的测试和调试时间。
那么,我们如何将混合的 Perl/HTML 模板转换为要发送到用户浏览器的纯 HTML 呢?如果向用户显示这些模板而没有任何形式的转换,它们将显示为 HTML 文件,Perl 代码在用户屏幕上逐字再现。这种显示显然是不受欢迎的。
关键是要有一个 CGI 程序,称为 wrapper.pl,在其查询字符串中获取模板的名称(即,CGI 程序在 URL 中问号后接收的参数)。一旦它收到模板名称,wrapper.pl 就会创建 Text::Template 的实例,并指示该模块执行将我们的模板转换为 HTML 页面所需的魔术。然后,我们可以将生成的 HTML 发送到用户的浏览器。就用户而言,该页面过去是并且现在也是 HTML;他不知道我们使用了模板来创建我们的输出。
这是一个简单的 wrapper.pl 版本
#!/usr/bin/perl - use strict; use diagnostics; use CGI; use Text::Template; # Create an instance of CGI my $query = new CGI; # Send an appropriate MIME header print $query-header("text/html"); # Get the name of the template my $file = "/home/httpd/html/templates/" . $query-param("keywords"); # Create an instance of template my $template = new Text::Template(-type => FILE, -source => $file); # Perform the evaluation, and send the results # to the user's browser print $template-fill_in;
这个程序可能看起来很简单,但我们在对 Text::Template 的调用中隐藏了大量的深度——首先,当我们打开文件时,以及当我们要求 Template 对象评估指示模板内的每个小型 Perl 程序时,它会这样做。最后,我们获取评估结果,并使用 print 语句将其发送到用户的浏览器。
假设存储模板的目录不仅使生成的 URL 更短,而且还使您的网站在某种程度上更安全,因为外部人员不会知道您的文件系统。最好也删除传递给 wrapper.pl 的文件名中对父目录(用“..”表示)的任何引用,以避免将我们的程序变成查看服务器硬盘上所有文件的便捷方式。一种简单的方法是将 $file 的原始赋值替换为以下两行
# Get the name of the template my $file = "/home/httpd/html/templates/" . $query->param("keywords"); # Remove possible security problems $file =~ s|/\.\./|/|g;
这将删除尝试上升一个或多个目录的操作,从而使某人更难以窥探我们服务器的内容。
因此,如果我们有一个名为 /home/httpd/html/templates/test.tmpl 的模板和一个名为 www.oursite.com 的网站,我们可以使用 URL http://www.oursite.com/cgi-bin/wrapper.pl?test.tmpl 以翻译后的形式查看该模板。如果我们在同一目录中还有另一个名为 foo.html 的模板,我们可以使用 URL http://www.oursite.com/cgi-bin/wrapper.pl?foo.html 查看它。
创建模板时您应该记住的一个奇怪的注意事项是,它们实际上是从您服务器上的 CGI 目录(通常称为 cgi-bin)提供的。在上述所有模板中,这没有任何区别。如果我们的模板要包含 URL 相对于(即,没有前导斜杠)而不是绝对(即,带有前导斜杠)命名的图像,则可能会导致问题。
例如,HTML 文件通常放在一个目录中,而这些文件使用的图像则放在一个子目录中,可能名为“images”。为了创建一个内部包含图像的 HTML 文件,我们可以执行以下操作
<HTML> <Head> <Title>Example of image</Title> </Head> <Body> <P>This is a sample Web page, containing an image.</P> <img src="images/graphic.gif"> </Body> </HTML>
但是,如果我们采用同一个文件并通过 wrapper.pl 馈送它,图像将不再出现。这是因为“image”子目录相对于 HTML 文件所在的目录而不是 CGI 程序所在的目录存在。
解决此问题的一种快速方法是使用 <base> HTML 标签,并使用与调用它的 URL 不同的 URL。 <base> 标签如下所示
<base href="http://www.oursite.com/text/english/">
有了这个标签,我们的浏览器将知道从 http://www.oursite.com/text/english/images 加载上面模板中的图像,无论文档是从 CGI 目录还是原始 HTML 目录加载的。这种方法的问题在于,它使将文件和目录移动到网站上的其他位置变得更加困难——这是一个通常值得权衡的取舍。
在我结束之前,请允许我发出一个警告。通常,对 CGI 目录以及其中包含的程序的访问权限仅限于一小部分可以信任的程序员,他们可以在您的系统上编写和修改代码。使用模板,该组突然扩展到包括网站的所有设计师,他们理论上可以修改模板内的代码以执行恶意行为。请记住,由于模板包含代码,因此最好限制对包含模板的目录的访问,而不是将其授予系统上的每个人。
简而言之,模板是将网站设计与其包含的 CGI 程序分离的有用方法。通过明智地使用它们,您将使每个人都更自由地做他们喜欢做的事情,以及他们最擅长的事情。
Reuven M. Lerner 是一位居住在以色列海法的互联网和 Web 顾问,自 1993 年初以来一直使用 Web。在业余时间,他烹饪、阅读并为社区的教育项目做志愿者。您可以通过 reuven@netvision.net.il 与他联系。