Zee注:虽然不是很复杂,也算是个XML的应用吧。 :)
Bryn Waibel 和 John Boylan
2000年7月10日
MSDN Library 是 MSDN 的信息中心库,包括超过 250,000 个文件,目录 (TOC) 是此类数据的主要访问点。
但是 MSDN 库中的 TOC 工作并不尽如人意,因此需要废弃和替换 — 越快越好。
包括 Bryn Waibel 和 Mike Barta 在内的 MSDN Web 设计小组,被委托创建新的 TOC。
所幸的是,解决方案非常直接:新的 TOC 使用 JavaScript 将 XML 作为数据层加以覆盖。
我们将在本文中向您展示如何完成这项工作,并包括一个可以作为设计模板的 TOC 示例。
我们希望 TOC 能够提供跨平台访问,改善的可维护性以及全球化所需的扩展字符集。
实际情况
如果以上问题简单的话,它同时也很严重 — 而且有可能恶化。所幸的是,答案很明确。
新的 TOC 必须能处理大型数据集以及许多不同来源的数据。
我们希望 TOC 能够提供跨平台访问,改善可维护性(使用标准的数据存储格式)以及全球化所需的扩展字符集。
我们还需要从 Library 的任何地方直接链接到 TOC 的任何节点上。
旧的 TOC 很难维护的原因是 Java 小程序的数据源链接不佳。需要数据源来说明超过 200 个不同数据集之间的关联,
将这些数据集链接在一起而不需要同时将其发送给用户的唯一方法是:将给定数据集手工复制到层次结构中。
当为 Web 构建的世界范围应用程序还处在初级阶段时,小程序还不能提供扩展的字符支持。这些小程序无法适应浏览器技术的进步以满足日益增长的语言与相应操作系统匹配的要求。
例如,对于 Java 小程序来说,要想查看 shift-jis 字符集,就只能使用缺省 ANSI 字符集为 shift-jis 集的操作系统中的浏览器。
同样,Java 小程序也不能处理诸如 UTF-8 的 Unicode 字符集。
此外,用户与最初 TOC 的交互变得缓慢而笨拙。更何况不能从一台 Macintosh 机访问 TOC,这就很大地限制了 Macintosh 用户的访问。
最后,TOC 使用的是一个专用数据结构。必须生成新的 Java 小程序专用数据结构,才能重用 TOC 源。
弄清实际情况
了解了我们就此模型缺少的内容,我们就可以针对我们须有的内容开始工作。开始探讨一些关键设计事项:
XML 结构 我们知道需要一个 XML 数据层。不管考虑何种解决方案,答案总是最终归结到 XML。
数据分块 数据必须分成足够小的块,以便任何连接情况下都可以发送给客户机。Windows 2000 小组试图在将 Windows 2000 帮助文件置为联机时遇到了相似的问题,
因此他们的代码显然只是一个开端。
与一个节点同步 TOC 中的每个节点必须可被发现,它只知道它所指向的 .htm 页。此数据必须可更新,并独立于 TOC 中不相关的更改。
划分供应商 MSDN Library 的数据来源于成百上千的供应商,包括多种格式:.rtf、.chm、.htm 等等。需要一种方式将它们都映像在一起并保持各自的独立性。
重用性 数据需要可重复使用。我们不希望在不同视图中显示同一个数据时不得不重新编辑 TOC 的某个部分。
在介绍 TOC 的生成过程后,将回过头来讨论这些问题并说明它们怎样影响最终的设计。
生成目录
现在我们以一个框架作为开始,共同推敲我们的 TOC。将所有片段 — XML、JavaScript、XSL 和 .asp 文件 — 拿来形成一个整体。
TOC XML 结构
创建 TOC 所面临的关键问题是结构的大小。由于整个 MSDN TOC 太大,以致不能整块发送给客户机。现在的 TOC 包含 250,000 个节点;按每个节点 150 个字节计算,其总量超过 37 MB,不能一下发送给用户。
因为主要考虑结构大小,所以我们尝试使文档类型定义 (DTD) 在不牺牲可读性的情况下尽可能地简练。同时我们还采用以属性为中心而不是以元素为中心的方法。这样,我们可以合并数据并给出严格遵守 TOC 中数据类型的 XML 结构。以属性为中心的方法提供 TOC 中节点对节点的联系,而以元素为中心的方法会导致 XML 的很多节点代表 TOC 中的同一节点。
对于我们的模型,我们从 HTML Help 2.0 中最新版本 .hxt 文件的 DTD 开始,致力于创建一个适合于我们的工作方案。正如以下代码所示,将每个节点命名为 MTN(MSDN TOC 节点),并且每个节点都包含 0 和更多个子 MTN 节点。我们添加了大量属性,这些属性不一定是实现 MSDN Library 所必需的,但却对说明我们站点上的所有类型 TOC 有用处。
Library 属性
以下是与 MSDN Library 的 TOC 直接相关的属性:
nodeType 属性有三个可能的值:
节点 此节点有子节点,子节点出现在此文件中。
页 这是一个终端节点;它没有子节点。
集合 此节点有子节点,但它们位于不同的文件中。
prePartum 属性引用一个可以在其中找到子节点的文件。此属性值是某个通向特定文件的路径,此文件在其根目录下含有指向它的节点的精确备份 — 除非其的 nodeType 值是 “node”,并且其子节点存在。对 XML Document Object Model (DOM) 来说,执行的操作是:通过 prePartum 装入所指向的文件并用该文件的顶部节点代替当前节点。
tocPath 属性包含 TOC 中给定节点零索引路径。每个节点索引通过虚线与其它索引分开,以便能从字符串表中容易地提取并直接插入到 XML DOM 中。TOC 中第一个节点的 tocPath 值为 0,第二个子节点的 tocPath 值为 0-1 依此类推 — 从而形成一个到 TOC 的顶端任何节点的直接路径。我们还给 tocPath 值的起始处添加了一个标记,以表明路径相关的区域。在 Library 的顶端,此标记是“lib”。我们将在以后讲到如何维护每个目录供应商的独立性时进一步讨论这一问题。
state 属性是显示驱动的属性;如果没有状态,节点将以其默认状态处理。节点可以有两个状态:
sel 这是节点在树中的当前节点。如果节点指向目录;将以粗体显示;如果 nodeType="node",节点将显示为打开状态。
open 此状态表明节点位于当前节点的路径中。只有 nodeType="node"的节点才能有此属性。
ref 属性包含指向相关节点所引用数据的路径。在 Library 中,这一路径是指向 .htm 或 .asp 文件的 URL。
title 属性包含当前节点的显示名。
数据分块
我们开始进行 Windows 2000 小组所从事的工作即将其帮助文件置成联机。Windows 2000 小组使用 XML 保持其数据,并脱机构建文件,形成 HTML 解决方案 — 在将数据发送给用户之前做大部分的构建逻辑工作。
在 Windows 2000 解决方案中,用户看到的内容以 Netscape 版本和 Internet Explorer 版本严格地保存在文件系统中。但这种方法有明显的缺陷 — 如需要构建多种解决方案,而每种只能满足不同客户机平台的需要。
性能考虑是脱机构建帮助文件的主要原因之一,特别是在 Windows 2000 最终发行版发布之前。Windows 2000 已在 XSL 转换和 XML DOM 处理方面进行了很多显著的改进。因此我们愿意再看看数据处理的情况。结果是,我们的 TOC 将 XML 数据存储在文件系统中,该数据随后会在服务器上依用户查看的需要加一转换。
与一个节点同步
与一个节点同步是设计此 TOC 时遇到的最棘手的问题。Windows 2000 小组使用连续的数字下标索引其节点,但这对我们来说确实是个问题,因为我们不想在每次添加和替换节点时更改变整个同步架构。同时,由于集合里有 250,000 个节点,我们也不想将同步信息都存储在一个地方;我们需要某种散列机制。一个显而易见的选择是将数据存储在给定文件的路径中,该文件由文件夹中的文件名加以索引。我们决定将信息存储在一个称为映像文件的 XML 文件中。<!DOCTYPE MsdnTocMap [<!ELEMENT MsdnTocMap (L+)>
<!ATTLIST MsdnTocMap rootToc CDATA #IMPLIED>
<!ELEMENT L EMPTY>
<!ATTLIST L url ID #REQUIRED pth CDATA #REQUIRED>]>
可以看到,映像文件非常简单,也非常有用。映像文件是使用 XML 创建名称/值对的一种方法。每个节点都由一个作为名字的文件名和一个作为值的 tocPath 组成,将该名称放入 ID 属性的 URL 中,而将值放入 CDATA 属性的 pth 中。
只给定一个文件的路径和文件名我们可以打开该目录下相应的 map.xml 文件并使用文件名查找到该节点的路径,这需要使用在其中装入了该文件的 XMLDOMDocument 对象的 nodeFromID。使用在 toc.asp 文件中定义的 formatFileName (strFName) 函数对文件名进行换码,如以下代码所示。function FormatFileName( sFileName )
{
if( "string" == typeof( sFileName ) )
{// 删除非字母字符、句号、虚线、下划线或括号(ID 属性是必需的)之外的所有字符
sFileName = sFileName.replace( /[^\w\.-_:]/gi , "" );
// 不能以数字开头
if ( sFileName.match( /^[\.\d]/i ) ) sFileName = "f" + sFileName;
return sFileName; }}
划分供应商
为了保持目录集之间的独立性,我们使用某个标记来表示每个集合。我们在 tocPath 说明中谈到的标记在下面发挥作用。
为每个供应商创建一个名称,然后通过在每个标记末尾添加一个唯一的整数来命名供应商树中顶部的节点。为了简单实现,使用从 0 开始的连续整数。从该供应商目录顶部节点的映像文件引用 tocPaths。然后我们将有一个 submap.xml 文件,它将每个标记从 TOC 的顶部映像到其各自的位置。存储此数据的文件只是另一个映像文件 — 文件的 ref 属性包含标记名,而 pth 是该标记的路径。现在我们可以相对其它片段来左右移动目录片段,这只需要更新主标记映像文件。也可以在一个目录块内部移动目录而不打乱块外部的其它节点顺序,其它好处就是通过引用与顶层不同的同一数据,可以随心所欲地生成数据视图。
应用程序示例
看完 XML 如何在解决方案中起作用之后,接下来要讨论以上片段如何集中在一起生成应用程序。我们将通过概要介绍附带的应用程序示例来完成这项工作。如果想建一个记录 TOC 技术的节点,表示一个包括 TOC 代码的一个应用程序示例,那么希望它看起来如下所示:
图 1. TOC 示例:“代码中心”中的 MSDN TOC 节点
假设您希望将“MSDN 代码示例”下的节点作为一个单个元素,那么需要两个表示此结构的 XML 文件: Msdnce.xml 和 Ltoc.xml。
msdnce.xml<MsdnToc>
<MTN title="MSDN Code Examples" nodeType="node" type="none"
tocPath="msdnce-0" hal="en-us">
<MTN title="The MSDN TOC" nodeType="collection" type="none"
tocPath="ltoc0" hal="en-us" prePartum="ltoc0.xml"/>
</MTN>
</MsdnToc>
msdnce.xml 文件非常简单;它只是控制所有 MSDN 代码示例的一个节点。
这种控制包括一个指向我们的示例 Ltoc0.xml 的单个集合节点。
ltoc.xml<MTN title="The MSDN TOC" nodeType="node" type="none" tocPath="ltoc0" hal="en-us">
<MTN title="Revamping MSDN TOC" nodeType="leaf" type="file"
ref="library.asp"?tocPath="ltoc0-0"/>
<MTN title="ASP Fles" nodeType="node" type="none"?tocPath="ltoc0-1">
<MTN title="loadtree.asp" nodeType="leaf" type="file"
ref="loadtree.asp.txt"?tocPath="ltoc0-1-0" />
<MTN title="toc.asp" nodeType="leaf" type="file"
ref="toc.asp.txt"?tocPath="ltoc0-1-1" />
<MTN title="default.asp" nodeType="leaf" type="file"
ref="default.asp.txt"?tocPath="ltoc0-1-2" />
</MTN>
<MTN title="CSS Files" nodeType="node" type="none"?tocPath="ltoc0-2" >
<MTN title="toc.css" nodeType="leaf" type="file"
ref="toc.css.txt"?tocPath="ltoc0-2-0" />
</MTN>
<MTN title="JS Files" nodeType="node" type="none"?tocPath="ltoc0-3" >
<MTN title="toc.js" nodeType="leaf" type="file"
ref="toc.js.txt"?tocPath="ltoc0-3-0" />
</MTN>
<MTN title="Include Files" nodeType="node" type="none"?tocPath="ltoc0-4" >
<MTN title="locals.inc" nodeType="leaf" type="file"
ref="locals.inc"?tocPath="ltoc0-4-0" />
</MTN>
<MTN title="XML Files" nodeType="node" type="none"?tocPath="ltoc0-5" >
<MTN title="msdnce.xml" nodeType="leaf" type="file"
ref="msdnce.xml"?tocPath="ltoc0-5-0" />
<MTN title="ltoc0.xml" nodeType="leaf" type="file"
ref="ltoc0.xml"?tocPath="ltoc0-5-1" />
<MTN title="map.xml" nodeType="leaf" type="file"
ref="map.xml"?tocPath="ltoc0-5-2" />
<MTN title="submap.xml" nodeType="leaf" type="file"
ref="submap.xml"?tocPath="ltoc0-5-3" />
</MTN>
<MTN title="XSL Files" nodeType="node" type="none"?tocPath="ltoc0-6" >
<MTN title="toc.xsl" nodeType="leaf" type="file"
ref="tocdown.xsl"?tocPath="ltoc0-6-0" />
</MTN>
<MTN title="Graphics Files" nodeType="node" type="none"?tocPath="ltoc0-7" >
<MTN title="bo.gif" nodeType="leaf" type="file" ref="bo.gif"?tocPath="ltoc0-7-0" />
<MTN title="bs.gif" nodeType="leaf" type="file" ref="bs.gif"?tocPath="ltoc0-7-1" />
<MTN title="dc.gif" nodeType="leaf" type="file" ref="dc.gif"?tocPath="ltoc0-7-2" />
</MTN>
</MTN>
我们已经掌握了命名两个集合的特权:ltoc 是“划分供应商”中提到的供应商标记之一;而 msdnce 是代表此数据视图名的顶级标记名。Ltoc0.xml 比 msdnce.xml 更有意义;它是 TOC 对此示例的表示。请记住在我们的命名系统中,ltoc0 是 ltoc 集合的第一个节点。
至此,我们可以很容易地看到可重复使用性是如何起作用的。我们可以从站点的任何其它 TOC 指向 ltoc0.xml,而全部要做的只是建一个如同 msdnce 文件中的二级节点的节点。我们还可以在 MSDN 代码示例节点下添加任意多的示例,并保持示例之间完全独立。
map.xml
map.xml 文件将目录映射到其子树内的路径中。特别有趣的是每个节点都是通过 ltoc0 子标记被引用的。这使得需要使用 ltoc0 数据的任何应用程序都指向同一个 ltoc0.xml 文件。XML 用户只需将自己的映像提供给 ltoc0,map.xml 文件会从 lotc0 自行提取。<?xml version="1.0"?>
<!DOCTYPE MsdnTocMap [<!ELEMENT MsdnTocMap (L+)>
<!ATTLIST MsdnTocMaprootToc CDATA #IMPLIED>
<!ELEMENT L EMPTY>
<!ATTLIST Lurl ID #REQUIREDpth CDATA #REQUIRED>]>
<MsdnTocMap>
<L url="library.asp"?pth="ltoc0-0"/>
<L url="loadtree.asp.txt"?pth="ltoc0-1-0" />
<L url="toc.asp.txt"?pth="ltoc0-1-1" />
<L url="default.asp.txt"?pth="ltoc0-1-2" />
<L url="toc.css.txt"?pth="ltoc0-2-0" />
<L url="toc.js.txt"?pth="ltoc0-3-0" />
<L url="locals.inc"?pth="ltoc0-4-0" />
<L url="msdnce.xml"?pth="ltoc0-5-0" />
<L url="ltoc0.xml"?pth="ltoc0-5-1" />
<L url="map.xml"?pth="ltoc0-5-2" />
<L url="submap.xml"?pth="ltoc0-5-3" />
<L url="tocdown.xsl"?pth="ltoc0-6-0" />
<L url="bo.gif"?pth="ltoc0-7-0" />
<L url="bs.gif"?pth="ltoc0-7-1" />
<L url="dc.gif"?pth="ltoc0-7-2" />
</MsdnTocMap>
submap.xml
submap.xml 文件是每个 XML 用户都保留的映像;它提供数据片段之间的联系。由于此视图只指向一组数据 (ltoc0),所以只有一个节点。注意到有可能在 ltoc0 内定义子区域也非常重要;在 submap.xml 中子区域的路径只相对 ltoc0 引用。假如想在其它视图中包括同一 .css 节点,则可以把此节点称为 toccss,并在 submap.xml 文件中将其引用为 ltoc0-2 — 从此视图中可以被转换为真实路径 msdnce-0-0-2。<?xml version="1.0"?>
<!DOCTYPE MsdnTocMap [<!ELEMENT MsdnTocMap (L+)>
<!ATTLIST MsdnTocMaprootToc CDATA #IMPLIED>
<!ELEMENT L EMPTY>
<!ATTLIST Lurl ID #REQUIREDpth CDATA #REQUIRED>]>
<MsdnTocMap>
<L url="ltoc0"?pth="msdnce-0-0" />
</MsdnTocMap>
locals.inc
locals.inc 文件包括此视图的所有信息,并被应用程序中的每个 .asp 文件所引用。对于我们的应用程序示例,该文件形式如下:<%// default.asp 的标题var L_HomePageTitle_HTMLText= "MSDN TOC Code Example";
// 文本方向设置(从右到左是 RTL)
var L_strBiDiMode_HTMLText = "LTR";
// 正在装入消息文本
var L_LoadingMsg_HTMLText = "Loading, click to cancel... ";
// 源信息
var RootDir = "msdnce.xml";
var DefaultTopic = "library.asp";
var SubMapPath = "subMap.xml";
var RootTocToken = "msdnce";%>
ASP
在 TOC 中有两个主要 .asp 负责 XML 处理和准备为客户机显示数据:loadtree.asp 和 toc.asp。
loadtree.asp
loadtree.asp 文件将 prePartum 和 tocPath 作为参数并输出驻留在指定的 tocPath 中的子节点。有简单和复杂两种途径实现这一操作。当需要的节点在文件的顶部由 prePartum 引用时,使用简单的方法。此时就像把文件装入 DOMDocument 对象一样简单,将它与 tocDown.xsl 一起转换,并将结果输出到页面中。一旦结果输出到页面,用户接口就可以开始对其进行处理。
当 loadtree.asp 必须在 prePartum 引用的问文件内部查找某个节点时,会出现第二种情况,也就是更为复杂的情况。当提供给 Loadtree.asp 的 tocPath 属性与装入文档顶部的 tocPath 参数不一致时,会出现这种情况。出现这种情况时,loadtree.asp 会以查询字符串的形式从传递给它的 tocPath 参数中清除 tocPath 参数(位于 XML 文件的顶部节点)。loadtree.asp 文件随后使用其余片段作为查找的路径。片段用完时,它会找到要找的节点。然后复制此节点(样式表需要从文档的根目录运行),转换副本,并将结果输送到页面。
toc.asp
toc.asp 文件将 ref 和 tocPath 都作为参数,装入顶级 TOC,并将所有数据从顶级向下输出到 ref 所引用的节点。如果它能找到被 ref 所引用的节点路径,它将遍历整个树结构,删除不需要的子树,直至到达该节点。如果没有找到,则它就输出到顶级。
XSL
通过将 XSL 转换为 XHTML,XSL 在浏览器中被用来显示 XML。服务器能够为每个的客户机配置选择不同的 .xsl 文件。XSL 文件在一定程度上非常相似,因为它们都有一个递归模板机制,可以将 XML 转换为 HTML 发送给用户。此机制类似于以下形式:<xsl:template match="MTN[@nodeType='node']">
<!-- HTML for node -->
<xsl:apply-templates select="MTN"/>
</xsl:template>
<xsl:template match="MTN[@nodeType='collection']">
<!-- HTML for collection --></xsl:template>
<xsl:template match="MTN[@nodeType='leaf']">
<!-- HTML for leaf --></xsl:template>
JavaScript 和 CSS
每个浏览器的 JavaScript 和层叠样式表 (CSS) 也可以不相同。附带示例中的版本只能在 Internet Explorer 4.0 和以后版本中运行 TOC。由 .xsl 文件生成的 HTML 为 .asp 文件完成大部分的信息传送,.asp 文件负责在其它浏览器中导航并将 TOC 拼凑成一体。
从这里取走
这是 MSDN Library 目录。它是您构建自己 TOC 的起点,特别是如果希望连接不同文件的组并希望方便地访问这些文件。我们设计 TOC 的目的是在您想要用 XML 处理分层数据的任一情况下提供各种模型 — 从目录到组织图表和示意图解。
Andrew Clinick 是“Microsoft Script Technology”组中的程序经理,机会使然,只要涉及脚本,他就可能大显身手一番。他的大部分业余时间都花在观看美国电视台转播的精彩橄榄球赛,以及向他的美国同事解说板球。