个性化“新”标签
和许多人一样,我花费大量时间在网络上。其中一些时间用于工作——为各种客户编写和调试程序。我还花费相当多的时间在网上阅读,关注来自现实世界和计算机行业的最新消息,甚至探索朋友和同事建议我访问的新站点。
网站上一个常见的功能是“新”图形的泛滥,这总是让我感到恼火。我并不介意网站作者让我知道最新更改或添加的项目。相反,让我困扰的是,这些标签表明文档是否是新的,而不是文档对 我 而言是否是新的。
当我第一次访问一个网站时,所有的文档都应该有“新”的指示,因为所有文档对我来说都是新的。当我返回该网站时,只有自上次访问以来添加的文档才应该有“新”图形,可能包括自上次访问以来修改的文档。换句话说,网站应该跟踪我的使用模式,而不是强迫我记住我是否阅读过某个特定文件。
本月,我们将研究这个问题。我们不仅将了解如何创建一个不会以这种特殊方式惹恼我的网站,而且还将了解在尝试处理网站维护、最终用户服务和程序可维护性时经常出现的一些权衡。
既然我已经贬低了在网站链接上放置“新”标签的做法,那么让我来演示一下,以便我们有一个清晰的起点。这是一个简单的 HTML 页面,上面有两个链接,其中一个链接旁边有一个“新”图形
<HTML> <Head><Title>Welcome to My Site</Title></Head> <Body> <H1>Welcome to My Site</H1> <P>Read <a href="resume.html">my resume.</a></P> <P>Read <a href="deathvalley.html"><img src="new.gif"> about my recent trip to Death Valley!</a></P> </Body> </HTML>
当页面作者认为时间足够长时,“新”徽标将消失。这些标签通过修改 HTML 文件来更新,根据需要插入或删除图形。
这种技术有许多优点,主要的优点是网站运行所需的资源较少。下载文本和图形不需要像 CGI 程序那样占用服务器的处理器资源,CGI 程序需要额外的内存和处理时间。
然而,这种技术也有许多缺点。首先,标签仅在网站管理员决定修改 HTML 文件时才会更改,而不是自动更改。其次,标签未能将用户的个人历史记录考虑在内,这意味着首次用户将看到与日常访问者相同的“新”标签。
我们如何解决这个问题?让我们从一个不使用个性化的简单解决方案开始,但提供的标签比上述方法更准确。我们可以自动过期标签,在文件发布的第一周打印“新”,第二周打印“已修改”。超过两周的文件将没有标签。
最简单的方法是通过服务器端包含(SSI)。SSI 的执行方式就像 CGI 程序一样,但它们的输出被插入到 HTML 文件中。当您希望在 HTML 文件中包含动态的或以其他方式可编程的文本,但没有足够的动态输出来证明将 HTML 埋藏在 CGI 程序中是合理的时,SSI 非常有用。
在这种特殊情况下,我们可以利用 Apache 的高级服务器端包含功能,该功能允许我们执行 CGI 程序并将其输出插入到 HTML 文件中。例如,我们可以稍微修改我们的文件,如下所示
<HTML> <Head><Title>Welcome to My Site</Title></Head> <Body> <H1>Welcome to My Site</H1> <P>Read <a href="resume.html">my resume.</a></P> <P>Read <a href="deathvalley.html"> <!-#include virtual="/cgi-bin/print-label.pl?deathvalley.html" -> about my recent trip to Death Valley!</a></P> </Body> </HTML>
正如您所见,第二个链接包含一个 SSI。SSI 的一个优点是它们看起来像 HTML 注释,因此如果您不小心将启用 SSI 的文件安装在不知道如何解析它们的服务器上,则整个 SSI 将被忽略。
SSI 能够工作要归功于一些魔法:在文档返回到用户的浏览器之前,它会被服务器解释(因此称为“服务器端 包含”)。Apache 会将所有 SSI 命令替换为它们的执行结果。这可能意味着打印一些简单的内容,例如文件的修改日期,但也可能像插入通过 CGI 调用的大型数据库访问客户端的结果一样复杂。
在上面的示例中,我们运行 CGI 程序 print-label.pl,其代码在清单 1 中。虽然这个程序是通过 SSI 而不是纯 CGI 调用运行的,但它的工作方式与 CGI 程序完全相同。我们使用 CGI.pm,这是用于编写 CGI 程序的标准 Perl 模块,来检索 keywords 参数,这是描述通过问号后的 GET 方法传递的参数的另一种方式。
在检查以确保文件存在后,我们使用 -M 运算符让 Perl 告诉我们自上次修改文件以来经过的天数。如果 $ctime 小于 7,则文件在过去七天内被修改,这意味着出于我们的目的,该文件应被视为“新”。我们使用 font 标签来告诉用户该文件是新的。
如果我们对站点上的每个链接都使用 SSI,“新!”消息将显示在所有少于一周的链接旁边。
我考虑了几种处理 print-label.pl 中的错误的方法,包括使用 Perl 的 die 函数提前退出并在用户屏幕上打印错误消息。最后,我决定如果文件不存在,或者根本没有指定文件名,程序应该静默退出。您可能希望向错误日志发送消息,这可以通过从 CGI 程序打印到 STDERR 来完成,如下所示
print STDERR "No such file \"$filename\"\n";
这种安排的一个主要问题是 CGI 程序本质上是资源消耗大户。如果一个页面上有十个链接,使用这种技术需要运行十个 CGI 程序——这意味着每次我们查看此页面时都会启动十个新的 Perl 进程。现在,我们将忽略性能影响,专注于如何使事情正常工作。我将在本文末尾和下个月更深入地讨论性能。
上述技术是一个良好的开端,但它仍然忽略了用户的角度。也就是说,链接是按绝对时间尺度过期的。但是,每周访问网站少于一次的用户将看到太少的“新”标签,而每周访问网站频率高于一次的用户将看到太多的“新”标签。
我们如何处理这种情况?一种方法是跟踪用户上次访问我们网站的时间,并将比较时间戳与该时间戳而不是文件的创建或修改日期进行比较。我们如何知道用户上次访问我们网站的时间?由于 HTTP(用于传输大多数 Web 文档的协议)是“无状态的”,因此每个事务都在真空中进行。当 Web 浏览器向服务器发出请求时,该请求与任何先前或后续请求无关。没有关于先前请求的信息被传递,并且我们在请求中所做的任何事情都不会保存以供以后使用。
最好和最简单的方法是使用 HTTP cookie,几乎每个浏览器都支持它。Cookie 是由服务器设置并存储在客户端计算机上的变量。Cookie 允许我们通过在用户计算机上存储信息来跨事务跟踪状态。当服务器下次遇到用户时,它可以将 cookie 上的时间戳与文件上的时间戳进行比较。
因此,我们可以重写上面的程序,使其根据用户上次访问网站的时间自动过期标签。每次用户访问我们的网站时,我们都会设置一个 cookie。cookie 的过期日期设置为未来一周,这意味着如果 cookie 存在,则该用户在过去一周内访问过我们的网站。我们的标签程序(清单 2,print-label.pl)然后有一种简单的方法来确定是否应该在链接旁边打印“新”——只有当 cookie 不存在时才应打印标签。
清单 2. 带 Cookie 检查的 print-label.pl
因为我们正在使用 CGI.pm,它包含编写 CGI 程序的所有必要功能,所以我们可以通过以下方式检查 cookie 是否存在
my $visited_recently = $query->cookie('RecentVisitor');
然后我们可以使用以下代码打印标签
if (!$visited_recently) { print "<font color=\"red\">(New!)</font>\n"; }
关于读取 cookie 的内容就这些了。但是我们如何写入 cookie 呢?这是一个更棘手的问题,它有许多潜在的解决方案。cookie 规范要求过期日期以完整的 UNIX 样式时间和日期戳写入,如
Thu Apr 8 02:25:30 IDT 1999
我们不能简单地创建一个过期时间为“未来一周”的 cookie 并发送它。我们还必须找到一种从我们的 HTML 文件中设置 cookie 的方法——除非我们想使用 CGI 程序来发送文本,但这将破坏最初使用 SSI 的目的。
一种解决方案,虽然不可否认不是最优雅或最有效的解决方案,是利用标准 HTML 支持的 META 标签。META 标签有许多用途,其中包括发送否则将在 HTTP 标头中发送的数据的能力。
由于 HTTP cookie 作为浏览器 HTTP 请求中的标头的一部分发送,因此可以使用以下 HTML 在页面的顶部,在 <Head> 部分中设置“RecentVisitor”
<META HTTP-EQUIV="Set-Cookie" CONTENT="RecentVisitor=1;expires=Thu Apr 15 02:19:17 1999; path=/">
这告诉浏览器它应该假装从服务器发送了一个 Set-Cookie HTTP 标头,并且 content 属性应被视为标头的值。也就是说,上面的 META 标签将 RecentVisitor cookie 设置为 1,并允许 cookie 位于我的域中的任何位置。cookie 设置为在 1999 年 4 月 15 日过期。
创建此 META 标签有点困难,因为日期取决于用户加载页面的时间。如果用户在 4 月 8 日加载页面,则 cookie 应设置为在 4 月 15 日过期。如果用户在 4 月 10 日加载页面,则 cookie 应在 4 月 17 日过期。我们需要根据用户访问的时间修改输出。
cookie 的过期日期必须随时间变化这一事实意味着我们需要在某处插入一个程序。最简单的方法是使用另一个通过 SSI 调用的程序,它将为我们创建 META 标签。这样一个程序 send-cookie.pl 如清单 3 所示。安装并就位后,我们可以说
<!-#include virtual="/cgi-bin/send-cookie.pl" ->
我们的程序 send-cookie.pl 通过根据用户访问的时间创建 META 标签来设置 cookie 的值。有了这个,每次访问我们的网站都会生成一个 cookie,该 cookie 会在一周内消失(或者如果您愿意,可以“崩溃”)。我们的 SSI 检查是否发送了该 cookie,如果发送了,则打印相应的“新”标签。
上述方法有两个主要问题,一个与用户界面有关,另一个与性能有关。
让我们首先解决用户界面问题。简而言之,如果用户重新加载页面会发生什么?用户第一次查看页面时,无论 cookie 之前是否已设置,cookie 都会通过 META 标签设置。下次用户加载页面时,即使仅在几秒钟后,也不再显示“新”标签,因为 cookie 已设置,表明用户在过去一周内访问过该网站。我们需要一种更精细的方法来跟踪这些标签。
第二个问题是更严重的问题——性能下降。为了实现此解决方案,我们需要为系统上的每个文档调用至少两个 CGI 程序。考虑到即使是最无辜的 CGI 程序也可能非常消耗资源,尤其是在用 Perl 编写时,这会给 Web 服务器增加巨大的负载。再加上启动 Perl 进程和执行此类外部程序所需的时间,我们的用户也将遭受损失,除非我们在硬件上进行大量投资。
我们可以使用 Mark-Jason Dominus 编写的 Text::Template 模块来解决用户界面问题,该模块最近重新发布为 1.20 版本。与大多数模块一样,此模块可从 CPAN(请参阅资源)获得,或通过使用现代 Perl 安装附带的 CPAN 模块获得。
Text::Template 允许我们在文件中混合 Perl 和 HTML。花括号 {} 内的所有内容都被视为 Perl 程序。块评估的结果将插入到文档中以代替其代码块。因此,如果我们说
<P>This is a first paragraph.</P> <P>{ 2 + 5; }</P> <P>This is a second paragraph.</P>
最终用户将看到
This is a first paragraph. 7 This is a second paragraph.在他的或她的屏幕上。
请记住,评估块的结果不是该块的输出,而是来自块中最后一行的返回值。因此,如果我们说
<P>This is a first paragraph.</P> <P>{ print 2 + 5;}</P> <P>This is a second paragraph.</P>
我们将看到
7 This is a first paragraph. 1 This is a second paragraph.“7”来自评估“print”,而“1”是嵌入式 Perl 块的最后一行的返回值。
为了使用 Text::Template,我们需要编写一个小型的 CGI 程序,该程序调用该模块并解析指示的文件。清单 4 中显示的程序 template.pl 简单而轻松地完成了这项工作。如果我们将它安装在 CGI 目录中,那么我们可以转到 /cgi-bin/template.pl?file.tmpl,模板文件 file.tmpl 将由 template.pl 解释,然后返回给用户的浏览器。
为了处理人们指定不寻常文件名可能引起的潜在安全问题,我们删除了字符串“../”的所有出现,并确保所有文件名都以目录 /usr/local/apache/share/templates/ 开头。您可能希望在您的系统上定义不同的模板目录。
现在我们已经安装了模板系统,我们可以重写我们的模板 cookie,其中内容和“新”标签仅在必要时打印。最终结果如清单 5 所示。
我们使用以下代码创建动态 META 标签
<META HTTP-EQUIV="Set-Cookie" CONTENT="RecentVisitor=1; expires={scalar localtime(time + 604800}; path=/">
正如您所见,此 META 标签包含一个小型的 Perl 代码块,该代码块返回适当的过期日期。日期设置为未来 604,800 秒,也就是“从今天起一周后”。
我们在模板稍后的位置检索 cookie,就在决定是否打印“新”标签之前
use CGI; my $query = new CGI; my $visited_recently = $query->cookie('RecentVisitor'); $outputstring .= "<font color=\"red\">(New!)</font>\n" unless $visited_recently; $outputstring;
请注意,我们如何在模板的代码块中导入 CGI 模块。然后,我们可以创建 CGI 的实例并使用它来检索一个或多个 cookie。我们不使用 CGI.pm 向用户的浏览器打印输出,因为这将由模板系统完成。
似乎我对“新”标签的痴迷已将我们引向各种新的和有趣的方向。本月,我们研究了 cookie、服务器端包含、CGI 程序和 HTML/Perl 模板。虽然模板确实在某种程度上减少了服务器的负载,但它们仍然需要调用 CGI 程序,这本质上比提供直接 HTML 文件的成本更高。
解决此问题的一种方法是将标签制作成我们 Web 服务器的固有部分。如果服务器可以跟踪 cookie 和标签,那么一切都会正常工作。大多数人不想摆弄他们的 Web 服务器软件到那种程度;Apache 可能是允许您摆弄源代码的自由软件,但我们中很少有人那么大胆。
然而,正如我们在之前几期 ATF 中看到的那样,我们可以通过安装 mod_perl 模块,使用 Perl 而不是 C 轻松修改服务器的各个部分。虽然这样的系统仍然需要为每个检索到的文档编写一些代码,但通过 mod_perl 运行 Perl 子例程到 Apache 的开销远低于外部 CGI 程序所需的开销。
下个月,我们将研究一个 mod_perl 模块,该模块遍历页面上的链接,并为访问该网站的用户添加对每个新项目的“新”标签。当我们完成时,我们将使网络对于像我这样的书呆子以及对于不应该记住他们上次访问网站时间的用户来说变得更好、更轻松。
本文中引用的所有清单都可通过匿名下载在文件 ftp.linuxjournal.com/pub/lj/listings/issue63/3473.tgz 中获得。
