2009年4月12日星期日

继承 Web 站点,第 2 部分: 优化您的 Web 站点

Brett McLaughlin (brett@newInstance.com), 作者兼编辑, O'Reilly Media

2008 年 6 月 12 日

让 Web 站点易于维护(请参阅 第 1 部分)之后,速度、可访问性和组织成为关键问题。学习如何分析站点的页面并改进其效率和布局。

假设您有一个 Web 站点,而且它仍然可以使用。您使用过此站点,它通过了验证,并且您确定它是易于维护的(有关如何实现这些目标,请参阅本系列的 前一篇文章)。即使它是可维护的,您的站点也可能运行和加载的速度很慢,它可能涉及文件系统上的所有文件 —— 或者,通常更糟的是,服务器端将所有的 HTML、CSS、JavaScript 都包含在了单个文件中 —— 而且几乎无法导航。那么,您如何接手一个可维护的 Web 站点并将其变成对于 Web 漫游者和 Web 设计者来说都很轻便、顺畅的目的地呢?

正如您可以使用标准工具和技术来让您的站点变得可维护(验证、分离 CSS 和 HTML,组织样式表)一样,您也可以执行一些常规任务来进一步改进站点的组织和使用。其中一些任务将提高设计者和程序员维护站点的能力;一些任务将让站点变得可以让您更快地访问;一些将让站点变得让人更容易而且更乐于使用它。这一切都很重要,而且有必要将它们摆到正确的位置。

再次讨论维护和组织


在上一篇文章中,您花费了大量时间来验证站点的 HTML 和 CSS。此外,您应该已经将页面的样式组件从 HTML 本身分离出来。现在,应该重审这些决定了,对页面的特定部分进行进一步分离,并确保 JavaScript 像 CSS 一样干净地从页面中分离出来。

HTML 和 CSS 不混合

无需将这一主题讲透(实在太多了),值得再提一次的是您需要 将 CSS 从主要 Web 页面中提取出来。CSS 应该是在外部样式表中,通常命名为类似
[page-name].css 或 [category/group-name].css 的名称。因此,您可能具有一个名为 homepage.html 的页面,而其 CSS 样式表名为

homepage.css,或者甚至是 default.css。您还会看到许多名称类似 mobile.css 或 print.css 的 CSS 文件,旨在分别格式化呈现给移动设备或打印机的文档。这些名称不仅仅是为了方便;它们使得查明站点上正在发生的事情变得非常直观,而这意味着搜索硬盘驱动器将花费更少的时间 —— 从而发生更少的失败。您和您的合作者快速无阻地工作时获得的效率提升实际上是不可测量的,但任何有经验的开发人员或管理者都会告诉您这十分显著。

曾经有一种争论认为,浏览器查找和加载附加 CSS 文件(以及 Web 页面 HTML 文件)所花费的时间是为了避免外部 CSS。甚至把我们活在一个 DSL、电缆调制解调器、T1 甚至 T3 世界的现实抛到一边,这种争论实在太愚蠢了(这已经够仁慈了)。除二进制算术之外,几乎就没有比计算机查找文件(假设文件位于相同的物理驱动器上)并在文件正好是文本的时候将其加载为纯文本更快 的操作了。事实上,浏览器将 Web 页面转换为 DOM 树(类似于树的节点组织)并可视化地呈现这些节点所花费的时间要比加载外部文本文件所花费的时间多得多。所以使用外部 CSS 吧,让自己轻松一些。


将 JavaScript 移动到外部文件中


不是针对 JavaScript

如果选择 VBScript 或 ASP.NET,或 Microsoft® 的 Ajax 风格,则在此讨论的与 JavaScript 有关的所有原则仍然适用。只需在内心中将选择的客户端技术 “JavaScript” 替换掉就行了。


将 HTML 与 CSS 分离的所有原因都是通用的;换句话说,并非因为 CSS 和 HTML 不能在一起,而是因为一般的最佳实践就是将结构(HTML)和内容(HTML 中的文本或数据)分开表示。这在松散意义上称为关注点分离。在编程领域,您甚至将看到一种类似的技术,称为单一职责原则,即将一个软件工件(类、模块等)仅做一件事情视为好的形式。将此原则应用到 Web 空间:HTML 文件代表数据;CSS 样式表代表特定结构化数据集的样式。因为这是两个不同的任务,所以它们归入两个不同的文件。


对于 Web 页面中的逻辑(通常用 JavaScript 表示),这同样适用。正如您通常会在 HTML 文档的头部(head 元素中)内看到 CSS 嵌套在

style 标记中一样,您还会在 HTML 文档的头部看到
JavaScript 嵌套在 script 标记中,如下:

<![CDATA[
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"><head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<script language="JavaScript" type="text/javascript">
/* Create a new XMLHttpRequest object to talk to the Web server */
var request = false;
/*@cc_on @*/
/*@if (@_jscript_version >= 5)
try {
request = new ActiveXObject("Msxml2.XMLHTTP");
} catch (e) {
try {
request = new ActiveXObject("Microsoft.XMLHTTP");
} catch (e2) {
request = false;
}
}
@end @*/

if (!request && typeof XMLHttpRequest != 'undefined') {
request = new XMLHttpRequest();
}

function getImageDetails() {
var url = "lookupImage.php?image=" + escape(getImageName(this.src));
request.open("GET", url, true);
request.onreadystatechange = showImageDetails;
request.send(null);
}

function showImageDetails() {
if (request.readyState == 4) {
if (request.status == 200) {
var response = request.responseText;
var splitResponse = response.split('|');
var title = splitResponse[1];
var date = splitResponse[2];
var description = splitResponse[3];

var titleElement = document.getElementById("info-title");
var dateElement = document.getElementById("info-date");
var descriptionElement = document.getElementById("info-text");

replaceText(titleElement, title);
replaceText(dateElement, date);
replaceText(descriptionElement, description);
}
}
}


<!-- and so on... there's a LOT more JavaScript here -->
</script><title>Hoverbox Image Gallery</title>

<style type="text/css">
*
{
border: 0;
margin: 0;
padding: 0;
}

/* =Basic HTML, Non-essential
----------------------------------------------------------------------*/

a
{
text-decoration: none;
}

body
{
background: #fff;
color: #777;
padding: 50px;
}

#page {
position: relative;
}

#images {
float: left;
width: 400px;
}

#details {
color: #000;
}

h1
{
background: inherit;
border-bottom: 1px dashed #ccc;
color: #933;
font: 32px Georgia, serif;
font-weight: bold;
margin: 0 0 20px;
padding: 0 0 15px;
text-align: center;
}
<!-- and so on... there's even more CSS that would follow -->
</style></head>
]]>



当然,这是两个 领域的最糟的部分:HTML 和 CSS 位于头部。但所有这些可以提取出来并放置在外部文件中;假设它们称为 hoverbox.css 和
hoverbox-ajax.js。那么,上面糟糕的代码就可以更改为:

<!CDATA[

<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"><head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />]]><b>
<script language="JavaScript" src="hoverbox-ajax.js"> </script></b>
<!CDATA[<title>Hoverbox Image Gallery</title>]]><b>

<link rel="stylesheet" href='css/hoverbox.css'
type="text/css" media="screen, projection" />
<!--[if IE]><link rel="stylesheet" href='css/ie_fixes.css' type="text/css" media="screen,
projection" /></b><!CDATA[<!endif]--></head>
]]>




注意:在这种情况下,实际上有两个 CSS 样式表:
hoverbox.css(对于所有浏览器),和 ie_fixes.css 中特定于 Internet Explorer 的 CSS 的修正集合。有关此特定示例代码的详细信息,请参阅本文结尾的 参考资料 部分。


一切都非常明显:第二个示例(其中 CSS 和 JavaScript 位于单独的文件中)令人难以置信地容易处理和管理。进一步,可维护性和可用性对于设计人员和开发人员都显著增加了。JavaScript 程序员和脚本设计人员在一个文件中工作,而不会弄乱核心 HTML。CSS 设计人员可以在 CSS 中进行修补,甚至为 Internet Explorer 或其他浏览器添加附加修复,而不必更改文档的结构。负责更改 HTML 的设计人员可以执行此任务,而无需担心滚动上百行脚本和样式,或更改某些东西,从而不仅弄乱页面的结构,还弄乱其操作和显示。

如果已经安排了 QA(质量保证)和测试过程,那么此处的价值就会成指数级增长。在这些情况下(至少作为一般规则),已更改的任何文件都必须重新测试。在此设置(关注点分离)中,可以更改 JavaScript 并进行测试,而无需 重新测试样式和结构。这是巨大的增值,尤其是在考虑用于测试 CSS 和 HTML 的工具和技术与用于测试交互的 JavaScript 驱动的 Web 页面的工具和技术完全不同时。

使用编程方式附加事件处理程序


JavaScript 在页面中的显示方式与 CSS 在页面中的显示方式之间有一个重要的区别。使用 CSS 时,规则应用于元素,所以将所有 CSS 放置到外部文件中相当容易。但是,使用 JavaScript 时,事情就不总是那么明朗;JavaScript 通常不应用于元素,而是应用于元素上 特定的事件。所以您将看到如下的 HTML:

<!CDATA[
<li><a href="#"><img src="img/photo01.jpg" alt="description" ]]>
<b>onmouseover="javascript:getImageDetails();"</b><!CDATA[ />

<img src="img/photo01.jpg" alt="description" class="preview" /></a></li>
<li><a href="#"><img src="img/photo02.jpg" alt="description" ]]>
<b>onmouseover="javascript:getImageDetails();"</b><!CDATA[ />
<img src="img/photo02.jpg" alt="description" class="preview" /></a></li>

<li><a href="#"><img src="img/photo03.jpg" alt="description" ]]>
<b>onmouseover="javascript:getImageDetails();"</b><!CDATA[ />
<img src="img/photo03.jpg" alt="description" class="preview" /></a></li>
<li><a href="#"><img src="img/photo04.jpg" alt="description" ]]>

<b>onmouseover="javascript:getImageDetails();"</b><!CDATA[ />
<img src="img/photo04.jpg" alt="description" class="preview" /></a></li>
<li><a href="#"><img src="img/photo05.jpg" alt="description" ]]>
<b>onmouseover="javascript:getImageDetails();"</b><!CDATA[ />

<img src="img/photo05.jpg" alt="description" class="preview" /></a></li>]]>


这可以一直延续。在这个例子中,图库中有 20 个图像,每个都有 onmouseover 事件处理程序。乍看起来,这似乎是不可避免的 — 是将 JavaScript 附加到事件中必不可少的一部分。而且,如果您已经获得需要调用单个 JavaScript 函数的单个事件,这可能行得通(例如,单击特定的图像将调用 JavaScript 函数 returnToHomePage() 等)。但是,在类似上述情形(大量图像或对象都附加到同一个 JavaScript 函数中)的情况下,您最终将真正弄乱您的 HTML 页面。


但是,避免这种情况并不是那么困难,您只需要再多使用一些
JavaScript。当然,这意味着您可以将额外的 JavaScript 放在外部文件中,并忘记它 —— 还是关注点分离。您真正需要知道的是如何使要附加事件处理程序的元素变得惟一。例如,在上述代码中,假设我想要进行更改,并将所有这些鼠标事件处理程序附加到 “preview” CSS 类的图像中,并将事件从 onmouseover 更改为 onClick(这实际上是本文最后对此代码执行的操作)。在这种情况下,我需要如下的 JavaScript 函数:

function addImageHandlers() {
var imagesDiv = document.getElementById("images");
var imageElements = imagesDiv.getElementsByTagName("img");
for (var i=0; i<imageElements.length; i++) {
var class = imageElements[i].className;
if (class == 'preview') {
imageElements[i].onclick = getImageDetails;
}
}
}



其中大部分详细信息是特定于页面的,而且本文的重点不在于介绍如何使用编程方式分配事件处理程序。但是在 nutshell 中,这抓取了特定 div 中的所有 img 元素,命名为 “images”,然后在 “preview” CSS 类中查找其中所有的图像。最后,我使用 onclick 处理程序并将其分配给我想在此事件上运行的方法:getImageDetails()。结果是我可以在 img 标记中挖掘所有这些事件处理程序。


惟一的缺点是您需要在页面的 body 的 onLoad 事件上运行此方法。但是,您已经将所有这些图像处理程序逐个抽取出来了。而且,最重要(但不是最明显)的是,您可以在 JavaScript 文件中更改分配了事件的元素,而不用 使用 HTML。假设您想要再次使用 onmouseover 事件或不同的元素;只需更改 addImageHandlers() 函数,您将永远不会打开 HTML。这是一个优点。


事实上,您可能想要一开始就让页面始终运行在实用程序函数中,比如 initPage()。然后,您可以将所需的一切放入此 JavaScript 函数中,添加、删除或更改事件处理程序,而且永远不用打开 HTML。关注点的进一步分离,对页面的未更改组件的测试更少,对 Web 页面的更新更容易。





回页首


服务器端包含:决定性因素


假设您从 HTML 提取出 CSS,将 JavaScript 放入外部文件中,您甚至设法将所有的事件处理程序任务在 onLoad 事件中合并为页面的 body 标记上的单个方法。您已经完成了页面的编程部分,对吗?不幸的是,尚未完成。您仍然要担心一件事:

服务器端包含(通常称为 SSI 或 SSIs)。它们通常如下所示:

<!--#config timefmt="%A %B %d, %Y" -->Today is <!--#echo var="DATE_LOCAL" -->


这将以指定格式打印出当天的日期。


服务器端包含在目前似乎是一次性事物;您可以大量使用,也可能完全不用。换句话说,您将发现到处存在这些包含的页面(用于时间戳、上次修改日期、计数器、页眉、页脚等等……好了,这个列表太长了)或者您在页面中根本 找不到服务器包含。事实上,您将很少发现一个站点使用 1 到 20 个 SSI;大多数站点或者完全避免使用 SSI(一个都不会用),或者疯狂使用 SSI。

SSI 是坏还是邪恶?

SSI 根本没有错。大多数程序员目前偏爱较新的脚本语言,比如 PHP、JSP 或 ASP(取决于所选的供应商或技术),但 SSI 通常是一种无需太多工作就可以在 Web 页面中添加少许功能的简单方法。

但是,SSI 在被过度使用时将会变成一个问题。例如,使用标准页脚时,可能具有一个颜色栏,一组联系链接和一个版权信息。可以将此页脚包括在站点的每个页面底部,如下所示:


<!--#include virtual="/footer.html" -->


但这相当方便,所以您也可以构造一个通用的页眉,并一开始就将其包括在文档中。导航通常也是通用的,所以 SSI 也可能适用。(虽然导航将基于当前页面有稍微的改变,但是带有导航链接的包含页面也可以使用一些 SSI 来实现一些显示逻辑。)而且您可能想列出最新的几篇博客,或者最近更新的几个页面,或者可能只运行另一个脚本来加载 cookie 并提供个性化问候。SSI 均适用。
of this.

问题在于每个步骤都会引发开销。您跟踪越来越多的小文件(这还没什么)但还要构建一组未归档的假设。每个页面必须将页眉和页脚包含在特定目录中(或对 SSI 包含进行更改以指向包含文件所在的正确目录)。它很快就变成了一个重要角色。而且,可能最重要的是,您将 SSI 作为脚本语言对待以构建动态页面。这不是 SSI 最擅长的;相反,SSI 擅长一些非常细微的功能。


缩减 SSI


如果试图让站点变得更好、(更)可维护、对于设计人员和开发人员更友好,则可以考虑尽可能合理地提取出尽可能多的 SSI。如果需要在当前日期中使用一个,那就没什么。如果您的页面仅仅是尖括号注释(<!-- 和
-->)之间的 SSI 包含和逻辑的组合,则可以考虑简化您的站点,或转向使用功能完全的 Web 脚本语言,比如 JSP 或 PHP。

此外,如果您已经 在使用 PHP 或 ASP.NET 或 JSP,那么您应该将页面中所有的 SSI 都提取出来。一旦您已经投入(在时间和专业技术上)到一个脚本语言中,那么就坚持下来。您将具有相同数量的开销,但您只需使用较少的技术。HTML、CSS、JavaScript 和您的脚本语言已经远远足够了,无需将 SSI 添加到此混合物中。






回页首


组织站点的文件

如果已经跟上来,尤其是您已经具有一个在页面中组合了 HTML、CSS 和 JavaScript 的站点,您就已经解决了许多问题 —— 并潜在地创造了一些新问题。一旦开始将结构(HTML)从显示(CSS)、操作和行为(JavaScript)分离出来,您就具有许多散播在各处的单独文件(HTML、CSS 和 JavaScript)。

当然,还有大多数 Web 站点具有的正常文件都包含:10 个、100 个甚至 500 个不同页面的 HTML 页面,潜在的页眉和页脚导航支持文件,图像,文本文件,更多图像(相信您已经明白了)。而且,将一个文件向下加载三个目录和向上加载一个目录不会耗费太多时间,查找 此文件却需要很多时间。再一次重申,组织是关键,如果您想让站点长期保留(也就是说,在漫长的周末睡上两次,并忘记一切),花一些时间对其进行组织是十分关键的。


按种类和类型全局组织文件

最常用的布局选项(除了仅仅将一切统统塞进一个目录之外)是从站点的根目录开始,按类型组织所有文件。例如,您已经具有一个 images/ 目录,一个 css/ 目录,可能还有一个 js/ 或 scripts/ 目录,然后所有的 HTML 文件都在根目录下。因此,您的所有文档都具有类似 images/header.gif 和

css/default.css 的路径。

这是重要的第一步,因为您立即知道图像在哪里 —— 在自己的目录中。而且您的 CSS 也在自己的目录中。脚本呢?完全一样。对于您或新的设计人员和程序员来说,组织事情十分容易,而且完全是自文档化的。无需解释或冗长的文档就能够弄明白一切。


如果您按照十分常见的实践将 HTML 文件存储在子目录中,则还有另一个小小的优点。假设您已经使用 blogs/ 目录存放所有的 HTML 博客帖子,使用 music/ 目录存放对 CD 和音乐会的所有评论。如果将所有的图像存放在站点根目录下的 images/ 目录中,则在 img 标记中不必使用相对路径 src="local-image.jpg",而是使用 src="/images/image-name.jpg"。


更重要的是,在您开始包含库模板中的页眉和页脚时,您已经在 blogs/ 目录中获得了一个文件来包含站点根目录(或者,甚至更好的是
templates/ 目录)中的页脚,您必须导航令人混淆的路径结构。所有路径都从站点根目录(/)开始并前进。这避免了跟踪您所在的目录,而且甚至更好的是,避免了类似 src="../../images/logo.png" 的冗长路径。


但是,此方法的缺点在于您的所有图像都位于一个位置;您的所有脚本位于一个位置;您的所有 CSS 位于一个位置。是的,这也是优点,但单个位置本质上意味着您可能具有名称冲突。如果您将
full-moon.jpg 存储在 /images/ 中以配合您令人深思的有关纽约的午夜博客帖子,然后在六个月之后,您在一个兄弟晚会上捕捉到您小舅子令人尴尬的一瞬,于是将
full-moon.jpg 存储在 /images/ 中,那时会发生什么?好了,就这样说吧,如果有人看到您写的有关曼哈顿美妙一夜的博客帖子时,他将会惊异于所看到的画面。


这是名称冲突 的典型情况:两个文件在同一目录中尝试共享一个名称。如果按种类组织一切,您将必须切记不要用具有相同名称的新文件覆盖旧文件。否则,您将发生冲突问题,没人想这样。

按站点分组的嵌套组织

第二种方法比全局分类稍微好一些,是利用分类的总体概念并将其递归应用。假设您的站点具有四个部分:产品、文档、支持和 FAQ。您已经在服务器的根目录下创建了四个目录,可能是 products/、docs/、support/ 和

faq/。然后,您要将分类方法应用于其中每个 目录。

因此在 products/ 中,您将具有 images/ 目录、css/ 目录和 scripts/ 目录等等。产品的所有 CSS、JavaScript、图像和资源将位于此子目录中。本质上,您已经创建了产品的迷你站点。然后,同样的规则适用于其他三个目录。每个目录都将具有图像和脚本等等,所有内容都存储在此子站点的目录中。


在组织上,这比较容易处理。如果您在操作产品页面,则所有资源都在此目录中。而且可能更重要的是,其他页面的所有资源都不 在产品迷你站点中。所以您只需处理少数文件,其中所有文件都与您操作的内容相关。

最后,这使得名称冲突更容易避免。您将处理更小的文件集合,使用相同名称的几率将减少。这并不意味着它们不会发生,但因为文件比较少,所以甚至可以在替换或覆盖具有相同名称的文件之前手动进行检查。

当然,这种方法的缺点在于您很容易重复一些资源。如果所有的 “迷你站点” 上的所有页面都使用相同的页眉、页脚或徽标怎么办?突然,迷你站点崩溃了,或者至少变得复杂了。当然,答案并不深奥;相当的显而易见。


使用全局和区段分组之间的混合

实践证明,为站点组合一个合理的物理布局,您实际上需要两个级别的分类。首先,您需要弄清楚站点中的一切全局 内容:跨越两个或多个迷你站点。所以如果您已经具有整个站点的页脚,或一个通用的徽标,或所有页面使用的 CSS 样式表,这些都是全局资源。它们不应该嵌套在子目录中,因为您的所有页面都需要它们。

然后,一旦您弄清楚全局资源之后,就将其放置在 Web 服务器根目录下的分类子目录中。所以,您已经将全局样式表放置在 /css/ 中,并将所有页面需要的脚本放置在

/scripts/ 中。然后,应用如前所述的迷你方法:分离到子目录中,并将只有特定迷你站点使用的资源放到相关子目录中(仍然是 images/ 或
scripts/ 或任何适当的位置)。

如果采用此方法(而且它是 Web 专家最常使用的方法),您实际获得了两个世界的最高境界。您通过隔离迷你站点减少了一个位置的文件数,并合理地避免了名称冲突。但您还降低了资源的重复,所以共享文件的迷你站点只使用此文件一次。


但是,您必须做出一个选择:是否、何时引用全局文件,使用 “../” 表示法(比如
../images/logo.jpg),或绝对路径表示法(比如
/images/logo.jpg)。您最好避免所有的 ../ 类型(这将迅速转化为 ../../ 甚至 ../../other-section/images)。如果您已经使用了此混合法,那么您将在类似 images/blog-header.gif 的路径中获得特定于本地迷你站点的图像。这十分便于查看和欣赏,而且仍然尽量保持自文档化。而且,结果里外都是易于理解的 HTML 页面。这对于您、您的团队成员以及使用您页面的任何人来说都是一件好事。


避免大桶方法


不管使用什么方法,实际上只有一个方法是明显错误的:大桶方法,即将一切抛到一个目录中。您通常可以通过 public_html 目录标识这些设置,此目录通常由启动站点或 GUI 工具使用。不管是启动站点还是 GUI 工具都需要您这样组织内容(也就是说,不 组织内容),对于一些人来说,通常更简单的是将一切都保留为 GUI 编辑器或 FTP 工具所布置的那样。


此方法最明显的问题是,与页面 数相比,系统中的文件数通常按指数级增长。所以一个页面通常引用两个或三个其他页面,五个或十个图像,可能一个 CSS 样式表和一些 JavaScript —— 甚至是类似通用样式表或 JavaScript 文件的共享资源,事情迅速变得不可控制,而且,事实上,您将费大力气去逐个组织您的 HTML 页面,那么为什么不将相同的原则应用于站点的物理布局呢?


在按类别全局分组一切那一节中也有相同的问题:名称冲突。您将图像放入具有数百个其他图像(更不用说数百个所有 类型的文件)的目录中,您将不得不认真思虑一番了。当然,您可以只执行一个简单的 ls 或

dir,但大多数不花时间组织其站点物理布局的人也不会花时间来采用这样的预防步骤。结果呢?覆盖的文件、图像(或
CSS 或 JavaScript)被神秘地 “更改”,页面乱七八糟地分散在站点中与您预期完全不同的部分中。





回页首


流线化 CSS

CSS 是一个可以事半功倍或事倍功半的地方之一。通过在站点或者至少是页面范围应用几个规则,就可以获得巨大的效果,但是通过应用许多特定的规则,您可以获得难以置信的详细的、极具魅力的页面。问题在于使用所有这些全局规则、伪选择器,以及仅应用于标有 “i-hardly-ever-appear” 类的段落元素中的图像的样式,您就可能开始让 CSS 膨胀起来。当您具有从未使用过的样式表规则(糟糕但不可怕)或互相冲突或覆盖的规则时,确实会让人心烦,而且是一个潜在的大问题,CSS 膨胀就会发生。要获得组织良好且可维护的站点,您的工作就是摆脱这两个问题。

挖掘未使用的规则


您应该已经通过 CSS 验证器(请参阅 参考资料 部分中 HTML
和 CSS 验证器的链接,以及本系列的上一篇文章,其中详细介绍了 CSS 验证器的使用)运行了您的页面。这将暴露其中大多数问题,即使执行的操作并不是那么清晰。首先,如果验证页面并发现几个 CSS 规则没有使用,不要立即断定您可以删除这些规则! 当您构建由多个页面甚至是整个页面使用的样式表时,您将使规则仅应用于站点页面的一个子集。您可以争论说,只有应用于站点每个页面的规则属于站点范围的 CSS,但这种逻辑只是说起来比做起来容易的典范。

接下来要进行一些必需而且乏味的工作。您将必须至少运行站点页面的大量取样来查明是否有一些规则没有在整个站点中使用。至少尝试 10 个页面,对于每个页面,尝试并记录下没有使用的规则(未使用的规则通常比使用的规则要少得多)。然后,一旦获得一个好的样本之后,就会看到哪些规则没有显示在任何页面上。尝试在样式表中将其注释掉,看看发生了什么。当然,随着 CSS 规则慢慢稳定,站点具有了 100 个或更多的页面,真正能将此测试出来的惟一方法是尝试站点上的每个页面,并看看有什么影响美观的地方没有。

不幸的是,必须指出,有时候这一优点不值得付出努力。您的页面将不会更快地加载,但是 CSS 将比较干净,而且是站点上真正发生的操作的更好的表示。在多数情况下,最好的解决方案是尝试并删除任何明显未使用的规则。但是如果在整个站点上使用了 10 个 CSS 样式表,或者单个样式表具有数百个规则,那么您可能会忙一整天。


忽略重叠规则


尝试根除互相重叠和覆盖的规则甚至是一项更加劳动密集的任务。它只关乎构建应用于页面的层次结构。在纸上,在头脑中,或者使用工具。(如果您找到了,就告诉我们!)例如,如果已经获得应用于整个页面的任何全局规则,则这位于层次结构的顶部。然后是应用于页面主体标记或可能一个 p 标记的任何规则。然后是应用于特定 p 标记或
div 的规则,等等。您最终将得到一个级联 —— 浏览器应用的相同种类的级联。顶部的规则,然后是其下的规则,都应用到页面和样式表上。更具体的规则覆盖更一般的规则。所以,应用于具有 “acoustic” 类的 “guitars” div 中链接的规则将替换并覆盖为整个页面设置的标记。


此处的问题在于构建和分析此层次结构都是非常乏味的。您已经看到应用于整个页面的规则,然后确保应用到元素上的其他任何规则都不会受最顶层规则的影响。换句话说,如果没有任何元素是由高层规则设置样式,因为更具体的规则始终有效,那么您应该摆脱那个未使用的规则。

例如,假设具有如下 CSS 规则:

body {
background: #fff;
color: #777;
padding: 50px;
}



然后,假设您的站点策略(已文档化并向整个团队解释了,对吗?如果您是一位独立开发人员,您是否已经将它们应用于显示?)指明所有页面在下列四个 div 元素之一中维护其内容:一个 div 用于页眉,一个用于页脚,一个用于导航,一个用于内容。每个 div 具有一个 ID 来标识它所提供的功能。所以您可能具有一些附加规则,如下:


body {
background: #fff;
color: #777;
padding: 50px;
}

<b>div#header {
background: inherit;
border-bottom: 1px dashed #ccc;
color: #000;
padding: 50px;
}

div#nav {
color: #888;
padding: 50px;
}

div#footer {
clear: both;
font: 10px Verdana, sans-serif;
padding: 10px 0;
text-align: center;
color: #000
}

div#content {
background: inherit;
color: #777;
}
</b>


此处的问题在于如果您将所有页面放在这四个
div 的其中一个中,那么在不覆盖的情况下,body 选择器中的 color: #777; 规则永远不会得到应用。每个 div 都具有自己的 color 来覆盖 body 选择器。但您如何发现这些东西呢?通过大量的试验、错误,有时候还需要一点点运气。所以您将必须自己决定是否值得去查找并删除这些实例。


但是,这里也有一些真正的好处:通过删除规则可以应用到的最具体的元素中尽可能多的规则,意味着您的整个页面具有比较少的规则。这同样意味着,如果您添加新内容,那么您不必疑惑它为什么显示为宽字体的粉色文本。可能不会有大量规则应用于其上,因为它没有使用 ID 或 CSS 类进行标记,或插入到样式元素中。越简单越好,意味着您可以设计而非删除应用于新内容的其他规则。





回页首


动态页面是否真正动态?

到目前为止,我们已经讨论了页面中所有必要的组件。如果您的页面具有 SSI,那么您或者确保它们是稀疏的,或者可能将其转换为其他脚本语言 —— 但针对 SSI 是否提供了有用的用途没有任何真正的分析。这同样适用于 CSS、JavaScript、所有图像等等。如果它存在于您的页面上,那么它必须就在它应该的位置上,对吗?

是否真正需要时间戳?


在大多数情况下,处理任何 Web 页面时,最消耗时间的部分是处理此页面中的动态信息。是否预加载图像翻转或初始化 XmlHttpRequest 对象以用于稍后的 Ajax 请求,或分配事件处理程序,您的代码将花时间处理这些问题。浏览器在呈现 HTML 方面十分卓越,当前甚至在加载图像方面也相当不错,但任何一种服务器处理都要花费时间。每个服务器端包含都必须处理,JSP(JavaServer 页面)标记中的每个 JavaBean、VBScript 中的每个宏都必须处理和执行。这都要花时间,而且通常意味着您的页面将不会快速呈现在用户浏览器中。


这就是您为什么必须摆脱您的程序员或脚本设计人员身份,用户的角度来考虑。您页面中一切需要花时间去加载的组件(尤其是在页面加载时加载的服务器端包含和 JavaScript,通常位于 body 标记的 onload 事件上)是否让您的页面变得更好?甚至更重要的是,是否改进了用户体验?因为如果它仅仅是一个装饰,或者是向别人展示的 “实在很酷” 的项目,那么就对了。


时间戳、日期戳、最后修改的页面、页面最后修改日期和时间 —— 所有这一切都可能是 Web 页面中可以删除的不重要的组件。如果是这样 —— 如果您可以说您的用户实际上不需要这些东西 —— 那么您删除它们也可以很好地服务您的用户和您的页面。您的页面加载时间应该会缩短,您已经确保了页面只包含它们所需的内容。

还要避免热心过度

在您大刀阔斧削减用户页面的所有功能之前,必须提一句警告。是的,您将降低页面的加载时间,是的,您可以删除页面中无关紧要的脚本和交互部分,但有时候您需要一些 交互性。而且,有时候,时间戳或上次查看的时间确实 有用。


许多时间,开发人员发现他们可以通过删除所有小的 SSI 或 onclick 和 onmouseover
事件极大地简化其页面。但是,事情还可能变得无法掌控。进行足够的删除之后,您的页面可能最终不仅仅是修剪,而是平凡单调。记住,要考虑(和回答)的问题是,“这是否改进了用户体验?”尽管答案是 “否” 的时候,删除一些东西是重要而且有用的,但在答案是 “是” 的时候,保留一些东西也同样重要。





回页首


结束语


在 Web 上的大多数时候确实是这样,简单性通常是大多数疾病的解药。如果您的站点庞大而且笨拙,您可能需要将其物理布局重新组织为比较简单的迷你站点。如果您的页面混乱得难以编辑、更新和维护,您可能需要分解您的 CSS 和 JavaScript。如果 SSI 遍布在页面的各个角落,最简单的方法通常是去掉 SSI。如果您的页面像岩石般坚固,从而似乎要花一段时间去加载,那么考虑切掉一些所谓的交互性,即那些看起来养眼或比较酷但实际上不会改进用户体验的交互性。

但是,在所有这些情况下,在执行任何操作之前,必须首先考虑它是否影响了用户的体验,然后考虑是否影响了您的工作环境。如果某件东西让您站点上的维护和工作变得容易了,但让您的页面看起来十分笨拙,或者使页面变得无法导航,那么您必须处理这项工作,从而让您的页面易于使用。但是如果您改进了用户体验,或者甚至就保持原样,但可以改进查找文件、或更改行为、或排列页面中的混乱链接的容易程度,那么您就为自己做了一件好事。

最后,目标在于查找问题、隔离问题并将其修复。然后,您的工作还没有完成;您必须查看所做的更改是改进了站点,还是没有作用,还是使站点在旁观者角度上变得难以使用。仅当所做的更改在用户角度上看起来仍然效果良好时,您才能将其应用。而且,进行小小的更改,您就会改进您的 Web 站点。而且,当您更改站点的外观或功能时,您必须坚持 这种方法。如果在您操作新内容时,您或您的开发人员或设计人员抛弃了所有这些原则,那么您的站点将继续在可维护和大麻烦之间摇摆 —— 而且您将度过许多个头疼的夜晚。



参考资料

学习

获得产品和技术

  • Head First
    HTML with CSS & XHTML
    (Elizabeth 和 Eric Freeman,O'Reilly Media,
    Inc.):了解有关标准化 HTML 和 XHTML、以及如何将 CSS 应用于 HTML 的更多信息。




  • JavaScript:
    The Definitive Guide
    (David Flanagan,O'Reilly Media, Inc.):包含有关使用 JavaScript、动态 Web 页面的详细说明,而且未来的版本将添加两章有关 Ajax 的内容。


  • 下载 IBM
    产品评估版
    ,并使用来自 DB2®、Lotus®、Rational®、Tivoli® 和 WebSphere® 中的应用程序开发工具和中间件产品。



讨论

2009年4月10日星期五

前端页面优化-HTTP/1.1协议中缓存处理和web服务器实现(二)

标签响应报头值,一个实体标签,提供了一个"模糊"缓存验证器.这将允许在不便存 储修改信息的情况下进行可靠的确认,包括HTTP 日期数据不充足和源服务器不想因使用修改日期而带来麻烦的情况.

实体标签描述见3.11 节,有关其报头的描述 见14.19,14.24,14.26,14.44.


13.3.3 强,弱验证器

由于源服务器和缓存器会比较两个验证器来确定他们是否代 表相同的条目,所以通常

希望实体发生任何变化,验证器也相应变化,这样的验证器为强验证器.

同时还会有这样的情况,服务器倾向于仅在发生重要的语义变化时才改变验证器。在资

源变化时验证器未必变化的称为弱验证器.


ETag通常情况下是强验证,Last-Modified通常情况下是弱验证,但通过相应设置,其验证 强度属性将会改变。


Etag
由于是强验证,其优先权高于Last-Modified。


当两类验证器同时存在时,必须两类条件都为真,否则仍然会从源服务器获取数据。


基于以上所述:缓存优先级或者前后顺序为:Max-Age > Expries > Etag > Last-Modified

这个优先级顺序是用户代理与源服务器处理缓存一个依据。值得注意的是ETag,虽然其为强验证, 但由于性能和同步问题,经常舍去不用,下面会详细讲到。



以上都是基于理论介绍,接下来简单的介绍下上文中涉及的各个报头,及其在服务器中的设置,以及出 现的问题及解决方案。


HTTP协议中各报头信息

Expries

Cache-Control


详见参考文档7


Last-Modified(详见参考 文档7)


文件最后修改时间。一般情况下可以理解为源服务器上文件的Last-Modified属性值。具 体实现和Web服务器以及资源(不仅仅是文件)属性有关。


Last-Modified
由服务器端生成,客户端通过If-Modified-Since或者说If-Unmodified-Since这个条件判断请求 来验证资源是否修改。我们常见的是使用If-Modified-Since


ETag


实体值,可用于比较来自同一资源的不同实体。可以简单理解为资源的版本信息。



Etag
主要为了解决Last-Modified无法解决的一些问题(详见参考文档7,11):

1、一些文件也许会周期性的更改,但是他的内容并不改变(仅仅 改变的修改时间),这个时候我们并不希望客户端认为这个文件被修改了,而重新GET;

2、某些文件修改非常频繁,比如在秒以下的时间内进行修改, (比方说1s内修改了N次),If-Modified-Since能检查到的粒度是s级的,这种修改无法判断(或者说UNIX记录MTIME只能精确到 秒)

3、某些服务器不能精确的得到文件的最后修改时间;


为此,HTTP/1.1引入了Etag(Entity Tags).Etag仅仅是一个和文件相关的标记,可以是一个版本标记,比如说v1.0.0或者说"2e681a-6-5d044840"这么一串看起来 很神秘的编码。
Etag 由服务器端生成,客户端通过If-Match或者说If-None-Match这个条件判断请求来验证资源是否修改。我们常见的是使用If-None- Match。HTTP/1.1虽然引入了ETag,但是并没有说明其具体生成规则,因此需要各服务器自己实现。但是不管怎样的算法,在服务器端都要进行计 算,都要进行比较,计算和比较就有开销,会带来性能损失。因此为了榨干这一点点性能,不少网站完全把Etag禁用了(比如YUI)。与此对应的是 HTTP/1.1鼓励服务器尽可能的开启Etag^_^


你如何选择要根据自己的实际情况。如果你选择了使用,那么恭喜你,还有下面一个问题需要解决。

问题:


当使用群集、负载均衡时,由于各文件最后更新时间,Web服务器设置等方面的差异,会造成相同实 体的ETag不同。并且由于HTTP协议规定,如果Etag和Last-Modified同时存在,则这两个条件必须同时满足才能返回304错误 ,否则会向源服务器发送获取实体的请求。


MSDN
上说IIS5存在此问题,但在IIS6和后续版本中已经解决。(你最好尝试一下)

解决方法:

1. 使用一种机制使多 台服务器上的相同实体的Etag相同

2. 直接去掉Etag



IIS相关实现及设置


Expries

Cache-Control

有两种方法设置:

1. 使用IIS

IISà属性àHTTP头à自定义 HTTP头à添加

Cache-Control:max-age:3600

Expires:Mon, 09 Jul 2018 05:53:19 GMT


2. 使用 adsutil.vbs(详见参考文档2)

到IIS的AdminScripts目录下去找到adsutil.vbs文件。

cd C:\Inetpub\AdminScripts

比如我们要给根目录下的imags目录添加Expires/Cache-Control,首先要 在metabase中给它加一个节点cscript adsutil.vbs create W3SVC/1/root/images "IisWebVirtualDir"

设置images目录下的文件获得Cache-Control: max-age=60,就这样

csript adsutil.vbs set W3SVC/1/root/images/HttpExpires "D, 0x3c"

设置images目录下的文件获得“Expires: Thu 27 Nov 2008 07:00:00 GMT”,

csript adsutil.vbs set W3SVC/1/root/images/HttpExpires "S, Thu 27 Nov 2008 07:00:00 GMT"

使用Metabase
Explorer

也可以修改

下载地址:


Last-Modified

这个基本不用更改,按默认生成的就好。

ETag


IIS
对Etag的计算算法是ETag = {Filetimestamp:ChangeNumber}, Filetimestamp为文件时间戳,ChangeNumber是metabase的change number

解决多服务器ETag同步方法:

1. 使所有IIS的 ETag设置一致。

设置方法详见参考文档3,4

2. 去掉ETag


除了写ISAPI,暂时没有找到其他IIS下去掉ETag的方法。


Apache相关实现及设置

Expries:

Cache-Control


具体设置见参考文档1

Last-Modified


同IIS

ETag

生成机制为:文件的inode(索引节点)、大小、最后修改时间决定,通过配置可以决定使用哪些 要素来生成。具体配置详见参考文档10

解决多服务器ETag同步方法:

1. 去掉ETag


使用配置 FileETag none


Apache对Etag的判断流 程为:(详见参考文档11)

首先判断是不是弱Etag。


如果不是,进入下面的情况:
强Etag 根据配置文件中的配置来设置Etag值,默认的Apache的FileEtag设置为:FileEtag、INode、Mtime、Size,也就是根据 这三个属性来生成Etag值,他们之间通过一些算法来实现,并输出成hex的格式,相邻属性之间用-分隔,比如:Etag "2e681a-6-5d044840"。这里面的三个段,分别代表了INode,MTime,Size根据算法算出的值的Hex格式。


当然,我们可以改变Apache的FileEtag设置,比如设置成FileEtag Size,那么得到的Etag可能为:Etag "6"


总之,设置了几个段,Etag值就有几个段。(不要误以为Etag就是固定的3段式)


说明

这 里说的都是Apache 2.2里面的Etag实现,因为HTTP/1.1并没有规定Etag必须是什么样的实现或者格式,因此你也可以修改或者完全编写自己的算法得到Etag, 比如"2e681a65d044840",客户端会记住并缓存下这个Etag,下次访问的时候直接拿这个值去和服务器生成的Etag对比。



当其为弱校验(弱Etag)时,重新考虑前面提到的3个问题:

问题1、一些文件也许会周期性的更改,但是他的内容并不改变(仅仅改变的修改时间),这个时候我 们并不希望客户端认为这个文件被修改了,而重新GET;

解决办法:如果使用强Etag,每次得会要求重新GET页面,如果使用Etag,比方说设置成 FileEtag Size等,就可以忽略MTime造成的Last-Modified时间修改从而影响了If-Modified-Since(IMS)这个校验了。这点和 弱Etag无关。


问题2、某些文件修改非常频繁,比如在秒以下的时间内进行修改,(比方说1s内修改了N 次),If-Modified-Since能检查到的粒度是s级的,这种修改无法判断(或者说UNIX记录MTIME只能精确到秒)

解 决办法:如果是这种情况,Apache会自动判断请求时间和修改时间之间的差值,如果小于1s,Apache会认为这个文件在这1秒内可能会再次被修改, 因此生成一个弱Etag(Weak Etag),这个Etag仅仅基于MTime来生成,因此MTime只能精确到s,所以1s内生成的Etag总是一样,这样就避免了使用强Etag造成的 1s内频繁的刷新Cache的情况。(貌似不用Etag,仅仅使用Last-Modified就可以解决,但是这针对的仅仅是修改超级频繁的情况,很多文 件可能同时也使用强Etag验证)。弱Etag以W/开始,比如:W/"2e681a"


问题3、某些服务器不能精确的得到文件的最后修改时间;

解决办法:生成Etag,因为Etag可以综合Inode,MTime和Size,可以避免这个 问题




IIS
的ETag 处理机制应该有相似之处。



至此我们较为全面 的介绍了HTTP/1.1协议中关于缓存的处理,以及用户代理,web服务器等相应实现策略。在进行静态文件处理,资源分离,反向代理过程中,很大程度上 要依赖于HTTP协议中缓存命令及各服务器的实现。可以说是我们进行前端优化的一个理论基础。


题外话:

在写这篇文章过程 中,需要查看HTTP/1.1协议,最先看的是英文版,但是阅读速度较慢,因此从网上搜索中文版,最后找到了三份。三份都翻译的很差。最后只能是相互对照 着来看。这么重要的文档,不明白为什么就没有很正式的官方翻译。






术语解释(详见参考文档7):

请求(Request)



一种HTTP 请求消息,参看 第5 章的定义。

应答(Response)


一种HTTP 应答消息,参看 第6 章的定义。

资源(Resource)


一种网络数据对象或 服务,可以用第3.2 节定义的URI 描述。资源可以以多种表现方式

(例如多种语 言,数据格式,大小和解决方案)或其他不同的途径获得。

实体(Entity)


作为请求或应答的有 效负荷而传输的信息.一个实体包含报头形式的维护信息和消息体形式的内容,由第7 节详述.

客户机(Client)


为发送请求建立连接 的程序.

用户代理(User agent)


初始化请求的客户端 程序.常见的如浏览器,编辑器,蜘蛛(网络穿越机器人),或其他的终端用户工具.

服务器(Server)


同意连接以便通过发 回应答为请求提供服务的应用程序.任何给定的程序都有可以既做客户端又做服务器;我们使用这些术语仅指特定连接中程序完成的任务,而不是指通常意义上程序的性能.同样,任何服务器都可以基于每个请求的性质扮演原服务器,代理,网管,或者隧道等诸角色之一。

原服务器(Origin server)


给定的资源驻留或创 建的地方.
保鲜 (Fresh)

如果一个应答的年 龄还没有超过保鲜寿命,它就是保鲜的.

陈旧 (Stale)

一个应答的年龄已 经超过了它的保鲜寿命,就是陈旧的.


参考文档:
1.
缓存友好的网页


http://shiningray.cn/cache-friendly-web-pages.html


2.
Config HTTP Header For Better Client Performance


http://hi.baidu.com/xletian/blog/item/756dcfce4ae17f0692457ed7.html


http://morganchengmo.spaces.live.com/blog/cns!9950CE918939932E!2132.entry


3.
The performance of a Web application may decrease, and the network bandwidth may increase after you add a Web server that is running IIS 5.0 to a Web farm that uses network load balancing


http://support.microsoft.com/kb/922733/en-us


4.
You may experience poor Web performance when you use Internet Explorer 6 to try to access a Web application that is hosted on Internet Information Services 6.0


http://support.microsoft.com/kb/922703/en-us


5.
High Performance Web Sites: Rule 13 – Configure ETags


http://developer.yahoo.net/blog/archives/2007/07/high_performanc_11.html


6.
面向站长和网站管 理员的Web缓存加速指南

http://www.mnot.net/cache_docs/#BROWSER
http://www.chedong.com/tech/cache_docs.html
中文翻译


7.
Http 1.1 Etag 与 Last-Modified


http://www.dbanotes.net/web/http_11_etag_lastmodified.html


8.
Hypertext Transfer Protocol -- HTTP/1.1

http://www.w3.org/Protocols/rfc2616/rfc2616.html

9.
The value in the ETAG field is updated when you modify a metabase property in IIS 6.0


http://support.microsoft.com/kb/900245/en-us


10.
Apache配置文档


http://doc.chinahtml.com/Manual/ApacheManual/mod/core.html


11.
浅谈Etag

http://bbs.chinaunix.net/thread-1186771-1-1.html

前端页面优化-HTTP/1.1协议中缓存处理和web服务器实现(一)

HTTP/1.1协议中缓存处理和web服务器实现

为了提供网站性能,很多地方都使用到缓存。此文章 主要从HTTP协议出发,谈论了
其缓存规范,以及在流行的Web服务器:IIS和Apache中是如何实现的。

HTTP/1.1 中缓存的目的:首先为了降低发送请求,其次是降低发送完 整响应。前者使HTTP请求/响应中往返流程减少,我们用"过期模型"来达到这 一目的(见13.2 节);后者使网络带宽需求降低,我们用"验证模型"机制来 达到这一目的(见13.3 节)。


在介绍这两个模型前,先让我们看下一个普通的 HTTP请求/响应流程。
HTTP请求资源大体流程过程如下:
1.
用户代理向源服务器发送第一次请求。
2.
源服务器进行响应,在第一次响应过程中会将一些报头域加入响应消息 中,其中和缓存相关的有:Age,Date,Expires,Cache-Control,Last-Modified,Etag等。
3.
当用户代理进行第二次请求时,浏览器会首请求资源的本地缓存,并且进 行检测看缓存是否过期。
检测方法:根据报头域Expires或者Cache-Control:max- age和实体年龄(并不是报头域Age,而是以Age等为基础计算得值。计算方法见下文)进行比较。
a)
如果缓存存在并 且是保鲜的,则不再向源服务器发送请求(减少了请求数,实现了第一个目的)。
b)
如果缓存过期,则用户代理会重新发送请求。发送时会将Last- Modified(如果存在)的值放入到If-Modified-Since/If-Unmodified-Since中,或者将Etag(如果存在)的 值放入到If-None-Match/If-Match中,从而形成两个报头域,用于源服务器的验证。
c)
如果缓存不存在(比如删除了IE临时文件),则用户代理会重新发送请 求,就和第一次发送请求的情况相同。
4.
源服务器再次接收到请求后,首先会检测并且验证请求报头域,检测内容 主要是看是否包含If-Modified-Since、If-Unmodified-Since、If-None-Match、If-Match等。
验证方法:
比较用户代理报头域中If-Modified-Since/If- Unmodified-Since的值和源服务器上文件的Last-Modified的值,检测文件是否更改过。
比较用户代理报头域中If-None-Match/If-Match的值和源服务器 上文件的ETag的值,检测是否相同。
a)
如果存在,并 且验证成功,则直接返回304(未更改)状态码,而不重新发送实体内容(节省了网络流量,实现了第二个目的)。
b)
如果不存在或 者验证不成功,则源服务器会重新读取实体,将新的内容发送给用户代理(同时附加新的报头域?)。
例外:如果用户直接点击刷新按钮,用户代理将不进行本地缓存的检测,将直接进入如上 所述步骤3-b,4的请求流程。
其大体流程图如下:



附件: 您所在的用户组无法下载或查看附件


在HTTP请求处理流程中,步骤3 使用的是过期模型,步骤4使用的是验证模型。下面是对这两个模型的简要介绍。两个模型的内容来自HTTP/1.1协议,但是为了便于理解,进行了一定程度 的删改。(详见参 考文档7)


过期模型(Expiration Model)

过期模型主要是指浏览器(客户代理User Agent)中的缓存处理机制。其主要原理为:源服务器在响应时会给实体加上相应的Date,以Date和客户端时间等一系列相关报头域计算出该实体年龄 (Age/Current_Age)。客户代理在请求时会将计算出的Age和Cache-Control:Max-age或者Expires进行比较,如 果没有年龄/没有超过最大年龄,或者没有超过过期时间,则认为该实体仍然是保鲜的,否则就认为其为过期的。

年 龄计算:


为了解缓存实体是否为最新,缓存器 需要知道其年龄是否已超过保鲜时限。我们在13.2.4 节中讨论如何计算后者,本节讨论如何计算响应或缓存实体的年龄。


在此讨论中我们用“now”来 表示“主机进行计算时时钟的当前值”。


使用HTTP 协 议的主机,除了运行源服务器和缓存器的主机,应当使用NTP[28]或其他类似协议来将其时钟同步到一个全球性的精确时间标准上来。


HTTP1.1
协议要求源服务器尽可能在发 送每条响应时都附加一个日期。报头来标明此响应产生的时间.(见14.18)我们用"日期值"这一短语来表示日期报头的值----一种适于算术操作的形式.


当从缓存获得响应消息时,HTTP1.1 用年龄响应报头来传达其年龄信息。


年龄值是缓存器估计的源服务器生成或重新确认响应的时间值。本质上,年龄值 是响应信息在从源服务器开始的所有缓存器驻留的时间加上其在网络路径上传输的时间.


我们用"age_value"来标明年龄报头的值----一种适于算术操作的表示方法.


一个响应的年龄可以通过两种完全独立的途径来计算:

1.
如果本地时 钟与源服务器时钟同步的相当好,则用"now"-日期值,若结果为负,则取零.

2.
如果从源服 务器开始的所有缓存器均执行HTTP1.1 则就取age_value


如上我们有两种方法计算响应的年龄,我们合并二者如下:


corrected_received_age = max(now - date_value, age_value)



无论那种方法都能得到可靠的结果。由于网络附加延时,一些重要时隙会在服务器产生响应和下一个缓存器或客户 收到它之间被忽略。如果不经修订,这一延迟会带来不正常的低年龄.

corrected_initial_age = corrected_received_age+ (now - request_time)


因为导致返回年龄值的请求一定在年龄值的产生之前就发出了,我们可以通过记录请求发出的时间来矫 正网络附加延时。因此当收到一个年龄值时,它必须与发出的请求时间有关,而于收到的响应时间无关。这将保证不论经历多少延时,其表现都是稳定的。


当缓存收到响应时的算法摘要:

/*

* age_value

* is the value of Age: header received by the cache with

* this response.

* date_value

* is the value of the origin server's Date: header

* request_time

* is the (local) time when the cache made the request

* that resulted in this cached response

* response_time

* is the (local) time when the cache received the

* response

* now

* is the current (local) time

*/

apparent_age = max(0, response_time - date_value);

/*
response_time = now?
*/

corrected_received_age = max(apparent_age, age_value);

response_delay = response_time - request_time;

corrected_initial_age = corrected_received_age + response_delay;

resident_time = now - response_time;

current_age = corrected_initial_age + resident_time;


以上段落英文更明白一些。

缓存实体的当前年龄是从缓存实体最后被服务器确认的时间(以秒 记)加上校正初始年龄。当缓存实体产生一条响应,它必须包含一个年龄报头区与缓存实体当前年龄一样的值。


注:年龄的计算公式看的比较晕,主要不明白各个时间点,以及相应的时间计算。

过期计算:

为了确定一条响应是新是旧,我们需要将其保鲜期限(freshness_lifetime)和年龄进行比较(年龄计算见上)本节讲解怎样计算保鲜期限,以及一条响应是否已经被排出。

freshness_lifetime计算方式为:

freshness_lifetime = max_age_value(Cache-Control:Max-Age/s- Max-Age值)

或者

freshness_lifetime = expires_value(Expires域)- date_value(Date域)

注:Max-Age优先于Expires进行计算


如果 Expires, Max-Age, s-Max-Age均未在响应中出现,且响应对缓存没有其他限制, 缓存可以用启发式算法计算freshness_lifetime。如果响应有最后修改时间,启发式过期值应不大于时间片。 典型设置为片断的10%

freshness_lifetime =(last_modified_value – date_value)* 10%


此公式在协议中并未出现,是根据协议内容推理出的。其他的启发 式算法HTTP协议也没有给出。换句话说:即 使不设置Expires, Max-Age,IE缓存器 也会使用启发式算法来设置?此想法需要讨论。


接下来计算响应是否过期就非常简单:

response_is_fresh = (freshness_lifetime > current_age)。不用过多解释吧o(∩_∩)o…


验证模型(Validation Model)

当用户代理想要用一个失时效的条目来响应客户请求,他首先必须向源服务器(也可能是中间缓存器)检验这一缓存条目是否仍然可用.我们称之为"验证"缓存条目.

由于我们不想当缓存条目为可用时必须为再传送整条响应而付出代价,而且不想当缓存条 目不可用时也必须多传一圈,HTTP1.1 协议支持使用条件反应方法。协议支持条件响应 方法的关键特征围绕"缓存验证器"展开。当源服务器生成一个 完整响应时,它同时传送一类验证器一直伴随着缓存条目。当一客户(用户代理或代理缓存器)对含有缓存条目的资源做出条件请求时,他同时在请求中包 含有相互关联的验证器.


服务器则核对此验证器和当前验证器,如果他们匹配(见13.3.3),则返回一个特定状态码(通常为304)且不含条目内容。否则,返回整个响应(包含条目内容)这样,我们避免了在验证器 匹配时传送整条响应,同时也避免了在不匹配时往返传输。

在HTTP/1.1 协议中,一个条件请求除了带 有特别的报头(包含验证器)来暗中的将它转入条件算法以外,和普通报头没有差别。

协议中包括缓存确认机制的主动和被动两种状态。具体说来,请求可以在当且仅 当又匹配确认时执行,也可以在当且仅当没有匹配确认时执行。


HTTP/1.0只有一种验证 器:Last-Modified

HTTP/1.1有两种验证 器:Last-Modified,ETag


13.3.1 最后修改日期 (Last-Modified Dates)

最后修改报头值经常被用作验证器.简言之,一缓存条目在最后修改期后未经修改则被认为是有效的.

13.3.2 标签缓存验证器(Entity Tag Cache Validators)